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.Font;
022    import java.awt.event.ActionEvent;
023    import java.io.IOException;
024    import java.io.PrintWriter;
025    import java.io.StringWriter;
026    import java.util.HashMap;
027    import java.util.Map;
028    
029    import javax.management.InstanceNotFoundException;
030    import javax.management.MalformedObjectNameException;
031    import javax.management.Notification;
032    import javax.management.NotificationFilterSupport;
033    import javax.management.NotificationListener;
034    import javax.management.ObjectName;
035    import javax.management.remote.JMXConnector;
036    import javax.management.remote.JMXConnectorFactory;
037    import javax.management.remote.JMXServiceURL;
038    import javax.swing.AbstractAction;
039    import javax.swing.JFrame;
040    import javax.swing.JOptionPane;
041    import javax.swing.JPanel;
042    import javax.swing.JScrollPane;
043    import javax.swing.JTabbedPane;
044    import javax.swing.JTextArea;
045    import javax.swing.JToggleButton;
046    import javax.swing.SwingUtilities;
047    import javax.swing.UIManager;
048    import javax.swing.UIManager.LookAndFeelInfo;
049    
050    import org.apache.logging.log4j.core.helpers.Assert;
051    import org.apache.logging.log4j.core.jmx.LoggerContextAdminMBean;
052    import org.apache.logging.log4j.core.jmx.StatusLoggerAdminMBean;
053    
054    /**
055     * Swing GUI that connects to a Java process via JMX and allows the user to view and
056     * modify the Log4j 2 configuration, as well as monitor status logs.
057     * 
058     * @see <a href=
059     *      "http://docs.oracle.com/javase/6/docs/technotes/guides/management/jconsole.html"
060     *      >http://docs.oracle.com/javase/6/docs/technotes/guides/management/jconsole.html</a >
061     */
062    public class ClientGUI extends JPanel implements NotificationListener {
063        private static final long serialVersionUID = -253621277232291174L;
064        private final Client client;
065        private JTextArea statusLogTextArea;
066        private JTabbedPane tabbedPane;
067        private JToggleButton wrapLinesToggleButton;
068    
069        private final AbstractAction toggleWrapAction = new AbstractAction() {
070            private static final long serialVersionUID = -4214143754637722322L;
071    
072            @Override
073            public void actionPerformed(final ActionEvent e) {
074                final boolean wrap = wrapLinesToggleButton.isSelected();
075                statusLogTextArea.setLineWrap(wrap);
076            }
077        };
078    
079        public ClientGUI(final Client client) throws InstanceNotFoundException,
080                MalformedObjectNameException, IOException {
081            this.client = Assert.isNotNull(client, "client");
082            createWidgets();
083            populateWidgets();
084            registerListeners();
085        }
086    
087        private void createWidgets() {
088            statusLogTextArea = new JTextArea();
089            statusLogTextArea.setEditable(false);
090            statusLogTextArea.setBackground(this.getBackground());
091            statusLogTextArea.setForeground(Color.black);
092            statusLogTextArea.setFont(new Font("Monospaced", Font.PLAIN,
093                    statusLogTextArea.getFont().getSize()));
094            statusLogTextArea.setWrapStyleWord(true);
095    
096            wrapLinesToggleButton = new JToggleButton(toggleWrapAction);
097            wrapLinesToggleButton.setToolTipText("Toggle line wrapping");
098            final JScrollPane scrollStatusLog = new JScrollPane(statusLogTextArea, //
099                    JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, //
100                    JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
101            scrollStatusLog.setCorner(JScrollPane.LOWER_RIGHT_CORNER, wrapLinesToggleButton);
102    
103            tabbedPane = new JTabbedPane();
104            tabbedPane.addTab("StatusLogger", scrollStatusLog);
105    
106            this.setLayout(new BorderLayout());
107            this.add(tabbedPane, BorderLayout.CENTER);
108        }
109    
110        private void populateWidgets() {
111    
112            final StatusLoggerAdminMBean statusAdmin = client.getStatusLoggerAdmin();
113            final String[] messages = statusAdmin.getStatusDataHistory();
114            for (final String message : messages) {
115                statusLogTextArea.append(message + "\n");
116            }
117    
118            for (final LoggerContextAdminMBean ctx : client.getLoggerContextAdmins()) {
119                final ClientEditConfigPanel editor = new ClientEditConfigPanel(ctx);
120                tabbedPane.addTab("LoggerContext: " + ctx.getName(), editor);
121            }
122        }
123    
124        private void registerListeners() throws InstanceNotFoundException,
125                MalformedObjectNameException, IOException {
126            final NotificationFilterSupport filter = new NotificationFilterSupport();
127            filter.enableType(StatusLoggerAdminMBean.NOTIF_TYPE_MESSAGE);
128            final ObjectName objName = new ObjectName(StatusLoggerAdminMBean.NAME);
129            client.getConnection().addNotificationListener(objName, this, filter,
130                    null);
131        }
132    
133        @Override
134        public void handleNotification(final Notification notif, final Object paramObject) {
135            if (StatusLoggerAdminMBean.NOTIF_TYPE_MESSAGE.equals(notif.getType())) {
136                statusLogTextArea.append(notif.getMessage() + "\n");
137            }
138        }
139    
140        /**
141         * Connects to the specified location and shows this panel in a window.
142         * <p>
143         * Useful links:
144         * http://www.componative.com/content/controller/developer/insights
145         * /jconsole3/
146         * 
147         * @param args must have at least one parameter, which specifies the
148         *            location to connect to. Must be of the form {@code host:port}
149         *            or {@code service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi}
150         *            or
151         *            {@code service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi}
152         * @throws Exception if anything goes wrong
153         */
154        public static void main(final String[] args) throws Exception {
155            if (args.length < 1) {
156                usage();
157                return;
158            }
159            String serviceUrl = args[0];
160            if (!serviceUrl.startsWith("service:jmx")) {
161                serviceUrl = "service:jmx:rmi:///jndi/rmi://" + args[0] + "/jmxrmi";
162            }
163            final JMXServiceURL url = new JMXServiceURL(serviceUrl);
164            final Map<String, String> paramMap = new HashMap<String, String>();
165            for (final Object objKey : System.getProperties().keySet()) {
166                final String key = (String) objKey;
167                paramMap.put(key, System.getProperties().getProperty(key));
168            }
169            final JMXConnector connector = JMXConnectorFactory.connect(url, paramMap);
170            final Client client = new Client(connector);
171            final String title = "Log4j JMX Client - " + url;
172    
173            SwingUtilities.invokeLater(new Runnable() {
174                @Override
175                public void run() {
176                    installLookAndFeel();
177                    try {
178                        final ClientGUI gui = new ClientGUI(client);
179                        final JFrame frame = new JFrame(title);
180                        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
181                        frame.getContentPane().add(gui, BorderLayout.CENTER);
182                        frame.pack();
183                        frame.setVisible(true);
184                    } catch (final Exception ex) {
185                        // if console is visible, print error so that
186                        // the stack trace remains visible after error dialog is
187                        // closed
188                        ex.printStackTrace();
189    
190                        // show error in dialog: there may not be a console window
191                        // visible
192                        final StringWriter sr = new StringWriter();
193                        ex.printStackTrace(new PrintWriter(sr));
194                        JOptionPane.showMessageDialog(null, sr.toString(), "Error",
195                                JOptionPane.ERROR_MESSAGE);
196                    }
197                }
198            });
199        }
200    
201        private static void usage() {
202            final String me = ClientGUI.class.getName();
203            System.err.println("Usage: java " + me + " <host>:<port>");
204            System.err.println("   or: java " + me
205                    + " service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi");
206            final String longAdr = " service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi";
207            System.err.println("   or: java " + me + longAdr);
208        }
209    
210        private static void installLookAndFeel() {
211            try {
212                for (final LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
213                    if ("Nimbus".equals(info.getName())) {
214                        UIManager.setLookAndFeel(info.getClassName());
215                        return;
216                    }
217                }
218            } catch (final Exception ex) {
219                ex.printStackTrace();
220            }
221            try {
222                UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
223            } catch (final Exception e) {
224                e.printStackTrace();
225            }
226        }
227    }