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 */
017package org.apache.logging.log4j.core.jmx;
018
019import java.lang.management.ManagementFactory;
020import java.util.List;
021import java.util.Map;
022import java.util.Set;
023import java.util.concurrent.Executor;
024import java.util.concurrent.ExecutorService;
025import java.util.concurrent.Executors;
026
027import javax.management.InstanceAlreadyExistsException;
028import javax.management.MBeanRegistrationException;
029import javax.management.MBeanServer;
030import javax.management.NotCompliantMBeanException;
031import javax.management.ObjectName;
032
033import org.apache.logging.log4j.LogManager;
034import org.apache.logging.log4j.core.Appender;
035import org.apache.logging.log4j.core.LoggerContext;
036import org.apache.logging.log4j.core.appender.AsyncAppender;
037import org.apache.logging.log4j.core.async.AsyncLogger;
038import org.apache.logging.log4j.core.async.AsyncLoggerConfig;
039import org.apache.logging.log4j.core.async.AsyncLoggerContext;
040import org.apache.logging.log4j.core.async.DaemonThreadFactory;
041import org.apache.logging.log4j.core.config.LoggerConfig;
042import org.apache.logging.log4j.core.impl.Log4jContextFactory;
043import org.apache.logging.log4j.core.selector.ContextSelector;
044import org.apache.logging.log4j.core.util.Loader;
045import org.apache.logging.log4j.spi.LoggerContextFactory;
046import org.apache.logging.log4j.status.StatusLogger;
047import org.apache.logging.log4j.util.PropertiesUtil;
048
049/**
050 * Creates MBeans to instrument various classes in the log4j class hierarchy.
051 * <p>
052 * All instrumentation for Log4j 2 classes can be disabled by setting system property {@code -Dlog4j2.disable.jmx=true}.
053 * </p>
054 */
055public final class Server {
056
057    /**
058     * The domain part, or prefix ({@value} ) of the {@code ObjectName} of all MBeans that instrument Log4J2 components.
059     */
060    public static final String DOMAIN = "org.apache.logging.log4j2";
061    private static final String PROPERTY_DISABLE_JMX = "log4j2.disable.jmx";
062    private static final String PROPERTY_ASYNC_NOTIF = "log4j2.jmx.notify.async";
063    private static final String THREAD_NAME_PREFIX = "log4j2.jmx.notif";
064    private static final StatusLogger LOGGER = StatusLogger.getLogger();
065    static final Executor executor = createExecutor();
066
067    private Server() {
068    }
069
070    /**
071     * Returns either a {@code null} Executor (causing JMX notifications to be sent from the caller thread) or a daemon
072     * background thread Executor, depending on the value of system property "log4j2.jmx.notify.async". If this
073     * property is not set, use a {@code null} Executor for web apps to avoid memory leaks and other issues when the
074     * web app is restarted.
075     * @see LOG4J2-938
076     */
077    private static ExecutorService createExecutor() {
078        boolean defaultAsync = !isWebApp();
079        boolean async = PropertiesUtil.getProperties().getBooleanProperty(PROPERTY_ASYNC_NOTIF, defaultAsync);
080        return async ? Executors.newFixedThreadPool(1, new DaemonThreadFactory(THREAD_NAME_PREFIX)) : null;
081    }
082
083    /**
084     * Returns {@code true} if we think we are running in a web container, based on the presence of the
085     * {@code javax.servlet.Servlet} class in the classpath.
086     */
087    private static boolean isWebApp() {
088        return Loader.isClassAvailable("javax.servlet.Servlet");
089    }
090
091    /**
092     * Either returns the specified name as is, or returns a quoted value containing the specified name with the special
093     * characters (comma, equals, colon, quote, asterisk, or question mark) preceded with a backslash.
094     *
095     * @param name the name to escape so it can be used as a value in an {@link ObjectName}.
096     * @return the escaped name
097     */
098    public static String escape(final String name) {
099        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}