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 }