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