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