001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache license, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the license for the specific language governing permissions and 015 * limitations under the license. 016 */ 017 package org.apache.logging.log4j.jmx.gui; 018 019 import java.awt.BorderLayout; 020 import java.awt.Color; 021 import java.awt.Component; 022 import java.awt.Font; 023 import java.awt.event.ActionEvent; 024 import java.io.IOException; 025 import java.io.PrintWriter; 026 import java.io.StringWriter; 027 import java.util.HashMap; 028 import java.util.Map; 029 030 import javax.management.InstanceNotFoundException; 031 import javax.management.JMException; 032 import javax.management.ListenerNotFoundException; 033 import javax.management.MBeanServerDelegate; 034 import javax.management.MBeanServerNotification; 035 import javax.management.MalformedObjectNameException; 036 import javax.management.Notification; 037 import javax.management.NotificationFilterSupport; 038 import javax.management.NotificationListener; 039 import javax.management.ObjectName; 040 import javax.management.remote.JMXConnector; 041 import javax.management.remote.JMXConnectorFactory; 042 import javax.management.remote.JMXServiceURL; 043 import javax.swing.AbstractAction; 044 import javax.swing.JFrame; 045 import javax.swing.JOptionPane; 046 import javax.swing.JPanel; 047 import javax.swing.JScrollPane; 048 import javax.swing.JTabbedPane; 049 import javax.swing.JTextArea; 050 import javax.swing.JToggleButton; 051 import javax.swing.ScrollPaneConstants; 052 import javax.swing.SwingUtilities; 053 import javax.swing.UIManager; 054 import javax.swing.UIManager.LookAndFeelInfo; 055 056 import org.apache.logging.log4j.core.jmx.LoggerContextAdminMBean; 057 import org.apache.logging.log4j.core.jmx.Server; 058 import org.apache.logging.log4j.core.jmx.StatusLoggerAdminMBean; 059 import org.apache.logging.log4j.core.util.Assert; 060 061 /** 062 * Swing GUI that connects to a Java process via JMX and allows the user to view 063 * and modify the Log4j 2 configuration, as well as monitor status logs. 064 * 065 * @see <a href= 066 * "http://docs.oracle.com/javase/6/docs/technotes/guides/management/jconsole.html" 067 * >http://docs.oracle.com/javase/6/docs/technotes/guides/management/ 068 * jconsole.html</a > 069 */ 070 public class ClientGui extends JPanel implements NotificationListener { 071 private static final long serialVersionUID = -253621277232291174L; 072 private static final int INITIAL_STRING_WRITER_SIZE = 1024; 073 private final Client client; 074 private final Map<ObjectName, Component> contextObjNameToTabbedPaneMap = new HashMap<ObjectName, Component>(); 075 private final Map<ObjectName, JTextArea> statusLogTextAreaMap = new HashMap<ObjectName, JTextArea>(); 076 private JTabbedPane tabbedPaneContexts; 077 078 public ClientGui(final Client client) throws IOException, JMException { 079 this.client = Assert.requireNonNull(client, "client"); 080 createWidgets(); 081 populateWidgets(); 082 083 // register for Notifications if LoggerContext MBean was added/removed 084 final ObjectName addRemoveNotifs = MBeanServerDelegate.DELEGATE_NAME; 085 final NotificationFilterSupport filter = new NotificationFilterSupport(); 086 filter.enableType(Server.DOMAIN); // only interested in Log4J2 MBeans 087 client.getConnection().addNotificationListener(addRemoveNotifs, this, null, null); 088 } 089 090 private void createWidgets() { 091 tabbedPaneContexts = new JTabbedPane(); 092 this.setLayout(new BorderLayout()); 093 this.add(tabbedPaneContexts, BorderLayout.CENTER); 094 } 095 096 private void populateWidgets() throws IOException, JMException { 097 for (final LoggerContextAdminMBean ctx : client.getLoggerContextAdmins()) { 098 addWidgetForLoggerContext(ctx); 099 } 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 }