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 */ 017package org.apache.logging.log4j.jmx.gui; 018 019import java.awt.BorderLayout; 020import java.awt.Color; 021import java.awt.Component; 022import java.awt.Font; 023import java.awt.event.ActionEvent; 024import java.io.IOException; 025import java.io.PrintWriter; 026import java.io.StringWriter; 027import java.util.HashMap; 028import java.util.Map; 029import java.util.Objects; 030import java.util.Properties; 031 032import javax.management.InstanceNotFoundException; 033import javax.management.JMException; 034import javax.management.ListenerNotFoundException; 035import javax.management.MBeanServerDelegate; 036import javax.management.MBeanServerNotification; 037import javax.management.MalformedObjectNameException; 038import javax.management.Notification; 039import javax.management.NotificationFilterSupport; 040import javax.management.NotificationListener; 041import javax.management.ObjectName; 042import javax.management.remote.JMXConnector; 043import javax.management.remote.JMXConnectorFactory; 044import javax.management.remote.JMXServiceURL; 045import javax.swing.AbstractAction; 046import javax.swing.JFrame; 047import javax.swing.JOptionPane; 048import javax.swing.JPanel; 049import javax.swing.JScrollPane; 050import javax.swing.JTabbedPane; 051import javax.swing.JTextArea; 052import javax.swing.JToggleButton; 053import javax.swing.ScrollPaneConstants; 054import javax.swing.SwingUtilities; 055import javax.swing.UIManager; 056import javax.swing.UIManager.LookAndFeelInfo; 057import javax.swing.WindowConstants; 058 059import org.apache.logging.log4j.core.jmx.LoggerContextAdminMBean; 060import org.apache.logging.log4j.core.jmx.Server; 061import org.apache.logging.log4j.core.jmx.StatusLoggerAdminMBean; 062 063/** 064 * Swing GUI that connects to a Java process via JMX and allows the user to view 065 * and modify the Log4j 2 configuration, as well as monitor status logs. 066 * 067 * @see <a href= 068 * "http://docs.oracle.com/javase/6/docs/technotes/guides/management/jconsole.html" 069 * >http://docs.oracle.com/javase/6/docs/technotes/guides/management/ 070 * jconsole.html</a > 071 */ 072public class ClientGui extends JPanel implements NotificationListener { 073 private static final long serialVersionUID = -253621277232291174L; 074 private static final int INITIAL_STRING_WRITER_SIZE = 1024; 075 private final Client client; 076 private final Map<ObjectName, Component> contextObjNameToTabbedPaneMap = new HashMap<>(); 077 private final Map<ObjectName, JTextArea> statusLogTextAreaMap = new HashMap<>(); 078 private JTabbedPane tabbedPaneContexts; 079 080 public ClientGui(final Client client) throws IOException, JMException { 081 this.client = Objects.requireNonNull(client, "client"); 082 createWidgets(); 083 populateWidgets(); 084 085 // register for Notifications if LoggerContext MBean was added/removed 086 final ObjectName addRemoveNotifs = MBeanServerDelegate.DELEGATE_NAME; 087 final NotificationFilterSupport filter = new NotificationFilterSupport(); 088 filter.enableType(Server.DOMAIN); // only interested in Log4J2 MBeans 089 client.getConnection().addNotificationListener(addRemoveNotifs, this, null, null); 090 } 091 092 private void createWidgets() { 093 tabbedPaneContexts = new JTabbedPane(); 094 this.setLayout(new BorderLayout()); 095 this.add(tabbedPaneContexts, BorderLayout.CENTER); 096 } 097 098 private void populateWidgets() throws IOException, JMException { 099 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}