ServerManagement: add Log Level support, simplified logging, "-noannounce" switch...
authorEddie <dev@fun2be.me>
Sat, 6 Jun 2015 10:13:31 +0000 (11:13 +0100)
committerEddie <dev@fun2be.me>
Sat, 6 Jun 2015 10:13:31 +0000 (11:13 +0100)
Log Level support. Add radio buttons under menu Log > Level to allow
setting of the maximum log level to record.

Use simplified project-standardised logp().

Introduce -noannouce command-line switch to disable multicast
announcements. This is the inverse of "-announce".

Move some variables from Class to Method local scope, and visa versa.

src/uk/ac/ntu/n0521366/wsyd/management/ServerManagement.form
src/uk/ac/ntu/n0521366/wsyd/management/ServerManagement.java

index fd295d9..4d32572 100644 (file)
@@ -2,7 +2,7 @@
 
 <Form version="1.5" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JFrameFormInfo">
   <NonVisualComponents>
-    <Component class="javax.swing.ButtonGroup" name="buttonGroup1">
+    <Component class="javax.swing.ButtonGroup" name="buttonGroupLogLevels">
     </Component>
     <Container class="javax.swing.JDialog" name="gDialogAbout">
       <Properties>
                 <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="gMenuLogServiceAnnounceActionPerformed"/>
               </Events>
             </MenuItem>
+            <Menu class="javax.swing.JMenu" name="gMenuLogLevel">
+              <Properties>
+                <Property name="text" type="java.lang.String" value="Level"/>
+              </Properties>
+              <SubComponents>
+                <MenuItem class="javax.swing.JRadioButtonMenuItem" name="gMenuLogLevelAll">
+                  <Properties>
+                    <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
+                      <ComponentRef name="buttonGroupLogLevels"/>
+                    </Property>
+                    <Property name="selected" type="boolean" value="true"/>
+                    <Property name="text" type="java.lang.String" value="ALL"/>
+                  </Properties>
+                  <AuxValues>
+                    <AuxValue name="JavaCodeGenerator_InitCodePost" type="java.lang.String" value="gMenuLogLevelAll.addActionListener(logLevelEvent);"/>
+                  </AuxValues>
+                </MenuItem>
+                <MenuItem class="javax.swing.JRadioButtonMenuItem" name="gMenuLogLevelFinest">
+                  <Properties>
+                    <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
+                      <ComponentRef name="buttonGroupLogLevels"/>
+                    </Property>
+                    <Property name="text" type="java.lang.String" value="FINEST"/>
+                  </Properties>
+                  <AuxValues>
+                    <AuxValue name="JavaCodeGenerator_InitCodePost" type="java.lang.String" value="gMenuLogLevelFinest.addActionListener(logLevelEvent);"/>
+                  </AuxValues>
+                </MenuItem>
+                <MenuItem class="javax.swing.JRadioButtonMenuItem" name="gMenuLogLevelFiner">
+                  <Properties>
+                    <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
+                      <ComponentRef name="buttonGroupLogLevels"/>
+                    </Property>
+                    <Property name="text" type="java.lang.String" value="FINER"/>
+                  </Properties>
+                  <AuxValues>
+                    <AuxValue name="JavaCodeGenerator_InitCodePost" type="java.lang.String" value="gMenuLogLevelFiner.addActionListener(logLevelEvent);"/>
+                  </AuxValues>
+                </MenuItem>
+                <MenuItem class="javax.swing.JRadioButtonMenuItem" name="gMenuLogLevelFine">
+                  <Properties>
+                    <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
+                      <ComponentRef name="buttonGroupLogLevels"/>
+                    </Property>
+                    <Property name="text" type="java.lang.String" value="FINE"/>
+                  </Properties>
+                  <AuxValues>
+                    <AuxValue name="JavaCodeGenerator_InitCodePost" type="java.lang.String" value="gMenuLogLevelFine.addActionListener(logLevelEvent);"/>
+                  </AuxValues>
+                </MenuItem>
+                <MenuItem class="javax.swing.JRadioButtonMenuItem" name="gMenuLogLevelConfig">
+                  <Properties>
+                    <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
+                      <ComponentRef name="buttonGroupLogLevels"/>
+                    </Property>
+                    <Property name="text" type="java.lang.String" value="CONFIG"/>
+                  </Properties>
+                  <AuxValues>
+                    <AuxValue name="JavaCodeGenerator_InitCodePost" type="java.lang.String" value="gMenuLogLevelConfig.addActionListener(logLevelEvent);"/>
+                  </AuxValues>
+                </MenuItem>
+                <MenuItem class="javax.swing.JRadioButtonMenuItem" name="gMenuLogLevelInfo">
+                  <Properties>
+                    <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
+                      <ComponentRef name="buttonGroupLogLevels"/>
+                    </Property>
+                    <Property name="text" type="java.lang.String" value="INFO"/>
+                  </Properties>
+                  <AuxValues>
+                    <AuxValue name="JavaCodeGenerator_InitCodePost" type="java.lang.String" value="gMenuLogLevelInfo.addActionListener(logLevelEvent);"/>
+                  </AuxValues>
+                </MenuItem>
+                <MenuItem class="javax.swing.JRadioButtonMenuItem" name="gMenuLogLevelWarning">
+                  <Properties>
+                    <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
+                      <ComponentRef name="buttonGroupLogLevels"/>
+                    </Property>
+                    <Property name="text" type="java.lang.String" value="WARNING"/>
+                  </Properties>
+                  <AuxValues>
+                    <AuxValue name="JavaCodeGenerator_InitCodePost" type="java.lang.String" value="gMenuLogLevelWarning.addActionListener(logLevelEvent);"/>
+                  </AuxValues>
+                </MenuItem>
+                <MenuItem class="javax.swing.JRadioButtonMenuItem" name="gMenuLogLevelSevere">
+                  <Properties>
+                    <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
+                      <ComponentRef name="buttonGroupLogLevels"/>
+                    </Property>
+                    <Property name="text" type="java.lang.String" value="SEVERE"/>
+                  </Properties>
+                  <AuxValues>
+                    <AuxValue name="JavaCodeGenerator_InitCodePost" type="java.lang.String" value="gMenuLogLevelSevere.addActionListener(logLevelEvent);"/>
+                  </AuxValues>
+                </MenuItem>
+                <MenuItem class="javax.swing.JRadioButtonMenuItem" name="gMenuLogLevelOff">
+                  <Properties>
+                    <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
+                      <ComponentRef name="buttonGroupLogLevels"/>
+                    </Property>
+                    <Property name="text" type="java.lang.String" value="OFF"/>
+                  </Properties>
+                  <AuxValues>
+                    <AuxValue name="JavaCodeGenerator_InitCodePost" type="java.lang.String" value="gMenuLogLevelOff.addActionListener(logLevelEvent);"/>
+                  </AuxValues>
+                </MenuItem>
+              </SubComponents>
+            </Menu>
           </SubComponents>
         </Menu>
         <Menu class="javax.swing.JMenu" name="gMenuServers">
index cafcc64..5dbfa01 100644 (file)
@@ -1,7 +1,12 @@
 /*
  * The MIT License
  *
- * Copyright 2015 Eddie Berrisford-Lynch <dev@fun2be.me>.
+ * Copyright 20
+    @Override
+    public void eventDispatched(AWTEvent event) {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+15 Eddie Berrisford-Lynch <dev@fun2be.me>.
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
  */
 package uk.ac.ntu.n0521366.wsyd.management;
 
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.Desktop;
 import java.io.FileWriter;
 import java.io.BufferedWriter;
 import java.io.File;
-import javax.swing.ImageIcon;
-import java.awt.Desktop;
+import java.io.IOException;
 import java.net.URI;
 import java.net.InetAddress;
-import java.io.IOException;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
 import java.net.DatagramPacket;
 import java.net.DatagramSocket;
 import java.net.InetSocketAddress;
-import javax.swing.Timer;
-import javax.swing.table.DefaultTableModel;
 import java.net.UnknownHostException;
-import java.util.logging.Logger;
-import java.util.logging.Level;
-import java.util.logging.LogRecord;
-import java.util.logging.Filter;
-import java.util.Date;
 import java.text.SimpleDateFormat;
 import java.text.MessageFormat;
+import java.util.logging.*;
+import java.util.Date;
 import java.util.ArrayList;
+import javax.swing.ImageIcon;
+import javax.swing.Timer;
+import javax.swing.table.DefaultTableModel;
 import uk.ac.ntu.n0521366.wsyd.libs.logging.TableModelHandler;
-import uk.ac.ntu.n0521366.wsyd.libs.message.MessageLogRecord;
-import uk.ac.ntu.n0521366.wsyd.libs.message.MessagePresence;
-import uk.ac.ntu.n0521366.wsyd.libs.message.MessageServerControl;
-import uk.ac.ntu.n0521366.wsyd.libs.net.WSYD_SocketAddress.Protocol;
+import uk.ac.ntu.n0521366.wsyd.libs.message.*;
 import uk.ac.ntu.n0521366.wsyd.libs.net.*;
-import uk.ac.ntu.n0521366.wsyd.libs.net.ServiceAddressMap.LastSeenHost;
 /**
+ * GUI client that receives log messages from the servers (and clients) and can restart/stop the servers.
+ * 
+ * Log messages are received over UDP encapsulated in {@see uk.ac.ntu.n0521366.wsyd.libs.net.NetworkMessage} objects and displayed in
+ * a scrolling table. Log messages are sent from remote hosts using the {@see uk.ac.ntu.n0521366.wsyd.libs.logging.PacketHandler} attached to
+ * each remote host's Logger instance.
+ * 
+ * Remote hosts discover the Log Service by watching for a multicast announcement from this application. Multicast
+ * can be enabled and disabled on the Log menu. Remote hosts will log to their local console until this Log Service
+ * is discovered.
  *
+ * The level of logging can be changed from ALL to OFF. The table can be set to automatically scroll.
+ * 
  * @author Eddie Berrisford-Lynch <dev@fun2be.me>
  */
 public class ServerManagement extends javax.swing.JFrame implements NetworkMessageEventListener, Filter {
     /**
-     * class-wide logger
+     * Handles display and sending of log messages.
      */
-    Logger LOGGER = null;
-    
+    private  Logger LOGGER;
+
     /**
      * Location of resource bundles, images, icons
      */
@@ -72,23 +81,19 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
     /**
      * Readable/displayable name of this application
      */
-    final String _title = "ServerManagement";
+    private final String _title = "ServerManagement";
 
     /**
-     * Network services to address map.
+     * Name of the network log server to advertise with multicast neighbour discovery
      */
-    ServiceAddressMap _serviceToAddressMap;
+    private final String logServiceName  = "LogService";
 
     /**
-     * The UDP listener address for incoming log messages
+     * Network services to address map.
      */
-    WSYD_SocketAddress _udpLogServiceSA = null;
+    ServiceAddressMap _serviceToAddressMap;
+
 
-    /**
-     * UDP multi-cast presence advertiser
-     */
-    WSYD_SocketAddress _multicastAdvertiserSA = null;
-    
     /**
      * Log service running in a SwingWorker thread.
      */
@@ -98,35 +103,59 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
      * Multi-cast neighbour advertise and discover service in a SwingWorker thread.
      */
     NetworkServerUDPMulticast _multicastServer = null;
+
+    /**
+     * Whether process should announce itself using multicast.
+     * 
+     * Provides neighbour discovery without DNS or manual IP address configuration.
+     */
     boolean _multicastAnnouncements = false;
 
     /**
      * Enable or suspend logging
      */
     private boolean _doLogging;
-    
+
     /**
      * Enable or suspend table auto-scroll
      */
     private boolean _autoScroll;
-    
+
     /**
      * Regular presence announcements
      */
-    Timer regularTasks;
+    Timer _regularTasks;
+
+    java.awt.event.ActionListener logLevelEvent;
     
     /**
      * Creates new GUI
      * Instantiating code <em>must</em> also call initListeners()
-     * 
+     *
      * @see #initListeners()
      */
     public ServerManagement() {
-        if (LOGGER == null) // single instance of the Logger shared by all class objects
-            LOGGER = Logger.getLogger(_title);
-        LOGGER.setLevel(Level.ALL);
+        LOGGER = Logger.getLogger(_title);
+        // workaround to ensure all levels of log messages are recorded locally before the ServerLog service is available on the network
+        Logger l = LOGGER;
+        while (l != null) {
+            try {
+                l.setLevel(Level.ALL);
+            } catch (SecurityException ex ) {
+                System.err.println("LOGGER: cannot set level for logger " + l.getName());
+            }
+            l = l.getParent();
+        }
+
         _serviceToAddressMap = new ServiceAddressMap(_title, LOGGER);
+
+        // receives notifications from the log level menu buttons
+        logLevelEvent = new java.awt.event.ActionListener() {
+            @Override
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                gMenuLogLevelActionPerformed(evt);
+            }
+        };
         initComponents();
         _doLogging = true;
         _autoScroll = true;
@@ -144,51 +173,65 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
         this();
         this._multicastAnnouncements = multicastAnnouncements;
         gMenuLogServiceAnnounce.setSelected(_multicastAnnouncements);
-        // TODO: implement constructor setting IP address of SocialServer from command-line value
         if (serverSocial != null)
-            _serviceToAddressMap.put("ServerSocial", new LastSeenHost(new InetSocketAddress(serverSocial, Network.PORTS_SERVER_SOCIAL), LastSeenHost.STATE.STATIC));
+            _serviceToAddressMap.put("ServerSocial", new ServiceAddressMap.LastSeenHost(new InetSocketAddress(serverSocial, Network.PORTS_SERVER_SOCIAL), ServiceAddressMap.LastSeenHost.STATE.STATIC));
     }
+
     /**
-     * Initialise listeners and other objects that require a reference to 'this'.
+     * Custom simplified logging function.
+     * 
+     * Encapsulates the commonly repeated code of Logger method calls
      * 
+     * @param level the Logger.Level of this message
+     * @param format MessageFormat.format() formatter
+     * @param message zero or more arguments for MessageFormat.format() to process
+     */
+    protected void logp(Level level, String format, Object... message) {
+        if (LOGGER != null)
+            LOGGER.logp(level, _title, null, MessageFormat.format(format, (Object[]) message));
+    }
+
+    /**
+     * Initialise listeners and other objects that require a reference to 'this'.
+     *
      * Passing 'this' from within the constructor is unsafe since the object is not
      * fully constructed, so do it here. The compiler and virtual machine are free to move 'final'
      * properties outside the constructor which means they may not be correctly
      * initialised before the constructor returns. This is especially problematic
      * in multi-threading applications.
-     * 
+     *
      * @return a reference to 'this' so method calls can be chained (e.g. new ServerManagement().initListeners().setVisible(true) )
      * @throws UnknownHostException
      */
-    public ServerManagement initListeners() throws UnknownHostException {        
+    public ServerManagement initListeners() throws UnknownHostException {
         LOGGER.addHandler(new TableModelHandler(this)); // send messages to the GUI log table
         LOGGER.setUseParentHandlers(false); // don't send messages to the default error stream logger of the parent
-        LOGGER.logp(Level.INFO, _title, null, "Server Management starting");
-        
-        _udpLogServiceSA = new WSYD_SocketAddress(Network.PORTS_SERVER_LOG, Protocol.UDP);
+        logp(Level.INFO, "Server Management starting");
+
+        WSYD_SocketAddress _udpLogServiceSA = new WSYD_SocketAddress(Network.PORTS_SERVER_LOG, WSYD_SocketAddress.Protocol.UDP);
         _udpLogService = new NetworkServerUDP(_udpLogServiceSA, _title + "Log", _serviceToAddressMap, LOGGER);
-        _udpLogService.getEventManager().addNetworkMessageEventListener(this, "Log");
+        _udpLogService.getEventManager().addNetworkMessageEventListener(this);
         _udpLogService.setSimulate(false);
         _udpLogService.execute();
 
-        _multicastAdvertiserSA = new WSYD_SocketAddress(Network.MULTICAST_IP, Network.PORTS_MULTICAST_DISCOVERY, Protocol.UDP);
+        WSYD_SocketAddress _multicastAdvertiserSA = new WSYD_SocketAddress(Network.MULTICAST_IPv4, Network.PORTS_MULTICAST_DISCOVERY, WSYD_SocketAddress.Protocol.UDP);
         _multicastServer = new NetworkServerUDPMulticast(_multicastAdvertiserSA, _title + "MC", _serviceToAddressMap, LOGGER);
         _multicastServer.getEventManager().addNetworkMessageEventListener(this, "Neighbour");
         _multicastServer.execute();
         // permit broadcasting to pseudo-host 'all' since this is multicast
-        _serviceToAddressMap.put("all", new LastSeenHost(new InetSocketAddress(Network.MULTICAST_IP, Network.PORTS_MULTICAST_DISCOVERY), LastSeenHost.STATE.STATIC));
-        
+        _serviceToAddressMap.put("all", new ServiceAddressMap.LastSeenHost(new InetSocketAddress(Network.MULTICAST_IPv4, Network.PORTS_MULTICAST_DISCOVERY), ServiceAddressMap.LastSeenHost.STATE.STATIC));
+
         // log any static entries in the service map
-        for (java.util.Map.Entry<String, LastSeenHost> entry: this._serviceToAddressMap.getEntrySet()) {
-            LastSeenHost host = entry.getValue();
-            if (host.state == LastSeenHost.STATE.STATIC)
-                LOGGER.logp(Level.FINER, _title, null, MessageFormat.format("Static Service: {0} = {1}:{2,number,#####}", entry.getKey(), host.address.getHostString(), host.address.getPort()));
+        for (java.util.Map.Entry<String, ServiceAddressMap.LastSeenHost> entry: this._serviceToAddressMap.getEntrySet()) {
+            ServiceAddressMap.LastSeenHost host = entry.getValue();
+            if (host.state == ServiceAddressMap.LastSeenHost.STATE.STATIC)
+                logp(Level.FINER, "Static Service: {0} = {1}:{2,number,#####}", entry.getKey(), host.address.getHostString(), host.address.getPort());
         }
-        
+
         ActionListener regularTasksActionListener = new ActionListener() {
             // provide a way to read the current version of the parent process object's fields
             ServerManagement owner;
-            
+
             // XXX: Anonymous class initialisation - effectively used as the body of the object's constructor
             {
                 // XXX: access to the outer class's instance object
@@ -203,19 +246,13 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
              */
             @Override
             public void actionPerformed(ActionEvent e) {
-                String serviceName  = "LogService";
                 // check the current value ofthe parent process' flag
                 if (owner._multicastAnnouncements) {
-                    // Create local log report first
-                    LogRecord record = new LogRecord(Level.FINEST, "Multicast: Announcing Presence");
-                    record.setSourceClassName(serviceName);
-                    record.setMillis(System.currentTimeMillis());
-                    LOGGER.log(record);
-
+                    logp(Level.FINEST, "Multicast: Announcing Presence");
                     // Announce the log service
-                    MessagePresence mp = new MessagePresence(_title + serviceName, _udpLogService.getSocketAddress());
+                    MessagePresence mp = new MessagePresence(logServiceName, _udpLogService.getSocketAddress());
                     NetworkMessage nm = NetworkMessage.createNetworkMessage("Neighbour", "all", mp);
-                    nm.setSender(_title + serviceName);
+                    nm.setSender(logServiceName);
                     _multicastServer.queueMessage(nm);
                 }
                 // clean up the known hosts map
@@ -234,19 +271,19 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
 
             }
         };
-        regularTasks = new Timer(1000, regularTasksActionListener);
-        regularTasks.setInitialDelay(100);
-        regularTasks.start();
-        
+        _regularTasks = new Timer(1000, regularTasksActionListener);
+        _regularTasks.setInitialDelay(100);
+        _regularTasks.start();
+
         return this;
     }
 
     /**
      * Add a log record to the log table if logging is enabled.
-     * 
+     *
      * Abusing an already-existing logging interface. This method isn't filtering, it is called by
      * the Logger's TableModelHandler to publish records.
-     * 
+     *
      * @see TableModelHandler
      * @param record
      * @return true if logging is enabled
@@ -254,7 +291,7 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
     @Override
     public boolean isLoggable(LogRecord record) {
         if (_doLogging) {
-            if (record != null) {
+            if (record != null && LOGGER.isLoggable(record.getLevel())) {
                 SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                 // add the log record to the log table
                 ((DefaultTableModel)gLogTable.getModel()).addRow(
@@ -289,20 +326,19 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
                     packetSend.setPort(address.getPort());
 
                     DatagramSocket socket = new DatagramSocket();
-                    // acknowledge receipt
                     socket.send(packetSend);
-                    LOGGER.logp(Level.FINEST, _title, null, MessageFormat.format("Sending packet for {0} to {1} ({3}:{4,number,integer}) from {2}", message.getIntent(), message.getTarget(), message.getSender(), packetSend.getAddress().getHostAddress(), packetSend.getPort()));
+                    logp(Level.FINEST, "Sending packet for {0} to {1} ({3}:{4,number,integer}) from {2}", message.getIntent(), message.getTarget(), message.getSender(), packetSend.getAddress().getHostAddress(), packetSend.getPort());
 
                     result = true; // successful
                 } catch (IOException e) {
-                    // TODO: serverSend() add IOException handler
-                    e.printStackTrace();
+                    logp(Level.SEVERE, "UDPSend(): ", e.toString());
                 }
             }
         }
         return result;
     }
 
+    
     /**
      * This method is called from within the constructor to initialize the form.
      * WARNING: Do NOT modify this code. The content of this method is always
@@ -312,7 +348,7 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
     // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
     private void initComponents() {
 
-        buttonGroup1 = new javax.swing.ButtonGroup();
+        buttonGroupLogLevels = new javax.swing.ButtonGroup();
         gDialogAbout = new javax.swing.JDialog();
         gTextAreaAbout = new javax.swing.JTextArea();
         gBtnAbout = new javax.swing.JButton();
@@ -327,6 +363,16 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
         gMenuLogControl = new javax.swing.JCheckBoxMenuItem();
         gMenuLogAutoScroll = new javax.swing.JCheckBoxMenuItem();
         gMenuLogServiceAnnounce = new javax.swing.JCheckBoxMenuItem();
+        gMenuLogLevel = new javax.swing.JMenu();
+        gMenuLogLevelAll = new javax.swing.JRadioButtonMenuItem();
+        gMenuLogLevelFinest = new javax.swing.JRadioButtonMenuItem();
+        gMenuLogLevelFiner = new javax.swing.JRadioButtonMenuItem();
+        gMenuLogLevelFine = new javax.swing.JRadioButtonMenuItem();
+        gMenuLogLevelConfig = new javax.swing.JRadioButtonMenuItem();
+        gMenuLogLevelInfo = new javax.swing.JRadioButtonMenuItem();
+        gMenuLogLevelWarning = new javax.swing.JRadioButtonMenuItem();
+        gMenuLogLevelSevere = new javax.swing.JRadioButtonMenuItem();
+        gMenuLogLevelOff = new javax.swing.JRadioButtonMenuItem();
         gMenuServers = new javax.swing.JMenu();
         gMenuServerSocial = new javax.swing.JMenu();
         gMenuServerSocialRestart = new javax.swing.JMenuItem();
@@ -470,6 +516,56 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
         });
         gMenuLog.add(gMenuLogServiceAnnounce);
 
+        gMenuLogLevel.setText("Level");
+
+        buttonGroupLogLevels.add(gMenuLogLevelAll);
+        gMenuLogLevelAll.setSelected(true);
+        gMenuLogLevelAll.setText("ALL");
+        gMenuLogLevelAll.addActionListener(logLevelEvent);
+        gMenuLogLevel.add(gMenuLogLevelAll);
+
+        buttonGroupLogLevels.add(gMenuLogLevelFinest);
+        gMenuLogLevelFinest.setText("FINEST");
+        gMenuLogLevelFinest.addActionListener(logLevelEvent);
+        gMenuLogLevel.add(gMenuLogLevelFinest);
+
+        buttonGroupLogLevels.add(gMenuLogLevelFiner);
+        gMenuLogLevelFiner.setText("FINER");
+        gMenuLogLevelFiner.addActionListener(logLevelEvent);
+        gMenuLogLevel.add(gMenuLogLevelFiner);
+
+        buttonGroupLogLevels.add(gMenuLogLevelFine);
+        gMenuLogLevelFine.setText("FINE");
+        gMenuLogLevelFine.addActionListener(logLevelEvent);
+        gMenuLogLevel.add(gMenuLogLevelFine);
+
+        buttonGroupLogLevels.add(gMenuLogLevelConfig);
+        gMenuLogLevelConfig.setText("CONFIG");
+        gMenuLogLevelConfig.addActionListener(logLevelEvent);
+        gMenuLogLevel.add(gMenuLogLevelConfig);
+
+        buttonGroupLogLevels.add(gMenuLogLevelInfo);
+        gMenuLogLevelInfo.setText("INFO");
+        gMenuLogLevelInfo.addActionListener(logLevelEvent);
+        gMenuLogLevel.add(gMenuLogLevelInfo);
+
+        buttonGroupLogLevels.add(gMenuLogLevelWarning);
+        gMenuLogLevelWarning.setText("WARNING");
+        gMenuLogLevelWarning.addActionListener(logLevelEvent);
+        gMenuLogLevel.add(gMenuLogLevelWarning);
+
+        buttonGroupLogLevels.add(gMenuLogLevelSevere);
+        gMenuLogLevelSevere.setText("SEVERE");
+        gMenuLogLevelSevere.addActionListener(logLevelEvent);
+        gMenuLogLevel.add(gMenuLogLevelSevere);
+
+        buttonGroupLogLevels.add(gMenuLogLevelOff);
+        gMenuLogLevelOff.setText("OFF");
+        gMenuLogLevelOff.addActionListener(logLevelEvent);
+        gMenuLogLevel.add(gMenuLogLevelOff);
+
+        gMenuLog.add(gMenuLogLevel);
+
         gMenuBar.add(gMenuLog);
 
         gMenuServers.setText("Servers");
@@ -524,6 +620,16 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
         pack();
     }// </editor-fold>//GEN-END:initComponents
 
+    /**
+     * Sets the log level in response to GUI Log Level menu selections.
+     * 
+     * @param evt
+     */
+    private void gMenuLogLevelActionPerformed(java.awt.event.ActionEvent evt) {
+        LOGGER.logp(Level.SEVERE, _title, null, "Changing to log level " + evt.getActionCommand());
+        LOGGER.setLevel(Level.parse(evt.getActionCommand()));
+    }
+
     private void gMenuServerSocialRestartActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_gMenuServerSocialRestartActionPerformed
         // TODO add your handling code here:
         LOGGER.logp(Level.FINEST, _title, null, "Requesting ServerSocial Restart");
@@ -532,16 +638,16 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
         nm.setSender(_title);
         UDPSend(nm);
 
-                
+
     }//GEN-LAST:event_gMenuServerSocialRestartActionPerformed
 
     /**
      * When the (disguised) mug-icon button is pressed load a web page in the system default browser.
-     * 
+     *
      * Displays a news story about Microsoft Store's 'Scroogle' mug which coincidentally has a tag line
      * that is almost identical to the chosen name of this application.
-     * 
-     * @param evt 
+     *
+     * @param evt
      */
     private void AboutAction(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_AboutAction
         try {
@@ -552,7 +658,7 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
 
     /**
      * Show the Help>About dialog and auto-close it after 20 seconds.
-     * 
+     *
      * @param evt
      */
     private void gMenuHelpAboutActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_gMenuHelpAboutActionPerformed
@@ -571,8 +677,8 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
 
     /**
      * Save log entries to a file.
-     * 
-     * @param evt 
+     *
+     * @param evt
      */
     private void gMenuFileSaveActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_gMenuFileSaveActionPerformed
         if (gFileChooser.showSaveDialog(this) == javax.swing.JFileChooser.APPROVE_OPTION) {
@@ -602,7 +708,7 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
 
     /**
      * Clear the log table.
-     * @param evt 
+     * @param evt
      */
     private void gMenuLogClearActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_gMenuLogClearActionPerformed
         ((DefaultTableModel)gLogTable.getModel()).setRowCount(0);
@@ -610,7 +716,7 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
 
     /**
      * Enable or disable logging.
-     * @param evt 
+     * @param evt
      */
     private void gMenuLogControlActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_gMenuLogControlActionPerformed
         _doLogging = !_doLogging;
@@ -619,7 +725,7 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
 
     /**
      * Enable or disable automatic scrolling of the log table.
-     * @param evt 
+     * @param evt
      */
     private void gMenuLogAutoScrollActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_gMenuLogAutoScrollActionPerformed
         _autoScroll = !_autoScroll;
@@ -628,7 +734,7 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
 
     /**
      * Enable or disable multicast log service announcements.
-     * @param evt 
+     * @param evt
      */
     private void gMenuLogServiceAnnounceActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_gMenuLogServiceAnnounceActionPerformed
         this._multicastAnnouncements = gMenuLogServiceAnnounce.isSelected();
@@ -643,7 +749,8 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
         NetworkMessage nm = event.getNetworkMessage();
         if (nm == null || !_doLogging)
             return;
-        // is it a LogRecord?
+
+        String type = nm.getMessage().getMessageType();
         switch (nm.getIntent()) {
             case "Log":
                 MessageLogRecord m = (MessageLogRecord) nm.getMessage();
@@ -651,31 +758,67 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
                     return;
                 this.isLoggable(m.record);
                 break;
+
             case "Neighbour":
-                String type = nm.getMessage().getMessageType();
-                    if (type.equals(MessagePresence.getType())) { // Presence
+                    if (type.equals(MessagePresence.getType())) {
                         MessagePresence mp = (MessagePresence)nm.getMessage();
+                        // add or update the last-seen time of the Sender host in the known services map
+                        ServiceAddressMap.LastSeenHost host = new ServiceAddressMap.LastSeenHost(new InetSocketAddress(nm.getReceivedFrom().getAddress(), mp.socketAddress.getPort()));
+                        this._serviceToAddressMap.put(mp.serviceName, host);
+                        logp(Level.FINEST, "Added \"{0}\" ({1}) to service map", mp.serviceName, host.address.toString());
                         switch (mp.serviceName) {
-                            case "ServerSocial":
+                            case "ServerSocialMC":
                                 gMenuServerSocial.setEnabled(true);
                                 break;
-                            case "ServerChat":
+                            case "ServerChatMC":
                                 gMenuServerChat.setEnabled(true);
-                                break;                                
+                                break;
                         }
                     }
-
+                break;
 
             default: // log all unhandled messages
-                LogRecord record = new LogRecord(Level.WARNING,
-                    MessageFormat.format("Unhandled NetworkMessage received with intent: \"{0}\" sender: \"{1}\" target: \"{2}\"",
-                        nm.getIntent(), nm.getSender(), nm.getTarget() )
-                );
-                record.setMillis(System.currentTimeMillis());
-                LOGGER.log(record);
+                logp(Level.WARNING, "Unhandled NetworkMessage received with intent: \"{0}\" sender: \"{1}\" target: \"{2}\"", nm.getIntent(), nm.getSender(), nm.getTarget());
         }
     }
 
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.ButtonGroup buttonGroupLogLevels;
+    private javax.swing.JButton gBtnAbout;
+    private javax.swing.JDialog gDialogAbout;
+    private javax.swing.JFileChooser gFileChooser;
+    private javax.swing.JScrollPane gLogScroller;
+    private javax.swing.JTable gLogTable;
+    private javax.swing.JMenuBar gMenuBar;
+    private javax.swing.JMenu gMenuFile;
+    private javax.swing.JMenuItem gMenuFileSave;
+    private javax.swing.JMenu gMenuHelp;
+    private javax.swing.JMenuItem gMenuHelpAbout;
+    private javax.swing.JMenu gMenuLog;
+    private javax.swing.JCheckBoxMenuItem gMenuLogAutoScroll;
+    private javax.swing.JMenuItem gMenuLogClear;
+    private javax.swing.JCheckBoxMenuItem gMenuLogControl;
+    private javax.swing.JMenu gMenuLogLevel;
+    private javax.swing.JRadioButtonMenuItem gMenuLogLevelAll;
+    private javax.swing.JRadioButtonMenuItem gMenuLogLevelConfig;
+    private javax.swing.JRadioButtonMenuItem gMenuLogLevelFine;
+    private javax.swing.JRadioButtonMenuItem gMenuLogLevelFiner;
+    private javax.swing.JRadioButtonMenuItem gMenuLogLevelFinest;
+    private javax.swing.JRadioButtonMenuItem gMenuLogLevelInfo;
+    private javax.swing.JRadioButtonMenuItem gMenuLogLevelOff;
+    private javax.swing.JRadioButtonMenuItem gMenuLogLevelSevere;
+    private javax.swing.JRadioButtonMenuItem gMenuLogLevelWarning;
+    private javax.swing.JCheckBoxMenuItem gMenuLogServiceAnnounce;
+    private javax.swing.JMenu gMenuServerChat;
+    private javax.swing.JMenuItem gMenuServerChatRestart;
+    private javax.swing.JMenuItem gMenuServerChatStop;
+    private javax.swing.JMenu gMenuServerSocial;
+    private javax.swing.JMenuItem gMenuServerSocialRestart;
+    private javax.swing.JMenuItem gMenuServerSocialStop;
+    private javax.swing.JMenu gMenuServers;
+    private javax.swing.JTextArea gTextAreaAbout;
+    // End of variables declaration//GEN-END:variables
+
     /**
      * @param args the command line arguments
      */
@@ -683,7 +826,7 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
         /* Set the Nimbus look and feel */
         //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
         /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
-         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
+         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
          */
         try {
             for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
@@ -701,26 +844,33 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
         //</editor-fold>
         /* Create and display the form */
         class App implements Runnable {
-            public boolean multicastAnnouncements = false;
+            public boolean multicastAnnouncements = true;
             public InetAddress serverSocial = null;
+
+            /**
+             * Constructs, initialises and starts the server management GUI.
+             */
             @Override
             public void run() {
                 try {
-                new ServerManagement(multicastAnnouncements, serverSocial).initListeners().setVisible(true);
+                    new ServerManagement(multicastAnnouncements, serverSocial).initListeners().setVisible(true);
                 }
-                catch(UnknownHostException e) {
-                    System.err.println("Error: cannot create log server listener socket");
+                catch(UnknownHostException ex) {
+                    Logger.getLogger(ServerManagement.class.getName()).log(Level.SEVERE, null, MessageFormat.format("Error: initListeners(): {0}", ex.toString()));
                 }
             }
         }
         App app = new App();
-        
-        // process command line arguments        
+
+        // process command line arguments. Use indexed for in order to easily read values of switch arguments
         for (int i = 0; i < args.length; i++) {
             switch (args[i]) {
                 case "-announce":
                     app.multicastAnnouncements = true;
                     break;
+                case "-noannounce":
+                    app.multicastAnnouncements = false;
+                    break;
                 case "-server":
                     if (args.length >= i+1) {
                         // read the next argument as an IP address
@@ -729,7 +879,7 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
                             temp = InetAddress.getByName(args[i+1]);
                             app.serverSocial = temp;
                         } catch (UnknownHostException e) {
-                            System.err.println(MessageFormat.format("Error: {0} is not a valid hostname or IP address", args[i+1]));
+                            Logger.getLogger(ServerManagement.class.getName()).log(Level.SEVERE, null, MessageFormat.format("Error: {0} is not a valid hostname or IP address", args[i+1]));
                         }
                     }
                 break;
@@ -737,31 +887,4 @@ public class ServerManagement extends javax.swing.JFrame implements NetworkMessa
         }
         java.awt.EventQueue.invokeLater(app);
     }
-
-    // Variables declaration - do not modify//GEN-BEGIN:variables
-    private javax.swing.ButtonGroup buttonGroup1;
-    private javax.swing.JButton gBtnAbout;
-    private javax.swing.JDialog gDialogAbout;
-    private javax.swing.JFileChooser gFileChooser;
-    private javax.swing.JScrollPane gLogScroller;
-    private javax.swing.JTable gLogTable;
-    private javax.swing.JMenuBar gMenuBar;
-    private javax.swing.JMenu gMenuFile;
-    private javax.swing.JMenuItem gMenuFileSave;
-    private javax.swing.JMenu gMenuHelp;
-    private javax.swing.JMenuItem gMenuHelpAbout;
-    private javax.swing.JMenu gMenuLog;
-    private javax.swing.JCheckBoxMenuItem gMenuLogAutoScroll;
-    private javax.swing.JMenuItem gMenuLogClear;
-    private javax.swing.JCheckBoxMenuItem gMenuLogControl;
-    private javax.swing.JCheckBoxMenuItem gMenuLogServiceAnnounce;
-    private javax.swing.JMenu gMenuServerChat;
-    private javax.swing.JMenuItem gMenuServerChatRestart;
-    private javax.swing.JMenuItem gMenuServerChatStop;
-    private javax.swing.JMenu gMenuServerSocial;
-    private javax.swing.JMenuItem gMenuServerSocialRestart;
-    private javax.swing.JMenuItem gMenuServerSocialStop;
-    private javax.swing.JMenu gMenuServers;
-    private javax.swing.JTextArea gTextAreaAbout;
-    // End of variables declaration//GEN-END:variables
 }