blob: 4a9e56f3feb48b694df415a4b87d9a06fc4eb090 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache license, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the license for the specific language governing permissions and
* limitations under the license.
*/
package org.apache.logging.log4j.jmx.gui;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import javax.management.InstanceNotFoundException;
import javax.management.JMException;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanServerDelegate;
import javax.management.MBeanServerNotification;
import javax.management.MalformedObjectNameException;
import javax.management.Notification;
import javax.management.NotificationFilterSupport;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JToggleButton;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;
import javax.swing.WindowConstants;
import org.apache.logging.log4j.core.jmx.LoggerContextAdminMBean;
import org.apache.logging.log4j.core.jmx.Server;
import org.apache.logging.log4j.core.jmx.StatusLoggerAdminMBean;
/**
* Swing GUI that connects to a Java process via JMX and allows the user to view
* and modify the Log4j 2 configuration, as well as monitor status logs.
*
* @see <a href=
* "http://docs.oracle.com/javase/6/docs/technotes/guides/management/jconsole.html"
* >http://docs.oracle.com/javase/6/docs/technotes/guides/management/
* jconsole.html</a >
*/
public class ClientGui extends JPanel implements NotificationListener {
private static final long serialVersionUID = -253621277232291174L;
private static final int INITIAL_STRING_WRITER_SIZE = 1024;
private final Client client;
private final Map<ObjectName, Component> contextObjNameToTabbedPaneMap = new HashMap<>();
private final Map<ObjectName, JTextArea> statusLogTextAreaMap = new HashMap<>();
private JTabbedPane tabbedPaneContexts;
public ClientGui(final Client client) throws IOException, JMException {
this.client = Objects.requireNonNull(client, "client");
createWidgets();
populateWidgets();
// register for Notifications if LoggerContext MBean was added/removed
final ObjectName addRemoveNotifs = MBeanServerDelegate.DELEGATE_NAME;
final NotificationFilterSupport filter = new NotificationFilterSupport();
filter.enableType(Server.DOMAIN); // only interested in Log4J2 MBeans
client.getConnection().addNotificationListener(addRemoveNotifs, this, null, null);
}
private void createWidgets() {
tabbedPaneContexts = new JTabbedPane();
this.setLayout(new BorderLayout());
this.add(tabbedPaneContexts, BorderLayout.CENTER);
}
private void populateWidgets() throws IOException, JMException {
for (final LoggerContextAdminMBean ctx : client.getLoggerContextAdmins()) {
addWidgetForLoggerContext(ctx);
}
}
private void addWidgetForLoggerContext(final LoggerContextAdminMBean ctx) throws MalformedObjectNameException,
IOException, InstanceNotFoundException {
final JTabbedPane contextTabs = new JTabbedPane();
contextObjNameToTabbedPaneMap.put(ctx.getObjectName(), contextTabs);
tabbedPaneContexts.addTab("LoggerContext: " + ctx.getName(), contextTabs);
final String contextName = ctx.getName();
final StatusLoggerAdminMBean status = client.getStatusLoggerAdmin(contextName);
if (status != null) {
final JTextArea text = createTextArea();
final String[] messages = status.getStatusDataHistory();
for (final String message : messages) {
text.append(message + '\n');
}
statusLogTextAreaMap.put(ctx.getObjectName(), text);
registerListeners(status);
final JScrollPane scroll = scroll(text);
contextTabs.addTab("StatusLogger", scroll);
}
final ClientEditConfigPanel editor = new ClientEditConfigPanel(ctx);
contextTabs.addTab("Configuration", editor);
}
private void removeWidgetForLoggerContext(final ObjectName loggerContextObjName) throws JMException, IOException {
final Component tab = contextObjNameToTabbedPaneMap.get(loggerContextObjName);
if (tab != null) {
tabbedPaneContexts.remove(tab);
}
statusLogTextAreaMap.remove(loggerContextObjName);
final ObjectName objName = client.getStatusLoggerObjectName(loggerContextObjName);
try {
// System.out.println("Remove listener for " + objName);
client.getConnection().removeNotificationListener(objName, this);
} catch (final ListenerNotFoundException ignored) {
}
}
private JTextArea createTextArea() {
final JTextArea result = new JTextArea();
result.setEditable(false);
result.setBackground(this.getBackground());
result.setForeground(Color.black);
result.setFont(new Font(Font.MONOSPACED, Font.PLAIN, result.getFont().getSize()));
result.setWrapStyleWord(true);
return result;
}
private JScrollPane scroll(final JTextArea text) {
final JToggleButton toggleButton = new JToggleButton();
toggleButton.setAction(new AbstractAction() {
private static final long serialVersionUID = -4214143754637722322L;
@Override
public void actionPerformed(final ActionEvent e) {
final boolean wrap = toggleButton.isSelected();
text.setLineWrap(wrap);
}
});
toggleButton.setToolTipText("Toggle line wrapping");
final JScrollPane scrollStatusLog = new JScrollPane(text, //
ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, //
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
scrollStatusLog.setCorner(ScrollPaneConstants.LOWER_RIGHT_CORNER, toggleButton);
return scrollStatusLog;
}
private void registerListeners(final StatusLoggerAdminMBean status) throws InstanceNotFoundException,
MalformedObjectNameException, IOException {
final NotificationFilterSupport filter = new NotificationFilterSupport();
filter.enableType(StatusLoggerAdminMBean.NOTIF_TYPE_MESSAGE);
final ObjectName objName = status.getObjectName();
// System.out.println("Add listener for " + objName);
client.getConnection().addNotificationListener(objName, this, filter, status.getContextName());
}
@Override
public void handleNotification(final Notification notif, final Object paramObject) {
SwingUtilities.invokeLater(() -> { // LOG4J2-538
handleNotificationInAwtEventThread(notif, paramObject);
});
}
private void handleNotificationInAwtEventThread(final Notification notif, final Object paramObject) {
if (StatusLoggerAdminMBean.NOTIF_TYPE_MESSAGE.equals(notif.getType())) {
if (!(paramObject instanceof ObjectName)) {
handle("Invalid notification object type", new ClassCastException(paramObject.getClass().getName()));
return;
}
final ObjectName param = (ObjectName) paramObject;
final JTextArea text = statusLogTextAreaMap.get(param);
if (text != null) {
text.append(notif.getMessage() + '\n');
}
return;
}
if (notif instanceof MBeanServerNotification) {
final MBeanServerNotification mbsn = (MBeanServerNotification) notif;
final ObjectName mbeanName = mbsn.getMBeanName();
if (MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(notif.getType())) {
onMBeanRegistered(mbeanName);
} else if (MBeanServerNotification.UNREGISTRATION_NOTIFICATION.equals(notif.getType())) {
onMBeanUnregistered(mbeanName);
}
}
}
/**
* Called every time a Log4J2 MBean was registered in the MBean server.
*
* @param mbeanName ObjectName of the registered Log4J2 MBean
*/
private void onMBeanRegistered(final ObjectName mbeanName) {
if (client.isLoggerContext(mbeanName)) {
try {
final LoggerContextAdminMBean ctx = client.getLoggerContextAdmin(mbeanName);
addWidgetForLoggerContext(ctx);
} catch (final Exception ex) {
handle("Could not add tab for new MBean " + mbeanName, ex);
}
}
}
/**
* Called every time a Log4J2 MBean was unregistered from the MBean server.
*
* @param mbeanName ObjectName of the unregistered Log4J2 MBean
*/
private void onMBeanUnregistered(final ObjectName mbeanName) {
if (client.isLoggerContext(mbeanName)) {
try {
removeWidgetForLoggerContext(mbeanName);
} catch (final Exception ex) {
handle("Could not remove tab for " + mbeanName, ex);
}
}
}
private void handle(final String msg, final Exception ex) {
System.err.println(msg);
ex.printStackTrace();
final StringWriter sw = new StringWriter(INITIAL_STRING_WRITER_SIZE);
ex.printStackTrace(new PrintWriter(sw));
JOptionPane.showMessageDialog(this, sw.toString(), msg, JOptionPane.ERROR_MESSAGE);
}
/**
* Connects to the specified location and shows this panel in a window.
* <p>
* Useful links:
* http://www.componative.com/content/controller/developer/insights
* /jconsole3/
*
* @param args must have at least one parameter, which specifies the
* location to connect to. Must be of the form {@code host:port}
* or {@code service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi}
* or
* {@code service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi}
* @throws Exception if anything goes wrong
*/
public static void main(final String[] args) throws Exception {
if (args.length < 1) {
usage();
return;
}
String serviceUrl = args[0];
if (!serviceUrl.startsWith("service:jmx")) {
serviceUrl = "service:jmx:rmi:///jndi/rmi://" + args[0] + "/jmxrmi";
}
final JMXServiceURL url = new JMXServiceURL(serviceUrl);
final Properties props = System.getProperties();
final Map<String, String> paramMap = new HashMap<>(props.size());
for (final String key : props.stringPropertyNames()) {
paramMap.put(key, props.getProperty(key));
}
final JMXConnector connector = JMXConnectorFactory.connect(url, paramMap);
final Client client = new Client(connector);
final String title = "Log4j JMX Client - " + url;
SwingUtilities.invokeLater(() -> {
installLookAndFeel();
try {
final ClientGui gui = new ClientGui(client);
final JFrame frame = new JFrame(title);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.getContentPane().add(gui, BorderLayout.CENTER);
frame.pack();
frame.setVisible(true);
} catch (final Exception ex) {
// if console is visible, print error so that
// the stack trace remains visible after error dialog is
// closed
ex.printStackTrace();
// show error in dialog: there may not be a console window
// visible
final StringWriter sr = new StringWriter();
ex.printStackTrace(new PrintWriter(sr));
JOptionPane.showMessageDialog(null, sr.toString(), "Error", JOptionPane.ERROR_MESSAGE);
}
});
}
private static void usage() {
final String me = ClientGui.class.getName();
System.err.println("Usage: java " + me + " <host>:<port>");
System.err.println(" or: java " + me + " service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi");
final String longAdr = " service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi";
System.err.println(" or: java " + me + longAdr);
}
private static void installLookAndFeel() {
try {
for (final LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
UIManager.setLookAndFeel(info.getClassName());
return;
}
}
} catch (final Exception ex) {
ex.printStackTrace();
}
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (final Exception e) {
e.printStackTrace();
}
}
}