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