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.AsyncLogger;
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.async.DaemonThreadFactory;
41  import org.apache.logging.log4j.core.config.LoggerConfig;
42  import org.apache.logging.log4j.core.impl.Log4jContextFactory;
43  import org.apache.logging.log4j.core.selector.ContextSelector;
44  import org.apache.logging.log4j.core.util.Loader;
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 = "log4j2.jmx.notif";
64      private static final StatusLogger LOGGER = StatusLogger.getLogger();
65      static final Executor executor = 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 LOG4J2-938
76       */
77      private static ExecutorService createExecutor() {
78          boolean defaultAsync = !isWebApp();
79          boolean async = PropertiesUtil.getProperties().getBooleanProperty(PROPERTY_ASYNC_NOTIF, defaultAsync);
80          return async ? Executors.newFixedThreadPool(1, new DaemonThreadFactory(THREAD_NAME_PREFIX)) : null;
81      }
82  
83      /**
84       * Returns {@code true} if we think we are running in a web container, based on the presence of the
85       * {@code javax.servlet.Servlet} class in the classpath.
86       */
87      private static boolean isWebApp() {
88          return Loader.isClassAvailable("javax.servlet.Servlet");
89      }
90  
91      /**
92       * Either returns the specified name as is, or returns a quoted value containing the specified name with the special
93       * characters (comma, equals, colon, quote, asterisk, or question mark) preceded with a backslash.
94       *
95       * @param name the name to escape so it can be used as a value in an {@link ObjectName}.
96       * @return the escaped name
97       */
98      public static String escape(final String name) {
99          final StringBuilder sb = new StringBuilder(name.length() * 2);
100         boolean needsQuotes = false;
101         for (int i = 0; i < name.length(); i++) {
102             final char c = name.charAt(i);
103             switch (c) {
104             case '\\':
105             case '*':
106             case '?':
107             case '\"':
108                 // quote, star, question & backslash must be escaped
109                 sb.append('\\');
110                 needsQuotes = true; // ... and can only appear in quoted value
111                 break;
112             case ',':
113             case '=':
114             case ':':
115                 // no need to escape these, but value must be quoted
116                 needsQuotes = true;
117                 break;
118             case '\r':
119                 // drop \r characters: \\r gives "invalid escape sequence"
120                 continue;
121             case '\n':
122                 // replace \n characters with \\n sequence
123                 sb.append("\\n");
124                 needsQuotes = true;
125                 continue;
126             }
127             sb.append(c);
128         }
129         if (needsQuotes) {
130             sb.insert(0, '\"');
131             sb.append('\"');
132         }
133         return sb.toString();
134     }
135 
136     public static void reregisterMBeansAfterReconfigure() {
137         // avoid creating Platform MBean Server if JMX disabled
138         if (PropertiesUtil.getProperties().getBooleanProperty(PROPERTY_DISABLE_JMX)) {
139             LOGGER.debug("JMX disabled for log4j2. Not registering MBeans.");
140             return;
141         }
142         final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
143         reregisterMBeansAfterReconfigure(mbs);
144     }
145 
146     public static void reregisterMBeansAfterReconfigure(final MBeanServer mbs) {
147         if (PropertiesUtil.getProperties().getBooleanProperty(PROPERTY_DISABLE_JMX)) {
148             LOGGER.debug("JMX disabled for log4j2. Not registering MBeans.");
149             return;
150         }
151 
152         // now provide instrumentation for the newly configured
153         // LoggerConfigs and Appenders
154         try {
155             final ContextSelector selector = getContextSelector();
156             if (selector == null) {
157                 LOGGER.debug("Could not register MBeans: no ContextSelector found.");
158                 return;
159             }
160             final List<LoggerContext> contexts = selector.getLoggerContexts();
161             for (final LoggerContext ctx : contexts) {
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 = AsyncLogger.createRingBufferAdmin(ctx.getName());
171                     register(mbs, rbmbean, rbmbean.getObjectName());
172                 }
173 
174                 // register the status logger and the context selector
175                 // repeatedly
176                 // for each known context: if one context is unregistered,
177                 // these MBeans should still be available for the other
178                 // contexts.
179                 registerStatusLogger(ctx.getName(), mbs, executor);
180                 registerContextSelector(ctx.getName(), selector, mbs, executor);
181 
182                 registerLoggerConfigs(ctx, mbs, executor);
183                 registerAppenders(ctx, mbs, executor);
184             }
185         } catch (final Exception ex) {
186             LOGGER.error("Could not register mbeans", ex);
187         }
188     }
189 
190     /**
191      * Unregister all log4j MBeans from the platform MBean server.
192      */
193     public static void unregisterMBeans() {
194         final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
195         unregisterMBeans(mbs);
196     }
197 
198     /**
199      * Unregister all log4j MBeans from the specified MBean server.
200      *
201      * @param mbs the MBean server to unregister from.
202      */
203     public static void unregisterMBeans(final MBeanServer mbs) {
204         unregisterStatusLogger("*", mbs);
205         unregisterContextSelector("*", mbs);
206         unregisterContexts(mbs);
207         unregisterLoggerConfigs("*", mbs);
208         unregisterAsyncLoggerRingBufferAdmins("*", mbs);
209         unregisterAsyncLoggerConfigRingBufferAdmins("*", mbs);
210         unregisterAppenders("*", mbs);
211         unregisterAsyncAppenders("*", mbs);
212     }
213 
214     /**
215      * Returns the {@code ContextSelector} of the current {@code Log4jContextFactory}.
216      *
217      * @return the {@code ContextSelector} of the current {@code Log4jContextFactory}
218      */
219     private static ContextSelector getContextSelector() {
220         final LoggerContextFactory factory = LogManager.getFactory();
221         if (factory instanceof Log4jContextFactory) {
222             final ContextSelector selector = ((Log4jContextFactory) factory).getSelector();
223             return selector;
224         }
225         return null;
226     }
227 
228     /**
229      * Unregisters all MBeans associated with the specified logger context (including MBeans for {@code LoggerConfig}s
230      * and {@code Appender}s from the platform MBean server.
231      *
232      * @param loggerContextName name of the logger context to unregister
233      */
234     public static void unregisterLoggerContext(final String loggerContextName) {
235         final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
236         unregisterLoggerContext(loggerContextName, mbs);
237     }
238 
239     /**
240      * Unregisters all MBeans associated with the specified logger context (including MBeans for {@code LoggerConfig}s
241      * and {@code Appender}s from the platform MBean server.
242      *
243      * @param contextName name of the logger context to unregister
244      * @param mbs the MBean Server to unregister the instrumented objects from
245      */
246     public static void unregisterLoggerContext(final String contextName, final MBeanServer mbs) {
247         final String pattern = LoggerContextAdminMBean.PATTERN;
248         final String search = String.format(pattern, escape(contextName), "*");
249         unregisterAllMatching(search, mbs); // unregister context mbean
250 
251         // now unregister all MBeans associated with this logger context
252         unregisterStatusLogger(contextName, mbs);
253         unregisterContextSelector(contextName, mbs);
254         unregisterLoggerConfigs(contextName, mbs);
255         unregisterAppenders(contextName, mbs);
256         unregisterAsyncAppenders(contextName, mbs);
257         unregisterAsyncLoggerRingBufferAdmins(contextName, mbs);
258         unregisterAsyncLoggerConfigRingBufferAdmins(contextName, mbs);
259     }
260 
261     private static void registerStatusLogger(final String contextName, final MBeanServer mbs, final Executor executor)
262             throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException {
263 
264         final StatusLoggerAdmin mbean = new StatusLoggerAdmin(contextName, executor);
265         register(mbs, mbean, mbean.getObjectName());
266     }
267 
268     private static void registerContextSelector(final String contextName, final ContextSelector selector,
269             final MBeanServer mbs, final Executor executor) throws InstanceAlreadyExistsException,
270             MBeanRegistrationException, NotCompliantMBeanException {
271 
272         final ContextSelectorAdmin mbean = new ContextSelectorAdmin(contextName, selector);
273         register(mbs, mbean, mbean.getObjectName());
274     }
275 
276     private static void unregisterStatusLogger(final String contextName, final MBeanServer mbs) {
277         final String pattern = StatusLoggerAdminMBean.PATTERN;
278         final String search = String.format(pattern, escape(contextName), "*");
279         unregisterAllMatching(search, mbs);
280     }
281 
282     private static void unregisterContextSelector(final String contextName, final MBeanServer mbs) {
283         final String pattern = ContextSelectorAdminMBean.PATTERN;
284         final String search = String.format(pattern, escape(contextName), "*");
285         unregisterAllMatching(search, mbs);
286     }
287 
288     private static void unregisterLoggerConfigs(final String contextName, final MBeanServer mbs) {
289         final String pattern = LoggerConfigAdminMBean.PATTERN;
290         final String search = String.format(pattern, escape(contextName), "*");
291         unregisterAllMatching(search, mbs);
292     }
293 
294     private static void unregisterContexts(final MBeanServer mbs) {
295         final String pattern = LoggerContextAdminMBean.PATTERN;
296         final String search = String.format(pattern, "*");
297         unregisterAllMatching(search, mbs);
298     }
299 
300     private static void unregisterAppenders(final String contextName, final MBeanServer mbs) {
301         final String pattern = AppenderAdminMBean.PATTERN;
302         final String search = String.format(pattern, escape(contextName), "*");
303         unregisterAllMatching(search, mbs);
304     }
305 
306     private static void unregisterAsyncAppenders(final String contextName, final MBeanServer mbs) {
307         final String pattern = AsyncAppenderAdminMBean.PATTERN;
308         final String search = String.format(pattern, escape(contextName), "*");
309         unregisterAllMatching(search, mbs);
310     }
311 
312     private static void unregisterAsyncLoggerRingBufferAdmins(final String contextName, final MBeanServer mbs) {
313         final String pattern1 = RingBufferAdminMBean.PATTERN_ASYNC_LOGGER;
314         final String search1 = String.format(pattern1, escape(contextName));
315         unregisterAllMatching(search1, mbs);
316     }
317 
318     private static void unregisterAsyncLoggerConfigRingBufferAdmins(final String contextName, final MBeanServer mbs) {
319         final String pattern2 = RingBufferAdminMBean.PATTERN_ASYNC_LOGGER_CONFIG;
320         final String search2 = String.format(pattern2, escape(contextName), "*");
321         unregisterAllMatching(search2, mbs);
322     }
323 
324     private static void unregisterAllMatching(final String search, final MBeanServer mbs) {
325         try {
326             final ObjectName pattern = new ObjectName(search);
327             final Set<ObjectName> found = mbs.queryNames(pattern, null);
328             for (final ObjectName objectName : found) {
329                 LOGGER.debug("Unregistering MBean {}", objectName);
330                 mbs.unregisterMBean(objectName);
331             }
332         } catch (final Exception ex) {
333             LOGGER.error("Could not unregister MBeans for " + search, ex);
334         }
335     }
336 
337     private static void registerLoggerConfigs(final LoggerContext ctx, final MBeanServer mbs, final Executor executor)
338             throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException {
339 
340         final Map<String, LoggerConfig> map = ctx.getConfiguration().getLoggers();
341         for (final String name : map.keySet()) {
342             final LoggerConfig cfg = map.get(name);
343             final LoggerConfigAdmin mbean = new LoggerConfigAdmin(ctx, cfg);
344             register(mbs, mbean, mbean.getObjectName());
345 
346             if (cfg instanceof AsyncLoggerConfig) {
347                 final AsyncLoggerConfig async = (AsyncLoggerConfig) cfg;
348                 final RingBufferAdmin rbmbean = async.createRingBufferAdmin(ctx.getName());
349                 register(mbs, rbmbean, rbmbean.getObjectName());
350             }
351         }
352     }
353 
354     private static void registerAppenders(final LoggerContext ctx, final MBeanServer mbs, final Executor executor)
355             throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException {
356 
357         final Map<String, Appender> map = ctx.getConfiguration().getAppenders();
358         for (final String name : map.keySet()) {
359             final Appender appender = map.get(name);
360 
361             if (appender instanceof AsyncAppender) {
362                 final AsyncAppender async = ((AsyncAppender) appender);
363                 final AsyncAppenderAdmin mbean = new AsyncAppenderAdmin(ctx.getName(), async);
364                 register(mbs, mbean, mbean.getObjectName());
365             } else {
366                 final AppenderAdmin mbean = new AppenderAdmin(ctx.getName(), appender);
367                 register(mbs, mbean, mbean.getObjectName());
368             }
369         }
370     }
371 
372     private static void register(final MBeanServer mbs, final Object mbean, final ObjectName objectName)
373             throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException {
374         LOGGER.debug("Registering MBean {}", objectName);
375         mbs.registerMBean(mbean, objectName);
376     }
377 }