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(() -> handleNotificationInAwtEventThread(notif, paramObject));
183     }
184 
185     private void handleNotificationInAwtEventThread(final Notification notif, final Object paramObject) {
186         if (StatusLoggerAdminMBean.NOTIF_TYPE_MESSAGE.equals(notif.getType())) {
187             if (!(paramObject instanceof ObjectName)) {
188                 handle("Invalid notification object type", new ClassCastException(paramObject.getClass().getName()));
189                 return;
190             }
191             final ObjectName param = (ObjectName) paramObject;
192             final JTextArea text = statusLogTextAreaMap.get(param);
193             if (text != null) {
194                 text.append(notif.getMessage() + '\n');
195             }
196             return;
197         }
198         if (notif instanceof MBeanServerNotification) {
199             final MBeanServerNotification mbsn = (MBeanServerNotification) notif;
200             final ObjectName mbeanName = mbsn.getMBeanName();
201             if (MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(notif.getType())) {
202                 onMBeanRegistered(mbeanName);
203             } else if (MBeanServerNotification.UNREGISTRATION_NOTIFICATION.equals(notif.getType())) {
204                 onMBeanUnregistered(mbeanName);
205             }
206         }
207     }
208 
209     /**
210      * Called every time a Log4J2 MBean was registered in the MBean server.
211      *
212      * @param mbeanName ObjectName of the registered Log4J2 MBean
213      */
214     private void onMBeanRegistered(final ObjectName mbeanName) {
215         if (client.isLoggerContext(mbeanName)) {
216             try {
217                 final LoggerContextAdminMBean ctx = client.getLoggerContextAdmin(mbeanName);
218                 addWidgetForLoggerContext(ctx);
219             } catch (final Exception ex) {
220                 handle("Could not add tab for new MBean " + mbeanName, ex);
221             }
222         }
223     }
224 
225     /**
226      * Called every time a Log4J2 MBean was unregistered from the MBean server.
227      *
228      * @param mbeanName ObjectName of the unregistered Log4J2 MBean
229      */
230     private void onMBeanUnregistered(final ObjectName mbeanName) {
231         if (client.isLoggerContext(mbeanName)) {
232             try {
233                 removeWidgetForLoggerContext(mbeanName);
234             } catch (final Exception ex) {
235                 handle("Could not remove tab for " + mbeanName, ex);
236             }
237         }
238     }
239 
240     private void handle(final String msg, final Exception ex) {
241         System.err.println(msg);
242         ex.printStackTrace();
243 
244         final StringWriter sw = new StringWriter(INITIAL_STRING_WRITER_SIZE);
245         ex.printStackTrace(new PrintWriter(sw));
246         JOptionPane.showMessageDialog(this, sw.toString(), msg, JOptionPane.ERROR_MESSAGE);
247     }
248 
249     /**
250      * Connects to the specified location and shows this panel in a window.
251      * <p>
252      * Useful links:
253      * http://www.componative.com/content/controller/developer/insights
254      * /jconsole3/
255      *
256      * @param args must have at least one parameter, which specifies the
257      *            location to connect to. Must be of the form {@code host:port}
258      *            or {@code service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi}
259      *            or
260      *            {@code service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi}
261      * @throws Exception if anything goes wrong
262      */
263     public static void main(final String[] args) throws Exception {
264         if (args.length < 1) {
265             usage();
266             return;
267         }
268         String serviceUrl = args[0];
269         if (!serviceUrl.startsWith("service:jmx")) {
270             serviceUrl = "service:jmx:rmi:///jndi/rmi://" + args[0] + "/jmxrmi";
271         }
272         final JMXServiceURL url = new JMXServiceURL(serviceUrl);
273         final Properties props = System.getProperties();
274         final Map<String, String> paramMap = new HashMap<>(props.size());
275         for (final String key : props.stringPropertyNames()) {
276             paramMap.put(key, props.getProperty(key));
277         }
278         final JMXConnector connector = JMXConnectorFactory.connect(url, paramMap);
279         final Client client = new Client(connector);
280         final String title = "Log4j JMX Client - " + url;
281 
282         SwingUtilities.invokeLater(() -> {
283             installLookAndFeel();
284             try {
285                 final ClientGui gui = new ClientGui(client);
286                 final JFrame frame = new JFrame(title);
287                 frame.setDefaultCloseOperation(WindowConstants.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     private static void usage() {
307         final String me = ClientGui.class.getName();
308         System.err.println("Usage: java " + me + " <host>:<port>");
309         System.err.println("   or: java " + me + " service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi");
310         final String longAdr = " service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi";
311         System.err.println("   or: java " + me + longAdr);
312     }
313 
314     private static void installLookAndFeel() {
315         try {
316             for (final LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
317                 if ("Nimbus".equals(info.getName())) {
318                     UIManager.setLookAndFeel(info.getClassName());
319                     return;
320                 }
321             }
322         } catch (final Exception ex) {
323             ex.printStackTrace();
324         }
325         try {
326             UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
327         } catch (final Exception e) {
328             e.printStackTrace();
329         }
330     }
331 }