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}