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 }