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