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    }