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