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(() -> 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}