View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements. See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache license, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License. You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the license for the specific language governing permissions and
    15   * limitations under the license.
    16   */
    17  package org.apache.logging.log4j.jmx.gui;
    18
    19  import java.awt.BorderLayout;
    20  import java.awt.Color;
    21  import java.awt.Component;
    22  import java.awt.Font;
    23  import java.awt.event.ActionEvent;
    24  import java.io.IOException;
    25  import java.io.PrintWriter;
    26  import java.io.StringWriter;
    27  import java.util.HashMap;
    28  import java.util.Map;
    29  import java.util.Properties;
    30  import javax.management.InstanceNotFoundException;
    31  import javax.management.JMException;
    32  import javax.management.ListenerNotFoundException;
    33  import javax.management.MBeanServerDelegate;
    34  import javax.management.MBeanServerNotification;
    35  import javax.management.MalformedObjectNameException;
    36  import javax.management.Notification;
    37  import javax.management.NotificationFilterSupport;
    38  import javax.management.NotificationListener;
    39  import javax.management.ObjectName;
    40  import javax.management.remote.JMXConnector;
    41  import javax.management.remote.JMXConnectorFactory;
    42  import javax.management.remote.JMXServiceURL;
    43  import javax.swing.AbstractAction;
    44  import javax.swing.JFrame;
    45  import javax.swing.JOptionPane;
    46  import javax.swing.JPanel;
    47  import javax.swing.JScrollPane;
    48  import javax.swing.JTabbedPane;
    49  import javax.swing.JTextArea;
    50  import javax.swing.JToggleButton;
    51  import javax.swing.ScrollPaneConstants;
    52  import javax.swing.SwingUtilities;
    53  import javax.swing.UIManager;
    54  import javax.swing.UIManager.LookAndFeelInfo;
    55  import javax.swing.WindowConstants;
    56
    57  import org.apache.logging.log4j.core.jmx.LoggerContextAdminMBean;
    58  import org.apache.logging.log4j.core.jmx.Server;
    59  import org.apache.logging.log4j.core.jmx.StatusLoggerAdminMBean;
    60  import org.apache.logging.log4j.core.util.Assert;
    61
    62  /**
    63   * Swing GUI that connects to a Java process via JMX and allows the user to view
    64   * and modify the Log4j 2 configuration, as well as monitor status logs.
    65   *
    66   * @see <a href=
    67   *      "http://docs.oracle.com/javase/6/docs/technotes/guides/management/jconsole.html"
    68   *      >http://docs.oracle.com/javase/6/docs/technotes/guides/management/
    69   *      jconsole.html</a >
    70   */
    71  public class ClientGui extends JPanel implements NotificationListener {
    72      private static final long serialVersionUID = -253621277232291174L;
    73      private static final int INITIAL_STRING_WRITER_SIZE = 1024;
    74      private final Client client;
    75      private final Map<ObjectName, Component> contextObjNameToTabbedPaneMap = new HashMap<ObjectName, Component>();
    76      private final Map<ObjectName, JTextArea> statusLogTextAreaMap = new HashMap<ObjectName, JTextArea>();
    77      private JTabbedPane tabbedPaneContexts;
    78
    79      public ClientGui(final Client client) throws IOException, JMException {
    80          this.client = Assert.requireNonNull(client, "client");
    81          createWidgets();
    82          populateWidgets();
    83
    84          // register for Notifications if LoggerContext MBean was added/removed
    85          final ObjectName addRemoveNotifs = MBeanServerDelegate.DELEGATE_NAME;
    86          final NotificationFilterSupport filter = new NotificationFilterSupport();
    87          filter.enableType(Server.DOMAIN); // only interested in Log4J2 MBeans
    88          client.getConnection().addNotificationListener(addRemoveNotifs, this, null, null);
    89      }
    90
    91      private void createWidgets() {
    92          tabbedPaneContexts = new JTabbedPane();
    93          this.setLayout(new BorderLayout());
    94          this.add(tabbedPaneContexts, BorderLayout.CENTER);
    95      }
    96
    97      private void populateWidgets() throws IOException, JMException {
    98          for (final LoggerContextAdminMBean ctx : client.getLoggerContextAdmins()) {
    99              addWidgetForLoggerContext(ctx);
    100         }
    101     }
    102
    103     private void addWidgetForLoggerContext(final LoggerContextAdminMBean ctx) throws MalformedObjectNameException,
    104             IOException, InstanceNotFoundException {
    105         final JTabbedPane contextTabs = new JTabbedPane();
    106         contextObjNameToTabbedPaneMap.put(ctx.getObjectName(), contextTabs);
    107         tabbedPaneContexts.addTab("LoggerContext: " + ctx.getName(), contextTabs);
    108
    109         final String contextName = ctx.getName();
    110         final StatusLoggerAdminMBean status = client.getStatusLoggerAdmin(contextName);
    111         if (status != null) {
    112             final JTextArea text = createTextArea();
    113             final String[] messages = status.getStatusDataHistory();
    114             for (final String message : messages) {
    115                 text.append(message + '\n');
    116             }
    117             statusLogTextAreaMap.put(ctx.getObjectName(), text);
    118             registerListeners(status);
    119             final JScrollPane scroll = scroll(text);
    120             contextTabs.addTab("StatusLogger", scroll);
    121         }
    122
    123         final ClientEditConfigPanel editor = new ClientEditConfigPanel(ctx);
    124         contextTabs.addTab("Configuration", editor);
    125     }
    126
    127     private void removeWidgetForLoggerContext(final ObjectName loggerContextObjName) throws JMException, IOException {
    128         final Component tab = contextObjNameToTabbedPaneMap.get(loggerContextObjName);
    129         if (tab != null) {
    130             tabbedPaneContexts.remove(tab);
    131         }
    132         statusLogTextAreaMap.remove(loggerContextObjName);
    133         final ObjectName objName = client.getStatusLoggerObjectName(loggerContextObjName);
    134         try {
    135             // System.out.println("Remove listener for " + objName);
    136             client.getConnection().removeNotificationListener(objName, this);
    137         } catch (final ListenerNotFoundException ignored) {
    138         }
    139     }
    140
    141     private JTextArea createTextArea() {
    142         final JTextArea result = new JTextArea();
    143         result.setEditable(false);
    144         result.setBackground(this.getBackground());
    145         result.setForeground(Color.black);
    146         result.setFont(new Font(Font.MONOSPACED, Font.PLAIN, result.getFont().getSize()));
    147         result.setWrapStyleWord(true);
    148         return result;
    149     }
    150
    151     private JScrollPane scroll(final JTextArea text) {
    152         final JToggleButton toggleButton = new JToggleButton();
    153         toggleButton.setAction(new AbstractAction() {
    154             private static final long serialVersionUID = -4214143754637722322L;
    155
    156             @Override
    157             public void actionPerformed(final ActionEvent e) {
    158                 final boolean wrap = toggleButton.isSelected();
    159                 text.setLineWrap(wrap);
    160             }
    161         });
    162         toggleButton.setToolTipText("Toggle line wrapping");
    163         final JScrollPane scrollStatusLog = new JScrollPane(text, //
    164                 ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, //
    165                 ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
    166         scrollStatusLog.setCorner(ScrollPaneConstants.LOWER_RIGHT_CORNER, toggleButton);
    167         return scrollStatusLog;
    168     }
    169
    170     private void registerListeners(final StatusLoggerAdminMBean status) throws InstanceNotFoundException,
    171             MalformedObjectNameException, IOException {
    172         final NotificationFilterSupport filter = new NotificationFilterSupport();
    173         filter.enableType(StatusLoggerAdminMBean.NOTIF_TYPE_MESSAGE);
    174         final ObjectName objName = status.getObjectName();
    175         // System.out.println("Add listener for " + objName);
    176         client.getConnection().addNotificationListener(objName, this, filter, status.getContextName());
    177     }
    178
    179     @Override
    180     public void handleNotification(final Notification notif, final Object paramObject) {
    181         SwingUtilities.invokeLater(new Runnable() {
    182             @Override
    183             public void run() { // LOG4J2-538
    184                 handleNotificationInAwtEventThread(notif, paramObject);
    185             }
    186         });
    187     }
    188
    189     private void handleNotificationInAwtEventThread(final Notification notif, final Object paramObject) {
    190         if (StatusLoggerAdminMBean.NOTIF_TYPE_MESSAGE.equals(notif.getType())) {
    191             if (!(paramObject instanceof ObjectName)) {
    192                 handle("Invalid notification object type", new ClassCastException(paramObject.getClass().getName()));
    193                 return;
    194             }
    195             final ObjectName param = (ObjectName) paramObject;
    196             final JTextArea text = statusLogTextAreaMap.get(param);
    197             if (text != null) {
    198                 text.append(notif.getMessage() + '\n');
    199             }
    200             return;
    201         }
    202         if (notif instanceof MBeanServerNotification) {
    203             final MBeanServerNotification mbsn = (MBeanServerNotification) notif;
    204             final ObjectName mbeanName = mbsn.getMBeanName();
    205             if (MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(notif.getType())) {
    206                 onMBeanRegistered(mbeanName);
    207             } else if (MBeanServerNotification.UNREGISTRATION_NOTIFICATION.equals(notif.getType())) {
    208                 onMBeanUnregistered(mbeanName);
    209             }
    210         }
    211     }
    212
    213     /**
    214      * Called every time a Log4J2 MBean was registered in the MBean server.
    215      *
    216      * @param mbeanName ObjectName of the registered Log4J2 MBean
    217      */
    218     private void onMBeanRegistered(final ObjectName mbeanName) {
    219         if (client.isLoggerContext(mbeanName)) {
    220             try {
    221                 final LoggerContextAdminMBean ctx = client.getLoggerContextAdmin(mbeanName);
    222                 addWidgetForLoggerContext(ctx);
    223             } catch (final Exception ex) {
    224                 handle("Could not add tab for new MBean " + mbeanName, ex);
    225             }
    226         }
    227     }
    228
    229     /**
    230      * Called every time a Log4J2 MBean was unregistered from the MBean server.
    231      *
    232      * @param mbeanName ObjectName of the unregistered Log4J2 MBean
    233      */
    234     private void onMBeanUnregistered(final ObjectName mbeanName) {
    235         if (client.isLoggerContext(mbeanName)) {
    236             try {
    237                 removeWidgetForLoggerContext(mbeanName);
    238             } catch (final Exception ex) {
    239                 handle("Could not remove tab for " + mbeanName, ex);
    240             }
    241         }
    242     }
    243
    244     private void handle(final String msg, final Exception ex) {
    245         System.err.println(msg);
    246         ex.printStackTrace();
    247
    248         final StringWriter sw = new StringWriter(INITIAL_STRING_WRITER_SIZE);
    249         ex.printStackTrace(new PrintWriter(sw));
    250         JOptionPane.showMessageDialog(this, sw.toString(), msg, JOptionPane.ERROR_MESSAGE);
    251     }
    252
    253     /**
    254      * Connects to the specified location and shows this panel in a window.
    255      * <p>
    256      * Useful links:
    257      * http://www.componative.com/content/controller/developer/insights
    258      * /jconsole3/
    259      *
    260      * @param args must have at least one parameter, which specifies the
    261      *            location to connect to. Must be of the form {@code host:port}
    262      *            or {@code service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi}
    263      *            or
    264      *            {@code service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi}
    265      * @throws Exception if anything goes wrong
    266      */
    267     public static void main(final String[] args) throws Exception {
    268         if (args.length < 1) {
    269             usage();
    270             return;
    271         }
    272         String serviceUrl = args[0];
    273         if (!serviceUrl.startsWith("service:jmx")) {
    274             serviceUrl = "service:jmx:rmi:///jndi/rmi://" + args[0] + "/jmxrmi";
    275         }
    276         final JMXServiceURL url = new JMXServiceURL(serviceUrl);
    277         final Properties props = System.getProperties();
    278         final Map<String, String> paramMap = new HashMap<String, String>(props.size());
    279         for (final String key : props.stringPropertyNames()) {
    280             paramMap.put(key, props.getProperty(key));
    281         }
    282         final JMXConnector connector = JMXConnectorFactory.connect(url, paramMap);
    283         final Client client = new Client(connector);
    284         final String title = "Log4j JMX Client - " + url;
    285
    286         SwingUtilities.invokeLater(new Runnable() {
    287             @Override
    288             public void run() {
    289                 installLookAndFeel();
    290                 try {
    291                     final ClientGui gui = new ClientGui(client);
    292                     final JFrame frame = new JFrame(title);
    293                     frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    294                     frame.getContentPane().add(gui, BorderLayout.CENTER);
    295                     frame.pack();
    296                     frame.setVisible(true);
    297                 } catch (final Exception ex) {
    298                     // if console is visible, print error so that
    299                     // the stack trace remains visible after error dialog is
    300                     // closed
    301                     ex.printStackTrace();
    302
    303                     // show error in dialog: there may not be a console window
    304                     // visible
    305                     final StringWriter sr = new StringWriter();
    306                     ex.printStackTrace(new PrintWriter(sr));
    307                     JOptionPane.showMessageDialog(null, sr.toString(), "Error", JOptionPane.ERROR_MESSAGE);
    308                 }
    309             }
    310         });
    311     }
    312
    313     private static void usage() {
    314         final String me = ClientGui.class.getName();
    315         System.err.println("Usage: java " + me + " <host>:<port>");
    316         System.err.println("   or: java " + me + " service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi");
    317         final String longAdr = " service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi";
    318         System.err.println("   or: java " + me + longAdr);
    319     }
    320
    321     private static void installLookAndFeel() {
    322         try {
    323             for (final LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
    324                 if ("Nimbus".equals(info.getName())) {
    325                     UIManager.setLookAndFeel(info.getClassName());
    326                     return;
    327                 }
    328             }
    329         } catch (final Exception ex) {
    330             ex.printStackTrace();
    331         }
    332         try {
    333             UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
    334         } catch (final Exception e) {
    335             e.printStackTrace();
    336         }
    337     }
    338 }