View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.jmx;
18  
19  import java.lang.management.ManagementFactory;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.Set;
23  import java.util.concurrent.Executor;
24  import java.util.concurrent.ExecutorService;
25  import java.util.concurrent.Executors;
26  
27  import javax.management.InstanceAlreadyExistsException;
28  import javax.management.MBeanRegistrationException;
29  import javax.management.MBeanServer;
30  import javax.management.NotCompliantMBeanException;
31  import javax.management.ObjectName;
32  
33  import org.apache.logging.log4j.LogManager;
34  import org.apache.logging.log4j.core.Appender;
35  import org.apache.logging.log4j.core.LoggerContext;
36  import org.apache.logging.log4j.core.appender.AsyncAppender;
37  import org.apache.logging.log4j.core.async.AsyncLoggerConfig;
38  import org.apache.logging.log4j.core.async.AsyncLoggerContext;
39  import org.apache.logging.log4j.core.async.DaemonThreadFactory;
40  import org.apache.logging.log4j.core.config.LoggerConfig;
41  import org.apache.logging.log4j.core.impl.Log4jContextFactory;
42  import org.apache.logging.log4j.core.selector.ContextSelector;
43  import org.apache.logging.log4j.core.util.Loader;
44  import org.apache.logging.log4j.spi.LoggerContextFactory;
45  import org.apache.logging.log4j.status.StatusLogger;
46  import org.apache.logging.log4j.util.PropertiesUtil;
47  
48  /**
49   * Creates MBeans to instrument various classes in the log4j class hierarchy.
50   * <p>
51   * All instrumentation for Log4j 2 classes can be disabled by setting system property {@code -Dlog4j2.disable.jmx=true}.
52   * </p>
53   */
54  public final class Server {
55  
56      /**
57       * The domain part, or prefix ({@value}) of the {@code ObjectName} of all MBeans that instrument Log4J2 components.
58       */
59      public static final String DOMAIN = "org.apache.logging.log4j2";
60      private static final String PROPERTY_DISABLE_JMX = "log4j2.disable.jmx";
61      private static final String PROPERTY_ASYNC_NOTIF = "log4j2.jmx.notify.async";
62      private static final String THREAD_NAME_PREFIX = "log4j2.jmx.notif";
63      private static final StatusLogger LOGGER = StatusLogger.getLogger();
64      static final Executor executor = isJmxDisabled() ? null : createExecutor();
65  
66      private Server() {
67      }
68  
69      /**
70       * Returns either a {@code null} Executor (causing JMX notifications to be sent from the caller thread) or a daemon
71       * background thread Executor, depending on the value of system property "log4j2.jmx.notify.async". If this
72       * property is not set, use a {@code null} Executor for web apps to avoid memory leaks and other issues when the
73       * web app is restarted.
74       * @see <a href="https://issues.apache.org/jira/browse/LOG4J2-938">LOG4J2-938</a>
75       */
76      private static ExecutorService createExecutor() {
77          final boolean defaultAsync = !isWebApp();
78          final boolean async = PropertiesUtil.getProperties().getBooleanProperty(PROPERTY_ASYNC_NOTIF, defaultAsync);
79          return async ? Executors.newFixedThreadPool(1, new DaemonThreadFactory(THREAD_NAME_PREFIX)) : null;
80      }
81  
82      /**
83       * Returns {@code true} if we think we are running in a web container, based on the presence of the
84       * {@code javax.servlet.Servlet} class in the classpath.
85       */
86      private static boolean isWebApp() {
87          return Loader.isClassAvailable("javax.servlet.Servlet");
88      }
89  
90      /**
91       * Either returns the specified name as is, or returns a quoted value containing the specified name with the special
92       * characters (comma, equals, colon, quote, asterisk, or question mark) preceded with a backslash.
93       *
94       * @param name the name to escape so it can be used as a value in an {@link ObjectName}.
95       * @return the escaped name
96       */
97      public static String escape(final String name) {
98          final StringBuilder sb = new StringBuilder(name.length() * 2);
99          boolean needsQuotes = false;
100         for (int i = 0; i < name.length(); i++) {
101             final char c = name.charAt(i);
102             switch (c) {
103             case '\\':
104             case '*':
105             case '?':
106             case '\"':
107                 // quote, star, question & backslash must be escaped
108                 sb.append('\\');
109                 needsQuotes = true; // ... and can only appear in quoted value
110                 break;
111             case ',':
112             case '=':
113             case ':':
114                 // no need to escape these, but value must be quoted
115                 needsQuotes = true;
116                 break;
117             case '\r':
118                 // drop \r characters: \\r gives "invalid escape sequence"
119                 continue;
120             case '\n':
121                 // replace \n characters with \\n sequence
122                 sb.append("\\n");
123                 needsQuotes = true;
124                 continue;
125             }
126             sb.append(c);
127         }
128         if (needsQuotes) {
129             sb.insert(0, '\"');
130             sb.append('\"');
131         }
132         return sb.toString();
133     }
134 
135     private static boolean isJmxDisabled() {
136         return PropertiesUtil.getProperties().getBooleanProperty(PROPERTY_DISABLE_JMX);
137     }
138 
139     public static void reregisterMBeansAfterReconfigure() {
140         // avoid creating Platform MBean Server if JMX disabled
141         if (isJmxDisabled()) {
142             LOGGER.debug("JMX disabled for log4j2. Not registering MBeans.");
143             return;
144         }
145         final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
146         reregisterMBeansAfterReconfigure(mbs);
147     }
148 
149     public static void reregisterMBeansAfterReconfigure(final MBeanServer mbs) {
150         if (isJmxDisabled()) {
151             LOGGER.debug("JMX disabled for log4j2. Not registering MBeans.");
152             return;
153         }
154 
155         // now provide instrumentation for the newly configured
156         // LoggerConfigs and Appenders
157         try {
158             final ContextSelector selector = getContextSelector();
159             if (selector == null) {
160                 LOGGER.debug("Could not register MBeans: no ContextSelector found.");
161                 return;
162             }
163             LOGGER.trace("Reregistering MBeans after reconfigure. Selector={}", selector);
164             final List<LoggerContext> contexts = selector.getLoggerContexts();
165             int i = 0;
166             for (final LoggerContext ctx : contexts) {
167                 LOGGER.trace("Reregistering context ({}/{}): '{}' {}", ++i, contexts.size(), ctx.getName(), ctx);
168                 // first unregister the context and all nested loggers,
169                 // appenders, statusLogger, contextSelector, ringbuffers...
170                 unregisterLoggerContext(ctx.getName(), mbs);
171 
172                 final LoggerContextAdmin mbean = new LoggerContextAdmin(ctx, executor);
173                 register(mbs, mbean, mbean.getObjectName());
174 
175                 if (ctx instanceof AsyncLoggerContext) {
176                     final RingBufferAdmin rbmbean = ((AsyncLoggerContext) ctx).createRingBufferAdmin();
177                     if (rbmbean.getBufferSize() > 0) {
178                     	// don't register if Disruptor not started (DefaultConfiguration: config not found)
179                     	register(mbs, rbmbean, rbmbean.getObjectName());
180                     }
181                 }
182 
183                 // register the status logger and the context selector
184                 // repeatedly
185                 // for each known context: if one context is unregistered,
186                 // these MBeans should still be available for the other
187                 // contexts.
188                 registerStatusLogger(ctx.getName(), mbs, executor);
189                 registerContextSelector(ctx.getName(), selector, mbs, executor);
190 
191                 registerLoggerConfigs(ctx, mbs, executor);
192                 registerAppenders(ctx, mbs, executor);
193             }
194         } catch (final Exception ex) {
195             LOGGER.error("Could not register mbeans", ex);
196         }
197     }
198 
199     /**
200      * Unregister all log4j MBeans from the platform MBean server.
201      */
202     public static void unregisterMBeans() {
203         final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
204         unregisterMBeans(mbs);
205     }
206 
207     /**
208      * Unregister all log4j MBeans from the specified MBean server.
209      *
210      * @param mbs the MBean server to unregister from.
211      */
212     public static void unregisterMBeans(final MBeanServer mbs) {
213         unregisterStatusLogger("*", mbs);
214         unregisterContextSelector("*", mbs);
215         unregisterContexts(mbs);
216         unregisterLoggerConfigs("*", mbs);
217         unregisterAsyncLoggerRingBufferAdmins("*", mbs);
218         unregisterAsyncLoggerConfigRingBufferAdmins("*", mbs);
219         unregisterAppenders("*", mbs);
220         unregisterAsyncAppenders("*", mbs);
221     }
222 
223     /**
224      * Returns the {@code ContextSelector} of the current {@code Log4jContextFactory}.
225      *
226      * @return the {@code ContextSelector} of the current {@code Log4jContextFactory}
227      */
228     private static ContextSelector getContextSelector() {
229         final LoggerContextFactory factory = LogManager.getFactory();
230         if (factory instanceof Log4jContextFactory) {
231             final ContextSelector selector = ((Log4jContextFactory) factory).getSelector();
232             return selector;
233         }
234         return null;
235     }
236 
237     /**
238      * Unregisters all MBeans associated with the specified logger context (including MBeans for {@code LoggerConfig}s
239      * and {@code Appender}s from the platform MBean server.
240      *
241      * @param loggerContextName name of the logger context to unregister
242      */
243     public static void unregisterLoggerContext(final String loggerContextName) {
244         final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
245         unregisterLoggerContext(loggerContextName, mbs);
246     }
247 
248     /**
249      * Unregisters all MBeans associated with the specified logger context (including MBeans for {@code LoggerConfig}s
250      * and {@code Appender}s from the platform MBean server.
251      *
252      * @param contextName name of the logger context to unregister
253      * @param mbs the MBean Server to unregister the instrumented objects from
254      */
255     public static void unregisterLoggerContext(final String contextName, final MBeanServer mbs) {
256         final String pattern = LoggerContextAdminMBean.PATTERN;
257         final String search = String.format(pattern, escape(contextName), "*");
258         unregisterAllMatching(search, mbs); // unregister context mbean
259 
260         // now unregister all MBeans associated with this logger context
261         unregisterStatusLogger(contextName, mbs);
262         unregisterContextSelector(contextName, mbs);
263         unregisterLoggerConfigs(contextName, mbs);
264         unregisterAppenders(contextName, mbs);
265         unregisterAsyncAppenders(contextName, mbs);
266         unregisterAsyncLoggerRingBufferAdmins(contextName, mbs);
267         unregisterAsyncLoggerConfigRingBufferAdmins(contextName, mbs);
268     }
269 
270     private static void registerStatusLogger(final String contextName, final MBeanServer mbs, final Executor executor)
271             throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException {
272 
273         final StatusLoggerAdmin mbean = new StatusLoggerAdmin(contextName, executor);
274         register(mbs, mbean, mbean.getObjectName());
275     }
276 
277     private static void registerContextSelector(final String contextName, final ContextSelector selector,
278             final MBeanServer mbs, final Executor executor) throws InstanceAlreadyExistsException,
279             MBeanRegistrationException, NotCompliantMBeanException {
280 
281         final ContextSelectorAdmin mbean = new ContextSelectorAdmin(contextName, selector);
282         register(mbs, mbean, mbean.getObjectName());
283     }
284 
285     private static void unregisterStatusLogger(final String contextName, final MBeanServer mbs) {
286         final String pattern = StatusLoggerAdminMBean.PATTERN;
287         final String search = String.format(pattern, escape(contextName), "*");
288         unregisterAllMatching(search, mbs);
289     }
290 
291     private static void unregisterContextSelector(final String contextName, final MBeanServer mbs) {
292         final String pattern = ContextSelectorAdminMBean.PATTERN;
293         final String search = String.format(pattern, escape(contextName), "*");
294         unregisterAllMatching(search, mbs);
295     }
296 
297     private static void unregisterLoggerConfigs(final String contextName, final MBeanServer mbs) {
298         final String pattern = LoggerConfigAdminMBean.PATTERN;
299         final String search = String.format(pattern, escape(contextName), "*");
300         unregisterAllMatching(search, mbs);
301     }
302 
303     private static void unregisterContexts(final MBeanServer mbs) {
304         final String pattern = LoggerContextAdminMBean.PATTERN;
305         final String search = String.format(pattern, "*");
306         unregisterAllMatching(search, mbs);
307     }
308 
309     private static void unregisterAppenders(final String contextName, final MBeanServer mbs) {
310         final String pattern = AppenderAdminMBean.PATTERN;
311         final String search = String.format(pattern, escape(contextName), "*");
312         unregisterAllMatching(search, mbs);
313     }
314 
315     private static void unregisterAsyncAppenders(final String contextName, final MBeanServer mbs) {
316         final String pattern = AsyncAppenderAdminMBean.PATTERN;
317         final String search = String.format(pattern, escape(contextName), "*");
318         unregisterAllMatching(search, mbs);
319     }
320 
321     private static void unregisterAsyncLoggerRingBufferAdmins(final String contextName, final MBeanServer mbs) {
322         final String pattern1 = RingBufferAdminMBean.PATTERN_ASYNC_LOGGER;
323         final String search1 = String.format(pattern1, escape(contextName));
324         unregisterAllMatching(search1, mbs);
325     }
326 
327     private static void unregisterAsyncLoggerConfigRingBufferAdmins(final String contextName, final MBeanServer mbs) {
328         final String pattern2 = RingBufferAdminMBean.PATTERN_ASYNC_LOGGER_CONFIG;
329         final String search2 = String.format(pattern2, escape(contextName), "*");
330         unregisterAllMatching(search2, mbs);
331     }
332 
333     private static void unregisterAllMatching(final String search, final MBeanServer mbs) {
334         try {
335             final ObjectName pattern = new ObjectName(search);
336             final Set<ObjectName> found = mbs.queryNames(pattern, null);
337             if (found.isEmpty()) {
338             	LOGGER.trace("Unregistering but no MBeans found matching '{}'", search);
339             } else {
340             	LOGGER.trace("Unregistering {} MBeans: {}", found.size(), found);
341             }
342             for (final ObjectName objectName : found) {
343                 mbs.unregisterMBean(objectName);
344             }
345         } catch (final Exception ex) {
346             LOGGER.error("Could not unregister MBeans for " + search, ex);
347         }
348     }
349 
350     private static void registerLoggerConfigs(final LoggerContext ctx, final MBeanServer mbs, final Executor executor)
351             throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException {
352 
353         final Map<String, LoggerConfig> map = ctx.getConfiguration().getLoggers();
354         for (final String name : map.keySet()) {
355             final LoggerConfig cfg = map.get(name);
356             final LoggerConfigAdmin mbean = new LoggerConfigAdmin(ctx, cfg);
357             register(mbs, mbean, mbean.getObjectName());
358 
359             if (cfg instanceof AsyncLoggerConfig) {
360                 final AsyncLoggerConfig async = (AsyncLoggerConfig) cfg;
361                 final RingBufferAdmin rbmbean = async.createRingBufferAdmin(ctx.getName());
362                 register(mbs, rbmbean, rbmbean.getObjectName());
363             }
364         }
365     }
366 
367     private static void registerAppenders(final LoggerContext ctx, final MBeanServer mbs, final Executor executor)
368             throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException {
369 
370         final Map<String, Appender> map = ctx.getConfiguration().getAppenders();
371         for (final String name : map.keySet()) {
372             final Appender appender = map.get(name);
373 
374             if (appender instanceof AsyncAppender) {
375                 final AsyncAppender async = ((AsyncAppender) appender);
376                 final AsyncAppenderAdmin mbean = new AsyncAppenderAdmin(ctx.getName(), async);
377                 register(mbs, mbean, mbean.getObjectName());
378             } else {
379                 final AppenderAdmin mbean = new AppenderAdmin(ctx.getName(), appender);
380                 register(mbs, mbean, mbean.getObjectName());
381             }
382         }
383     }
384 
385     private static void register(final MBeanServer mbs, final Object mbean, final ObjectName objectName)
386             throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException {
387         LOGGER.debug("Registering MBean {}", objectName);
388         mbs.registerMBean(mbean, objectName);
389     }
390 }