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.helpers.Assert;
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  
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 Map<ObjectName, Component> contextObjNameToTabbedPaneMap = new HashMap<ObjectName, Component>();
75      private 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.isNotNull(client, "client");
80          createWidgets();
81          populateWidgets();
82  
83          // register for Notifications if LoggerContext MBean was added/removed
84          ObjectName addRemoveNotifs = MBeanServerDelegate.DELEGATE_NAME;
85          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         JTabbedPane contextTabs = new JTabbedPane();
105         contextObjNameToTabbedPaneMap.put(ctx.getObjectName(), contextTabs);
106         tabbedPaneContexts.addTab("LoggerContext: " + ctx.getName(), contextTabs);
107 
108         String contextName = ctx.getName();
109         StatusLoggerAdminMBean status = client.getStatusLoggerAdmin(contextName);
110         if (status != null) {
111             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             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(ObjectName loggerContextObjName) throws JMException, IOException {
127         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 (ListenerNotFoundException ignored) {
137         }
138     }
139 
140     private JTextArea createTextArea() {
141         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(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         if (StatusLoggerAdminMBean.NOTIF_TYPE_MESSAGE.equals(notif.getType())) {
181             JTextArea text = statusLogTextAreaMap.get(paramObject);
182             if (text != null) {
183                 text.append(notif.getMessage() + "\n");
184             }
185             return;
186         }
187         if (notif instanceof MBeanServerNotification) {
188             MBeanServerNotification mbsn = (MBeanServerNotification) notif;
189             ObjectName mbeanName = mbsn.getMBeanName();
190             if (MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(notif.getType())) {
191                 onMBeanRegistered(mbeanName);
192             } else if (MBeanServerNotification.UNREGISTRATION_NOTIFICATION.equals(notif.getType())) {
193                 onMBeanUnregistered(mbeanName);
194             }
195         }
196     }
197 
198     /**
199      * Called every time a Log4J2 MBean was registered in the MBean server.
200      * 
201      * @param mbeanName ObjectName of the registered Log4J2 MBean
202      */
203     private void onMBeanRegistered(ObjectName mbeanName) {
204         if (client.isLoggerContext(mbeanName)) {
205             try {
206                 LoggerContextAdminMBean ctx = client.getLoggerContextAdmin(mbeanName);
207                 addWidgetForLoggerContext(ctx);
208             } catch (Exception ex) {
209                 handle("Could not add tab for new MBean " + mbeanName, ex);
210             }
211         }
212     }
213 
214     /**
215      * Called every time a Log4J2 MBean was unregistered from the MBean server.
216      * 
217      * @param mbeanName ObjectName of the unregistered Log4J2 MBean
218      */
219     private void onMBeanUnregistered(ObjectName mbeanName) {
220         if (client.isLoggerContext(mbeanName)) {
221             try {
222                 removeWidgetForLoggerContext(mbeanName);
223             } catch (Exception ex) {
224                 handle("Could not remove tab for " + mbeanName, ex);
225             }
226         }
227     }
228 
229     private void handle(String msg, Exception ex) {
230         System.err.println(msg);
231         ex.printStackTrace();
232 
233         StringWriter sw = new StringWriter(INITIAL_STRING_WRITER_SIZE);
234         ex.printStackTrace(new PrintWriter(sw));
235         JOptionPane.showMessageDialog(this, sw.toString(), msg, JOptionPane.ERROR_MESSAGE);
236     }
237 
238     /**
239      * Connects to the specified location and shows this panel in a window.
240      * <p>
241      * Useful links:
242      * http://www.componative.com/content/controller/developer/insights
243      * /jconsole3/
244      * 
245      * @param args must have at least one parameter, which specifies the
246      *            location to connect to. Must be of the form {@code host:port}
247      *            or {@code service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi}
248      *            or
249      *            {@code service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi}
250      * @throws Exception if anything goes wrong
251      */
252     public static void main(final String[] args) throws Exception {
253         if (args.length < 1) {
254             usage();
255             return;
256         }
257         String serviceUrl = args[0];
258         if (!serviceUrl.startsWith("service:jmx")) {
259             serviceUrl = "service:jmx:rmi:///jndi/rmi://" + args[0] + "/jmxrmi";
260         }
261         final JMXServiceURL url = new JMXServiceURL(serviceUrl);
262         final Map<String, String> paramMap = new HashMap<String, String>();
263         for (final Object objKey : System.getProperties().keySet()) {
264             final String key = (String) objKey;
265             paramMap.put(key, System.getProperties().getProperty(key));
266         }
267         final JMXConnector connector = JMXConnectorFactory.connect(url, paramMap);
268         final Client client = new Client(connector);
269         final String title = "Log4j JMX Client - " + url;
270 
271         SwingUtilities.invokeLater(new Runnable() {
272             @Override
273             public void run() {
274                 installLookAndFeel();
275                 try {
276                     final ClientGUI gui = new ClientGUI(client);
277                     final JFrame frame = new JFrame(title);
278                     frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
279                     frame.getContentPane().add(gui, BorderLayout.CENTER);
280                     frame.pack();
281                     frame.setVisible(true);
282                 } catch (final Exception ex) {
283                     // if console is visible, print error so that
284                     // the stack trace remains visible after error dialog is
285                     // closed
286                     ex.printStackTrace();
287 
288                     // show error in dialog: there may not be a console window
289                     // visible
290                     final StringWriter sr = new StringWriter();
291                     ex.printStackTrace(new PrintWriter(sr));
292                     JOptionPane.showMessageDialog(null, sr.toString(), "Error", JOptionPane.ERROR_MESSAGE);
293                 }
294             }
295         });
296     }
297 
298     private static void usage() {
299         final String me = ClientGUI.class.getName();
300         System.err.println("Usage: java " + me + " <host>:<port>");
301         System.err.println("   or: java " + me + " service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi");
302         final String longAdr = " service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi";
303         System.err.println("   or: java " + me + longAdr);
304     }
305 
306     private static void installLookAndFeel() {
307         try {
308             for (final LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
309                 if ("Nimbus".equals(info.getName())) {
310                     UIManager.setLookAndFeel(info.getClassName());
311                     return;
312                 }
313             }
314         } catch (final Exception ex) {
315             ex.printStackTrace();
316         }
317         try {
318             UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
319         } catch (final Exception e) {
320             e.printStackTrace();
321         }
322     }
323 }