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    }