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 import java.util.Properties; 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 import javax.swing.WindowConstants; 056 057 import org.apache.logging.log4j.core.jmx.LoggerContextAdminMBean; 058 import org.apache.logging.log4j.core.jmx.Server; 059 import org.apache.logging.log4j.core.jmx.StatusLoggerAdminMBean; 060 import org.apache.logging.log4j.core.util.Assert; 061 062 /** 063 * Swing GUI that connects to a Java process via JMX and allows the user to view 064 * and modify the Log4j 2 configuration, as well as monitor status logs. 065 * 066 * @see <a href= 067 * "http://docs.oracle.com/javase/6/docs/technotes/guides/management/jconsole.html" 068 * >http://docs.oracle.com/javase/6/docs/technotes/guides/management/ 069 * jconsole.html</a > 070 */ 071 public class ClientGui extends JPanel implements NotificationListener { 072 private static final long serialVersionUID = -253621277232291174L; 073 private static final int INITIAL_STRING_WRITER_SIZE = 1024; 074 private final Client client; 075 private final Map<ObjectName, Component> contextObjNameToTabbedPaneMap = new HashMap<ObjectName, Component>(); 076 private final Map<ObjectName, JTextArea> statusLogTextAreaMap = new HashMap<ObjectName, JTextArea>(); 077 private JTabbedPane tabbedPaneContexts; 078 079 public ClientGui(final Client client) throws IOException, JMException { 080 this.client = Assert.requireNonNull(client, "client"); 081 createWidgets(); 082 populateWidgets(); 083 084 // register for Notifications if LoggerContext MBean was added/removed 085 final ObjectName addRemoveNotifs = MBeanServerDelegate.DELEGATE_NAME; 086 final NotificationFilterSupport filter = new NotificationFilterSupport(); 087 filter.enableType(Server.DOMAIN); // only interested in Log4J2 MBeans 088 client.getConnection().addNotificationListener(addRemoveNotifs, this, null, null); 089 } 090 091 private void createWidgets() { 092 tabbedPaneContexts = new JTabbedPane(); 093 this.setLayout(new BorderLayout()); 094 this.add(tabbedPaneContexts, BorderLayout.CENTER); 095 } 096 097 private void populateWidgets() throws IOException, JMException { 098 for (final LoggerContextAdminMBean ctx : client.getLoggerContextAdmins()) { 099 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 }