    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     */
    017    package org.apache.logging.log4j.core.jmx;
    018
    019    import java.lang.management.ManagementFactory;
    020    import java.util.List;
    021    import java.util.Map;
    022    import java.util.Set;
    023    import java.util.concurrent.Executor;
    024    import java.util.concurrent.ExecutorService;
    025    import java.util.concurrent.Executors;
    026
    027    import javax.management.InstanceAlreadyExistsException;
    028    import javax.management.MBeanRegistrationException;
    029    import javax.management.MBeanServer;
    030    import javax.management.NotCompliantMBeanException;
    031    import javax.management.ObjectName;
    032
    033    import org.apache.logging.log4j.LogManager;
    034    import org.apache.logging.log4j.core.Appender;
    035    import org.apache.logging.log4j.core.LoggerContext;
    036    import org.apache.logging.log4j.core.appender.AsyncAppender;
    037    import org.apache.logging.log4j.core.async.AsyncLogger;
    038    import org.apache.logging.log4j.core.async.AsyncLoggerConfig;
    039    import org.apache.logging.log4j.core.async.AsyncLoggerContext;
    040    import org.apache.logging.log4j.core.async.DaemonThreadFactory;
    041    import org.apache.logging.log4j.core.config.LoggerConfig;
    042    import org.apache.logging.log4j.core.impl.Log4jContextFactory;
    043    import org.apache.logging.log4j.core.selector.ContextSelector;
    044    import org.apache.logging.log4j.core.util.Loader;
    045    import org.apache.logging.log4j.spi.LoggerContextFactory;
    046    import org.apache.logging.log4j.status.StatusLogger;
    047    import 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     */
    055    public 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    }