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 */
017package org.apache.logging.log4j.jmx.gui;
018
019import java.awt.BorderLayout;
020import java.awt.Color;
021import java.awt.Component;
022import java.awt.Font;
023import java.awt.event.ActionEvent;
024import java.io.IOException;
025import java.io.PrintWriter;
026import java.io.StringWriter;
027import java.util.HashMap;
028import java.util.Map;
029import java.util.Objects;
030import java.util.Properties;
031
032import javax.management.InstanceNotFoundException;
033import javax.management.JMException;
034import javax.management.ListenerNotFoundException;
035import javax.management.MBeanServerDelegate;
036import javax.management.MBeanServerNotification;
037import javax.management.MalformedObjectNameException;
038import javax.management.Notification;
039import javax.management.NotificationFilterSupport;
040import javax.management.NotificationListener;
041import javax.management.ObjectName;
042import javax.management.remote.JMXConnector;
043import javax.management.remote.JMXConnectorFactory;
044import javax.management.remote.JMXServiceURL;
045import javax.swing.AbstractAction;
046import javax.swing.JFrame;
047import javax.swing.JOptionPane;
048import javax.swing.JPanel;
049import javax.swing.JScrollPane;
050import javax.swing.JTabbedPane;
051import javax.swing.JTextArea;
052import javax.swing.JToggleButton;
053import javax.swing.ScrollPaneConstants;
054import javax.swing.SwingUtilities;
055import javax.swing.UIManager;
056import javax.swing.UIManager.LookAndFeelInfo;
057import javax.swing.WindowConstants;
058
059import org.apache.logging.log4j.core.jmx.LoggerContextAdminMBean;
060import org.apache.logging.log4j.core.jmx.Server;
061import org.apache.logging.log4j.core.jmx.StatusLoggerAdminMBean;
062
063/**
064 * Swing GUI that connects to a Java process via JMX and allows the user to view
065 * and modify the Log4j 2 configuration, as well as monitor status logs.
066 *
067 * @see <a href=
068 *      "http://docs.oracle.com/javase/6/docs/technotes/guides/management/jconsole.html"
069 *      >http://docs.oracle.com/javase/6/docs/technotes/guides/management/
070 *      jconsole.html</a >
071 */
072public class ClientGui extends JPanel implements NotificationListener {
073    private static final long serialVersionUID = -253621277232291174L;
074    private static final int INITIAL_STRING_WRITER_SIZE = 1024;
075    private final Client client;
076    private final Map<ObjectName, Component> contextObjNameToTabbedPaneMap = new HashMap<>();
077    private final Map<ObjectName, JTextArea> statusLogTextAreaMap = new HashMap<>();
078    private JTabbedPane tabbedPaneContexts;
079
080    public ClientGui(final Client client) throws IOException, JMException {
081        this.client = Objects.requireNonNull(client, "client");
082        createWidgets();
083        populateWidgets();
084
085        // register for Notifications if LoggerContext MBean was added/removed
086        final ObjectName addRemoveNotifs = MBeanServerDelegate.DELEGATE_NAME;
087        final NotificationFilterSupport filter = new NotificationFilterSupport();
088        filter.enableType(Server.DOMAIN); // only interested in Log4J2 MBeans
089        client.getConnection().addNotificationListener(addRemoveNotifs, this, null, null);
090    }
091
092    private void createWidgets() {
093        tabbedPaneContexts = new JTabbedPane();
094        this.setLayout(new BorderLayout());
095        this.add(tabbedPaneContexts, BorderLayout.CENTER);
096    }
097
098    private void populateWidgets() throws IOException, JMException {
099        for (final LoggerContextAdminMBean ctx : client.getLoggerContextAdmins()) {
100            addWidgetForLoggerContext(ctx);
101        }
102    }
103
104    private void addWidgetForLoggerContext(final LoggerContextAdminMBean ctx) throws MalformedObjectNameException,
105            IOException, InstanceNotFoundException {
106        final JTabbedPane contextTabs = new JTabbedPane();
107        contextObjNameToTabbedPaneMap.put(ctx.getObjectName(), contextTabs);
108        tabbedPaneContexts.addTab("LoggerContext: " + ctx.getName(), contextTabs);
109
110        final String contextName = ctx.getName();
111        final StatusLoggerAdminMBean status = client.getStatusLoggerAdmin(contextName);
112        if (status != null) {
113            final JTextArea text = createTextArea();
114            final String[] messages = status.getStatusDataHistory();
115            for (final String message : messages) {
116                text.append(message + '\n');
117            }
118            statusLogTextAreaMap.put(ctx.getObjectName(), text);
119            registerListeners(status);
120            final JScrollPane scroll = scroll(text);
121            contextTabs.addTab("StatusLogger", scroll);
122        }
123
124        final ClientEditConfigPanel editor = new ClientEditConfigPanel(ctx);
125        contextTabs.addTab("Configuration", editor);
126    }
127
128    private void removeWidgetForLoggerContext(final ObjectName loggerContextObjName) throws JMException, IOException {
129        final Component tab = contextObjNameToTabbedPaneMap.get(loggerContextObjName);
130        if (tab != null) {
131            tabbedPaneContexts.remove(tab);
132        }
133        statusLogTextAreaMap.remove(loggerContextObjName);
134        final ObjectName objName = client.getStatusLoggerObjectName(loggerContextObjName);
135        try {
136            // System.out.println("Remove listener for " + objName);
137            client.getConnection().removeNotificationListener(objName, this);
138        } catch (final ListenerNotFoundException ignored) {
139        }
140    }
141
142    private JTextArea createTextArea() {
143        final JTextArea result = new JTextArea();
144        result.setEditable(false);
145        result.setBackground(this.getBackground());
146        result.setForeground(Color.black);
147        result.setFont(new Font(Font.MONOSPACED, Font.PLAIN, result.getFont().getSize()));
148        result.setWrapStyleWord(true);
149        return result;
150    }
151
152    private JScrollPane scroll(final JTextArea text) {
153        final JToggleButton toggleButton = new JToggleButton();
154        toggleButton.setAction(new AbstractAction() {
155            private static final long serialVersionUID = -4214143754637722322L;
156
157            @Override
158            public void actionPerformed(final ActionEvent e) {
159                final boolean wrap = toggleButton.isSelected();
160                text.setLineWrap(wrap);
161            }
162        });
163        toggleButton.setToolTipText("Toggle line wrapping");
164        final JScrollPane scrollStatusLog = new JScrollPane(text, //
165                ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, //
166                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
167        scrollStatusLog.setCorner(ScrollPaneConstants.LOWER_RIGHT_CORNER, toggleButton);
168        return scrollStatusLog;
169    }
170
171    private void registerListeners(final StatusLoggerAdminMBean status) throws InstanceNotFoundException,
172            MalformedObjectNameException, IOException {
173        final NotificationFilterSupport filter = new NotificationFilterSupport();
174        filter.enableType(StatusLoggerAdminMBean.NOTIF_TYPE_MESSAGE);
175        final ObjectName objName = status.getObjectName();
176        // System.out.println("Add listener for " + objName);
177        client.getConnection().addNotificationListener(objName, this, filter, status.getContextName());
178    }
179
180    @Override
181    public void handleNotification(final Notification notif, final Object paramObject) {
182        SwingUtilities.invokeLater(new Runnable() {
183            @Override
184            public void run() { // LOG4J2-538
185                handleNotificationInAwtEventThread(notif, paramObject);
186            }
187        });
188    }
189
190    private void handleNotificationInAwtEventThread(final Notification notif, final Object paramObject) {
191        if (StatusLoggerAdminMBean.NOTIF_TYPE_MESSAGE.equals(notif.getType())) {
192            if (!(paramObject instanceof ObjectName)) {
193                handle("Invalid notification object type", new ClassCastException(paramObject.getClass().getName()));
194                return;
195            }
196            final ObjectName param = (ObjectName) paramObject;
197            final JTextArea text = statusLogTextAreaMap.get(param);
198            if (text != null) {
199                text.append(notif.getMessage() + '\n');
200            }
201            return;
202        }
203        if (notif instanceof MBeanServerNotification) {
204            final MBeanServerNotification mbsn = (MBeanServerNotification) notif;
205            final ObjectName mbeanName = mbsn.getMBeanName();
206            if (MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(notif.getType())) {
207                onMBeanRegistered(mbeanName);
208            } else if (MBeanServerNotification.UNREGISTRATION_NOTIFICATION.equals(notif.getType())) {
209                onMBeanUnregistered(mbeanName);
210            }
211        }
212    }
213
214    /**
215     * Called every time a Log4J2 MBean was registered in the MBean server.
216     *
217     * @param mbeanName ObjectName of the registered Log4J2 MBean
218     */
219    private void onMBeanRegistered(final ObjectName mbeanName) {
220        if (client.isLoggerContext(mbeanName)) {
221            try {
222                final LoggerContextAdminMBean ctx = client.getLoggerContextAdmin(mbeanName);
223                addWidgetForLoggerContext(ctx);
224            } catch (final Exception ex) {
225                handle("Could not add tab for new MBean " + mbeanName, ex);
226            }
227        }
228    }
229
230    /**
231     * Called every time a Log4J2 MBean was unregistered from the MBean server.
232     *
233     * @param mbeanName ObjectName of the unregistered Log4J2 MBean
234     */
235    private void onMBeanUnregistered(final ObjectName mbeanName) {
236        if (client.isLoggerContext(mbeanName)) {
237            try {
238                removeWidgetForLoggerContext(mbeanName);
239            } catch (final Exception ex) {
240                handle("Could not remove tab for " + mbeanName, ex);
241            }
242        }
243    }
244
245    private void handle(final String msg, final Exception ex) {
246        System.err.println(msg);
247        ex.printStackTrace();
248
249        final StringWriter sw = new StringWriter(INITIAL_STRING_WRITER_SIZE);
250        ex.printStackTrace(new PrintWriter(sw));
251        JOptionPane.showMessageDialog(this, sw.toString(), msg, JOptionPane.ERROR_MESSAGE);
252    }
253
254    /**
255     * Connects to the specified location and shows this panel in a window.
256     * <p>
257     * Useful links:
258     * http://www.componative.com/content/controller/developer/insights
259     * /jconsole3/
260     *
261     * @param args must have at least one parameter, which specifies the
262     *            location to connect to. Must be of the form {@code host:port}
263     *            or {@code service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi}
264     *            or
265     *            {@code service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi}
266     * @throws Exception if anything goes wrong
267     */
268    public static void main(final String[] args) throws Exception {
269        if (args.length < 1) {
270            usage();
271            return;
272        }
273        String serviceUrl = args[0];
274        if (!serviceUrl.startsWith("service:jmx")) {
275            serviceUrl = "service:jmx:rmi:///jndi/rmi://" + args[0] + "/jmxrmi";
276        }
277        final JMXServiceURL url = new JMXServiceURL(serviceUrl);
278        final Properties props = System.getProperties();
279        final Map<String, String> paramMap = new HashMap<>(props.size());
280        for (final String key : props.stringPropertyNames()) {
281            paramMap.put(key, props.getProperty(key));
282        }
283        final JMXConnector connector = JMXConnectorFactory.connect(url, paramMap);
284        final Client client = new Client(connector);
285        final String title = "Log4j JMX Client - " + url;
286
287        SwingUtilities.invokeLater(new Runnable() {
288            @Override
289            public void run() {
290                installLookAndFeel();
291                try {
292                    final ClientGui gui = new ClientGui(client);
293                    final JFrame frame = new JFrame(title);
294                    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
295                    frame.getContentPane().add(gui, BorderLayout.CENTER);
296                    frame.pack();
297                    frame.setVisible(true);
298                } catch (final Exception ex) {
299                    // if console is visible, print error so that
300                    // the stack trace remains visible after error dialog is
301                    // closed
302                    ex.printStackTrace();
303
304                    // show error in dialog: there may not be a console window
305                    // visible
306                    final StringWriter sr = new StringWriter();
307                    ex.printStackTrace(new PrintWriter(sr));
308                    JOptionPane.showMessageDialog(null, sr.toString(), "Error", JOptionPane.ERROR_MESSAGE);
309                }
310            }
311        });
312    }
313
314    private static void usage() {
315        final String me = ClientGui.class.getName();
316        System.err.println("Usage: java " + me + " <host>:<port>");
317        System.err.println("   or: java " + me + " service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi");
318        final String longAdr = " service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi";
319        System.err.println("   or: java " + me + longAdr);
320    }
321
322    private static void installLookAndFeel() {
323        try {
324            for (final LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
325                if ("Nimbus".equals(info.getName())) {
326                    UIManager.setLookAndFeel(info.getClassName());
327                    return;
328                }
329            }
330        } catch (final Exception ex) {
331            ex.printStackTrace();
332        }
333        try {
334            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
335        } catch (final Exception e) {
336            e.printStackTrace();
337        }
338    }
339}