001/*
002 * Copyright 2012-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.context.logging;
018
019import java.util.Collections;
020import java.util.LinkedHashMap;
021import java.util.List;
022import java.util.Locale;
023import java.util.Map;
024import java.util.concurrent.atomic.AtomicBoolean;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028
029import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
030import org.springframework.boot.SpringApplication;
031import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
032import org.springframework.boot.context.event.ApplicationFailedEvent;
033import org.springframework.boot.context.event.ApplicationPreparedEvent;
034import org.springframework.boot.context.event.ApplicationStartingEvent;
035import org.springframework.boot.context.properties.bind.Bindable;
036import org.springframework.boot.context.properties.bind.Binder;
037import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
038import org.springframework.boot.logging.LogFile;
039import org.springframework.boot.logging.LogLevel;
040import org.springframework.boot.logging.LoggingInitializationContext;
041import org.springframework.boot.logging.LoggingSystem;
042import org.springframework.boot.logging.LoggingSystemProperties;
043import org.springframework.context.ApplicationContext;
044import org.springframework.context.ApplicationEvent;
045import org.springframework.context.ApplicationListener;
046import org.springframework.context.event.ContextClosedEvent;
047import org.springframework.context.event.GenericApplicationListener;
048import org.springframework.core.Ordered;
049import org.springframework.core.ResolvableType;
050import org.springframework.core.env.ConfigurableEnvironment;
051import org.springframework.core.env.Environment;
052import org.springframework.util.LinkedMultiValueMap;
053import org.springframework.util.MultiValueMap;
054import org.springframework.util.ObjectUtils;
055import org.springframework.util.ResourceUtils;
056import org.springframework.util.StringUtils;
057
058/**
059 * An {@link ApplicationListener} that configures the {@link LoggingSystem}. If the
060 * environment contains a {@code logging.config} property it will be used to bootstrap the
061 * logging system, otherwise a default configuration is used. Regardless, logging levels
062 * will be customized if the environment contains {@code logging.level.*} entries and
063 * logging groups can be defined with {@code logging.group}.
064 * <p>
065 * Debug and trace logging for Spring, Tomcat, Jetty and Hibernate will be enabled when
066 * the environment contains {@code debug} or {@code trace} properties that aren't set to
067 * {@code "false"} (i.e. if you start your application using
068 * {@literal java -jar myapp.jar [--debug | --trace]}). If you prefer to ignore these
069 * properties you can set {@link #setParseArgs(boolean) parseArgs} to {@code false}.
070 * <p>
071 * By default, log output is only written to the console. If a log file is required the
072 * {@code logging.path} and {@code logging.file} properties can be used.
073 * <p>
074 * Some system properties may be set as side effects, and these can be useful if the
075 * logging configuration supports placeholders (i.e. log4j or logback):
076 * <ul>
077 * <li>{@code LOG_FILE} is set to the value of path of the log file that should be written
078 * (if any).</li>
079 * <li>{@code PID} is set to the value of the current process ID if it can be determined.
080 * </li>
081 * </ul>
082 *
083 * @author Dave Syer
084 * @author Phillip Webb
085 * @author Andy Wilkinson
086 * @author Madhura Bhave
087 * @since 2.0.0
088 * @see LoggingSystem#get(ClassLoader)
089 */
090public class LoggingApplicationListener implements GenericApplicationListener {
091
092        private static final ConfigurationPropertyName LOGGING_LEVEL = ConfigurationPropertyName
093                        .of("logging.level");
094
095        private static final ConfigurationPropertyName LOGGING_GROUP = ConfigurationPropertyName
096                        .of("logging.group");
097
098        private static final Bindable<Map<String, String>> STRING_STRING_MAP = Bindable
099                        .mapOf(String.class, String.class);
100
101        private static final Bindable<Map<String, String[]>> STRING_STRINGS_MAP = Bindable
102                        .mapOf(String.class, String[].class);
103
104        /**
105         * The default order for the LoggingApplicationListener.
106         */
107        public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 20;
108
109        /**
110         * The name of the Spring property that contains a reference to the logging
111         * configuration to load.
112         */
113        public static final String CONFIG_PROPERTY = "logging.config";
114
115        /**
116         * The name of the Spring property that controls the registration of a shutdown hook
117         * to shut down the logging system when the JVM exits.
118         * @see LoggingSystem#getShutdownHandler
119         */
120        public static final String REGISTER_SHUTDOWN_HOOK_PROPERTY = "logging.register-shutdown-hook";
121
122        /**
123         * The name of the {@link LoggingSystem} bean.
124         */
125        public static final String LOGGING_SYSTEM_BEAN_NAME = "springBootLoggingSystem";
126
127        private static final Map<String, List<String>> DEFAULT_GROUP_LOGGERS;
128        static {
129                MultiValueMap<String, String> loggers = new LinkedMultiValueMap<>();
130                loggers.add("web", "org.springframework.core.codec");
131                loggers.add("web", "org.springframework.http");
132                loggers.add("web", "org.springframework.web");
133                loggers.add("web", "org.springframework.boot.actuate.endpoint.web");
134                loggers.add("web",
135                                "org.springframework.boot.web.servlet.ServletContextInitializerBeans");
136                loggers.add("sql", "org.springframework.jdbc.core");
137                loggers.add("sql", "org.hibernate.SQL");
138                DEFAULT_GROUP_LOGGERS = Collections.unmodifiableMap(loggers);
139        }
140
141        private static final Map<LogLevel, List<String>> LOG_LEVEL_LOGGERS;
142
143        static {
144                MultiValueMap<LogLevel, String> loggers = new LinkedMultiValueMap<>();
145                loggers.add(LogLevel.DEBUG, "sql");
146                loggers.add(LogLevel.DEBUG, "web");
147                loggers.add(LogLevel.DEBUG, "org.springframework.boot");
148                loggers.add(LogLevel.TRACE, "org.springframework");
149                loggers.add(LogLevel.TRACE, "org.apache.tomcat");
150                loggers.add(LogLevel.TRACE, "org.apache.catalina");
151                loggers.add(LogLevel.TRACE, "org.eclipse.jetty");
152                loggers.add(LogLevel.TRACE, "org.hibernate.tool.hbm2ddl");
153                LOG_LEVEL_LOGGERS = Collections.unmodifiableMap(loggers);
154        }
155
156        private static final Class<?>[] EVENT_TYPES = { ApplicationStartingEvent.class,
157                        ApplicationEnvironmentPreparedEvent.class, ApplicationPreparedEvent.class,
158                        ContextClosedEvent.class, ApplicationFailedEvent.class };
159
160        private static final Class<?>[] SOURCE_TYPES = { SpringApplication.class,
161                        ApplicationContext.class };
162
163        private static final AtomicBoolean shutdownHookRegistered = new AtomicBoolean(false);
164
165        private final Log logger = LogFactory.getLog(getClass());
166
167        private LoggingSystem loggingSystem;
168
169        private int order = DEFAULT_ORDER;
170
171        private boolean parseArgs = true;
172
173        private LogLevel springBootLogging = null;
174
175        @Override
176        public boolean supportsEventType(ResolvableType resolvableType) {
177                return isAssignableFrom(resolvableType.getRawClass(), EVENT_TYPES);
178        }
179
180        @Override
181        public boolean supportsSourceType(Class<?> sourceType) {
182                return isAssignableFrom(sourceType, SOURCE_TYPES);
183        }
184
185        private boolean isAssignableFrom(Class<?> type, Class<?>... supportedTypes) {
186                if (type != null) {
187                        for (Class<?> supportedType : supportedTypes) {
188                                if (supportedType.isAssignableFrom(type)) {
189                                        return true;
190                                }
191                        }
192                }
193                return false;
194        }
195
196        @Override
197        public void onApplicationEvent(ApplicationEvent event) {
198                if (event instanceof ApplicationStartingEvent) {
199                        onApplicationStartingEvent((ApplicationStartingEvent) event);
200                }
201                else if (event instanceof ApplicationEnvironmentPreparedEvent) {
202                        onApplicationEnvironmentPreparedEvent(
203                                        (ApplicationEnvironmentPreparedEvent) event);
204                }
205                else if (event instanceof ApplicationPreparedEvent) {
206                        onApplicationPreparedEvent((ApplicationPreparedEvent) event);
207                }
208                else if (event instanceof ContextClosedEvent && ((ContextClosedEvent) event)
209                                .getApplicationContext().getParent() == null) {
210                        onContextClosedEvent();
211                }
212                else if (event instanceof ApplicationFailedEvent) {
213                        onApplicationFailedEvent();
214                }
215        }
216
217        private void onApplicationStartingEvent(ApplicationStartingEvent event) {
218                this.loggingSystem = LoggingSystem
219                                .get(event.getSpringApplication().getClassLoader());
220                this.loggingSystem.beforeInitialize();
221        }
222
223        private void onApplicationEnvironmentPreparedEvent(
224                        ApplicationEnvironmentPreparedEvent event) {
225                if (this.loggingSystem == null) {
226                        this.loggingSystem = LoggingSystem
227                                        .get(event.getSpringApplication().getClassLoader());
228                }
229                initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
230        }
231
232        private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
233                ConfigurableListableBeanFactory beanFactory = event.getApplicationContext()
234                                .getBeanFactory();
235                if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
236                        beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);
237                }
238        }
239
240        private void onContextClosedEvent() {
241                if (this.loggingSystem != null) {
242                        this.loggingSystem.cleanUp();
243                }
244        }
245
246        private void onApplicationFailedEvent() {
247                if (this.loggingSystem != null) {
248                        this.loggingSystem.cleanUp();
249                }
250        }
251
252        /**
253         * Initialize the logging system according to preferences expressed through the
254         * {@link Environment} and the classpath.
255         * @param environment the environment
256         * @param classLoader the classloader
257         */
258        protected void initialize(ConfigurableEnvironment environment,
259                        ClassLoader classLoader) {
260                new LoggingSystemProperties(environment).apply();
261                LogFile logFile = LogFile.get(environment);
262                if (logFile != null) {
263                        logFile.applyToSystemProperties();
264                }
265                initializeEarlyLoggingLevel(environment);
266                initializeSystem(environment, this.loggingSystem, logFile);
267                initializeFinalLoggingLevels(environment, this.loggingSystem);
268                registerShutdownHookIfNecessary(environment, this.loggingSystem);
269        }
270
271        private void initializeEarlyLoggingLevel(ConfigurableEnvironment environment) {
272                if (this.parseArgs && this.springBootLogging == null) {
273                        if (isSet(environment, "debug")) {
274                                this.springBootLogging = LogLevel.DEBUG;
275                        }
276                        if (isSet(environment, "trace")) {
277                                this.springBootLogging = LogLevel.TRACE;
278                        }
279                }
280        }
281
282        private boolean isSet(ConfigurableEnvironment environment, String property) {
283                String value = environment.getProperty(property);
284                return (value != null && !value.equals("false"));
285        }
286
287        private void initializeSystem(ConfigurableEnvironment environment,
288                        LoggingSystem system, LogFile logFile) {
289                LoggingInitializationContext initializationContext = new LoggingInitializationContext(
290                                environment);
291                String logConfig = environment.getProperty(CONFIG_PROPERTY);
292                if (ignoreLogConfig(logConfig)) {
293                        system.initialize(initializationContext, null, logFile);
294                }
295                else {
296                        try {
297                                ResourceUtils.getURL(logConfig).openStream().close();
298                                system.initialize(initializationContext, logConfig, logFile);
299                        }
300                        catch (Exception ex) {
301                                // NOTE: We can't use the logger here to report the problem
302                                System.err.println("Logging system failed to initialize "
303                                                + "using configuration from '" + logConfig + "'");
304                                ex.printStackTrace(System.err);
305                                throw new IllegalStateException(ex);
306                        }
307                }
308        }
309
310        private boolean ignoreLogConfig(String logConfig) {
311                return !StringUtils.hasLength(logConfig) || logConfig.startsWith("-D");
312        }
313
314        private void initializeFinalLoggingLevels(ConfigurableEnvironment environment,
315                        LoggingSystem system) {
316                if (this.springBootLogging != null) {
317                        initializeLogLevel(system, this.springBootLogging);
318                }
319                setLogLevels(system, environment);
320        }
321
322        protected void initializeLogLevel(LoggingSystem system, LogLevel level) {
323                List<String> loggers = LOG_LEVEL_LOGGERS.get(level);
324                if (loggers != null) {
325                        for (String logger : loggers) {
326                                system.setLogLevel(logger, level);
327                        }
328                }
329        }
330
331        protected void setLogLevels(LoggingSystem system, Environment environment) {
332                if (!(environment instanceof ConfigurableEnvironment)) {
333                        return;
334                }
335                Binder binder = Binder.get(environment);
336                Map<String, String[]> groups = getGroups();
337                binder.bind(LOGGING_GROUP, STRING_STRINGS_MAP.withExistingValue(groups));
338                Map<String, String> levels = binder.bind(LOGGING_LEVEL, STRING_STRING_MAP)
339                                .orElseGet(Collections::emptyMap);
340                levels.forEach((name, level) -> {
341                        String[] groupedNames = groups.get(name);
342                        if (ObjectUtils.isEmpty(groupedNames)) {
343                                setLogLevel(system, name, level);
344                        }
345                        else {
346                                setLogLevel(system, groupedNames, level);
347                        }
348                });
349        }
350
351        private Map<String, String[]> getGroups() {
352                Map<String, String[]> groups = new LinkedHashMap<>();
353                DEFAULT_GROUP_LOGGERS.forEach(
354                                (name, loggers) -> groups.put(name, StringUtils.toStringArray(loggers)));
355                return groups;
356        }
357
358        private void setLogLevel(LoggingSystem system, String[] names, String level) {
359                for (String name : names) {
360                        setLogLevel(system, name, level);
361                }
362        }
363
364        private void setLogLevel(LoggingSystem system, String name, String level) {
365                try {
366                        name = name.equalsIgnoreCase(LoggingSystem.ROOT_LOGGER_NAME) ? null : name;
367                        system.setLogLevel(name, coerceLogLevel(level));
368                }
369                catch (RuntimeException ex) {
370                        this.logger.error("Cannot set level '" + level + "' for '" + name + "'");
371                }
372        }
373
374        private LogLevel coerceLogLevel(String level) {
375                String trimmedLevel = level.trim();
376                if ("false".equalsIgnoreCase(trimmedLevel)) {
377                        return LogLevel.OFF;
378                }
379                return LogLevel.valueOf(trimmedLevel.toUpperCase(Locale.ENGLISH));
380        }
381
382        private void registerShutdownHookIfNecessary(Environment environment,
383                        LoggingSystem loggingSystem) {
384                boolean registerShutdownHook = environment
385                                .getProperty(REGISTER_SHUTDOWN_HOOK_PROPERTY, Boolean.class, false);
386                if (registerShutdownHook) {
387                        Runnable shutdownHandler = loggingSystem.getShutdownHandler();
388                        if (shutdownHandler != null
389                                        && shutdownHookRegistered.compareAndSet(false, true)) {
390                                registerShutdownHook(new Thread(shutdownHandler));
391                        }
392                }
393        }
394
395        void registerShutdownHook(Thread shutdownHook) {
396                Runtime.getRuntime().addShutdownHook(shutdownHook);
397        }
398
399        public void setOrder(int order) {
400                this.order = order;
401        }
402
403        @Override
404        public int getOrder() {
405                return this.order;
406        }
407
408        /**
409         * Sets a custom logging level to be used for Spring Boot and related libraries.
410         * @param springBootLogging the logging level
411         */
412        public void setSpringBootLogging(LogLevel springBootLogging) {
413                this.springBootLogging = springBootLogging;
414        }
415
416        /**
417         * Sets if initialization arguments should be parsed for {@literal debug} and
418         * {@literal trace} properties (usually defined from {@literal --debug} or
419         * {@literal --trace} command line args). Defaults to {@code true}.
420         * @param parseArgs if arguments should be parsed
421         */
422        public void setParseArgs(boolean parseArgs) {
423                this.parseArgs = parseArgs;
424        }
425
426}