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.InstanceNotFoundException;
029import javax.management.MBeanRegistrationException;
030import javax.management.MBeanServer;
031import javax.management.NotCompliantMBeanException;
032import javax.management.ObjectName;
033
034import org.apache.logging.log4j.LogManager;
035import org.apache.logging.log4j.core.Appender;
036import org.apache.logging.log4j.core.LoggerContext;
037import org.apache.logging.log4j.core.appender.AsyncAppender;
038import org.apache.logging.log4j.core.async.AsyncLoggerConfig;
039import org.apache.logging.log4j.core.async.AsyncLoggerContext;
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.Constants;
044import org.apache.logging.log4j.core.util.Log4jThreadFactory;
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    private static final String CONTEXT_NAME_ALL = "*";
058        /**
059     * The domain part, or prefix ({@value}) of the {@code ObjectName} of all MBeans that instrument Log4J2 components.
060     */
061    public static final String DOMAIN = "org.apache.logging.log4j2";
062    private static final String PROPERTY_DISABLE_JMX = "log4j2.disable.jmx";
063    private static final String PROPERTY_ASYNC_NOTIF = "log4j2.jmx.notify.async";
064    private static final String THREAD_NAME_PREFIX = "jmx.notif";
065    private static final StatusLogger LOGGER = StatusLogger.getLogger();
066    static final Executor executor = isJmxDisabled() ? null : createExecutor();
067
068    private Server() {
069    }
070
071    /**
072     * Returns either a {@code null} Executor (causing JMX notifications to be sent from the caller thread) or a daemon
073     * background thread Executor, depending on the value of system property "log4j2.jmx.notify.async". If this
074     * property is not set, use a {@code null} Executor for web apps to avoid memory leaks and other issues when the
075     * web app is restarted.
076     * @see <a href="https://issues.apache.org/jira/browse/LOG4J2-938">LOG4J2-938</a>
077     */
078    private static ExecutorService createExecutor() {
079        final boolean defaultAsync = !Constants.IS_WEB_APP;
080        final boolean async = PropertiesUtil.getProperties().getBooleanProperty(PROPERTY_ASYNC_NOTIF, defaultAsync);
081        return async ? Executors.newFixedThreadPool(1, Log4jThreadFactory.createDaemonThreadFactory(THREAD_NAME_PREFIX))
082                : null;
083    }
084
085    /**
086     * Either returns the specified name as is, or returns a quoted value containing the specified name with the special
087     * characters (comma, equals, colon, quote, asterisk, or question mark) preceded with a backslash.
088     *
089     * @param name the name to escape so it can be used as a value in an {@link ObjectName}.
090     * @return the escaped name
091     */
092    public static String escape(final String name) {
093        final StringBuilder sb = new StringBuilder(name.length() * 2);
094        boolean needsQuotes = false;
095        for (int i = 0; i < name.length(); i++) {
096            final char c = name.charAt(i);
097            switch (c) {
098            case '\\':
099            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}