    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements. See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache license, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License. You may obtain a copy of the License at
    008     *
    009     *      http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the license for the specific language governing permissions and
    015     * limitations under the license.
    016     */
    017    package org.apache.logging.log4j.jmx.gui;
    018
    019    import java.awt.BorderLayout;
    020    import java.awt.Color;
    021    import java.awt.Component;
    022    import java.awt.Font;
    023    import java.awt.event.ActionEvent;
    024    import java.io.IOException;
    025    import java.io.PrintWriter;
    026    import java.io.StringWriter;
    027    import java.util.HashMap;
    028    import java.util.Map;
    029    import java.util.Properties;
    030    import javax.management.InstanceNotFoundException;
    031    import javax.management.JMException;
    032    import javax.management.ListenerNotFoundException;
    033    import javax.management.MBeanServerDelegate;
    034    import javax.management.MBeanServerNotification;
    035    import javax.management.MalformedObjectNameException;
    036    import javax.management.Notification;
    037    import javax.management.NotificationFilterSupport;
    038    import javax.management.NotificationListener;
    039    import javax.management.ObjectName;
    040    import javax.management.remote.JMXConnector;
    041    import javax.management.remote.JMXConnectorFactory;
    042    import javax.management.remote.JMXServiceURL;
    043    import javax.swing.AbstractAction;
    044    import javax.swing.JFrame;
    045    import javax.swing.JOptionPane;
    046    import javax.swing.JPanel;
    047    import javax.swing.JScrollPane;
    048    import javax.swing.JTabbedPane;
    049    import javax.swing.JTextArea;
    050    import javax.swing.JToggleButton;
    051    import javax.swing.ScrollPaneConstants;
    052    import javax.swing.SwingUtilities;
    053    import javax.swing.UIManager;
    054    import javax.swing.UIManager.LookAndFeelInfo;
    055    import javax.swing.WindowConstants;
    056
    057    import org.apache.logging.log4j.core.jmx.LoggerContextAdminMBean;
    058    import org.apache.logging.log4j.core.jmx.Server;
    059    import org.apache.logging.log4j.core.jmx.StatusLoggerAdminMBean;
    060    import org.apache.logging.log4j.core.util.Assert;
    061
    062    /**
    063     * Swing GUI that connects to a Java process via JMX and allows the user to view
    064     * and modify the Log4j 2 configuration, as well as monitor status logs.
    065     *
    066     * @see <a href=
    067     *      "http://docs.oracle.com/javase/6/docs/technotes/guides/management/jconsole.html"
    068     *      >http://docs.oracle.com/javase/6/docs/technotes/guides/management/
    069     *      jconsole.html</a >
    070     */
    071    public class ClientGui extends JPanel implements NotificationListener {
    072        private static final long serialVersionUID = -253621277232291174L;
    073        private static final int INITIAL_STRING_WRITER_SIZE = 1024;
    074        private final Client client;
    075        private final Map<ObjectName, Component> contextObjNameToTabbedPaneMap = new HashMap<ObjectName, Component>();
    076        private final Map<ObjectName, JTextArea> statusLogTextAreaMap = new HashMap<ObjectName, JTextArea>();
    077        private JTabbedPane tabbedPaneContexts;
    078
    079        public ClientGui(final Client client) throws IOException, JMException {
    080            this.client = Assert.requireNonNull(client, "client");
    081            createWidgets();
    082            populateWidgets();
    083
    084            // register for Notifications if LoggerContext MBean was added/removed
    085            final ObjectName addRemoveNotifs = MBeanServerDelegate.DELEGATE_NAME;
    086            final NotificationFilterSupport filter = new NotificationFilterSupport();
    087            filter.enableType(Server.DOMAIN); // only interested in Log4J2 MBeans
    088            client.getConnection().addNotificationListener(addRemoveNotifs, this, null, null);
    089        }
    090
    091        private void createWidgets() {
    092            tabbedPaneContexts = new JTabbedPane();
    093            this.setLayout(new BorderLayout());
    094            this.add(tabbedPaneContexts, BorderLayout.CENTER);
    095        }
    096
    097        private void populateWidgets() throws IOException, JMException {
    098            for (final LoggerContextAdminMBean ctx : client.getLoggerContextAdmins()) {
    099                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    }