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.Objects;
30  import java.util.Properties;
31  
32  import javax.management.InstanceNotFoundException;
33  import javax.management.JMException;
34  import javax.management.ListenerNotFoundException;
35  import javax.management.MBeanServerDelegate;
36  import javax.management.MBeanServerNotification;
37  import javax.management.MalformedObjectNameException;
38  import javax.management.Notification;
39  import javax.management.NotificationFilterSupport;
40  import javax.management.NotificationListener;
41  import javax.management.ObjectName;
42  import javax.management.remote.JMXConnector;
43  import javax.management.remote.JMXConnectorFactory;
44  import javax.management.remote.JMXServiceURL;
45  import javax.swing.AbstractAction;
46  import javax.swing.JFrame;
47  import javax.swing.JOptionPane;
48  import javax.swing.JPanel;
49  import javax.swing.JScrollPane;
50  import javax.swing.JTabbedPane;
51  import javax.swing.JTextArea;
52  import javax.swing.JToggleButton;
53  import javax.swing.ScrollPaneConstants;
54  import javax.swing.SwingUtilities;
55  import javax.swing.UIManager;
56  import javax.swing.UIManager.LookAndFeelInfo;
57  import javax.swing.WindowConstants;
58  
59  import org.apache.logging.log4j.core.jmx.LoggerContextAdminMBean;
60  import org.apache.logging.log4j.core.jmx.Server;
61  import org.apache.logging.log4j.core.jmx.StatusLoggerAdminMBean;
62  
63  /**
64   * Swing GUI that connects to a Java process via JMX and allows the user to view
65   * and modify the Log4j 2 configuration, as well as monitor status logs.
66   *
67   * @see <a href=
68   *      "http://docs.oracle.com/javase/6/docs/technotes/guides/management/jconsole.html"
69   *      >http://docs.oracle.com/javase/6/docs/technotes/guides/management/
70   *      jconsole.html</a >
71   */
72  public class ClientGui extends JPanel implements NotificationListener {
73      private static final long serialVersionUID = -253621277232291174L;
74      private static final int INITIAL_STRING_WRITER_SIZE = 1024;
75      private final Client client;
76      private final Map<ObjectName, Component> contextObjNameToTabbedPaneMap = new HashMap<>();
77      private final Map<ObjectName, JTextArea> statusLogTextAreaMap = new HashMap<>();
78      private JTabbedPane tabbedPaneContexts;
79  
80      public ClientGui(final Client client) throws IOException, JMException {
81          this.client = Objects.requireNonNull(client, "client");
82          createWidgets();
83          populateWidgets();
84  
85          // register for Notifications if LoggerContext MBean was added/removed
86          final ObjectName addRemoveNotifs = MBeanServerDelegate.DELEGATE_NAME;
87          final NotificationFilterSupport filter = new NotificationFilterSupport();
88          filter.enableType(Server.DOMAIN); // only interested in Log4J2 MBeans
89          client.getConnection().addNotificationListener(addRemoveNotifs, this, null, null);
90      }
91  
92      private void createWidgets() {
93          tabbedPaneContexts = new JTabbedPane();
94          this.setLayout(new BorderLayout());
95          this.add(tabbedPaneContexts, BorderLayout.CENTER);
96      }
97  
98      private void populateWidgets() throws IOException, JMException {
99          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 }