From 36d4246289689cce081e3527930b9f4e40fb6d50 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Oct 2015 15:25:06 +0100 Subject: [PATCH] Add opt-in support for registering a shutdown hook to shut down logging This commit adds a new property, logging.register-shutdown-hook, that when set to true, will cause LoggingApplicationListener to register a shutdown hook the first time it initializes a logging system. When the JVM exits, the shutdown hook shuts down each of the supported logging systems, ensuring that all of their appenders have been flushed and closed. Closes gh-4026 --- .../appendix-application-properties.adoc | 1 + .../logging/LoggingApplicationListener.java | 24 +++++++ .../boot/logging/LoggingSystem.java | 11 ++++ .../boot/logging/java/JavaLoggingSystem.java | 15 +++++ .../logging/log4j/Log4JLoggingSystem.java | 14 ++++ .../logging/log4j2/Log4J2LoggingSystem.java | 13 ++++ .../logging/logback/LogbackLoggingSystem.java | 14 ++++ ...itional-spring-configuration-metadata.json | 6 ++ .../LoggingApplicationListenerTests.java | 65 ++++++++++++++++++- 9 files changed, 162 insertions(+), 1 deletion(-) diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index b7af65f8a4c..eb5a9fe0368 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -65,6 +65,7 @@ content into your application; rather pick only the properties that you need. logging.pattern.console= # appender pattern for output to the console (only supported with the default logback setup) logging.pattern.file= # appender pattern for output to the file (only supported with the default logback setup) logging.pattern.level= # appender pattern for the log level (default %5p, only supported with the default logback setup) + logging.register-shutdown-hook=false # register a shutdown hook for the logging system when it is initialized # IDENTITY ({sc-spring-boot}/context/ContextIdApplicationContextInitializer.{sc-ext}[ContextIdApplicationContextInitializer]) spring.application.name= diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/LoggingApplicationListener.java b/spring-boot/src/main/java/org/springframework/boot/logging/LoggingApplicationListener.java index 58ff58c489e..f3479883448 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/LoggingApplicationListener.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/LoggingApplicationListener.java @@ -19,6 +19,7 @@ package org.springframework.boot.logging; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -76,6 +77,13 @@ public class LoggingApplicationListener implements GenericApplicationListener { */ public static final String CONFIG_PROPERTY = "logging.config"; + /** + * The name of the Spring property that controls the registration of a shutdown hook + * to shut down the logging system when the JVM exits. + * @see LoggingSystem#getShutdownHandler + */ + public static final String REGISTER_SHOW_HOOK_PROPERTY = "logging.register-shutdown-hook"; + /** * The name of the Spring property that contains the path where the logging * configuration can be found. @@ -100,6 +108,8 @@ public class LoggingApplicationListener implements GenericApplicationListener { private static MultiValueMap LOG_LEVEL_LOGGERS; + private static AtomicBoolean shutdownHookRegistered = new AtomicBoolean(false); + static { LOG_LEVEL_LOGGERS = new LinkedMultiValueMap(); LOG_LEVEL_LOGGERS.add(LogLevel.DEBUG, "org.springframework.boot"); @@ -201,6 +211,7 @@ public class LoggingApplicationListener implements GenericApplicationListener { initializeEarlyLoggingLevel(environment); initializeSystem(environment, this.loggingSystem); initializeFinalLoggingLevels(environment, this.loggingSystem); + registerShutdownHookIfNecessary(environment, this.loggingSystem); } private String getExceptionConversionWord(ConfigurableEnvironment environment) { @@ -299,6 +310,19 @@ public class LoggingApplicationListener implements GenericApplicationListener { return LogLevel.valueOf(level.toUpperCase()); } + private void registerShutdownHookIfNecessary(Environment environment, + LoggingSystem loggingSystem) { + boolean registerShutdownHook = new RelaxedPropertyResolver(environment) + .getProperty(REGISTER_SHOW_HOOK_PROPERTY, Boolean.class, false); + if (registerShutdownHook) { + Runnable shutdownHandler = loggingSystem.getShutdownHandler(); + if (shutdownHandler != null + && shutdownHookRegistered.compareAndSet(false, true)) { + Runtime.getRuntime().addShutdownHook(new Thread()); + } + } + } + public void setOrder(int order) { this.order = order; } diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystem.java b/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystem.java index f16a2a25226..03e331c5dfe 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystem.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystem.java @@ -92,6 +92,17 @@ public abstract class LoggingSystem { public void cleanUp() { } + /** + * Returns a {@link Runnable} that can handle shutdown of this logging system when the + * JVM exits. The default implementation returns {@code null}, indicating that no + * shutdown is required. + * + * @return the shutdown handler, or {@code null} + */ + public Runnable getShutdownHandler() { + return null; + } + /** * Sets the logging level for a given logger. * @param loggerName the name of the logger to set diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/java/JavaLoggingSystem.java b/spring-boot/src/main/java/org/springframework/boot/logging/java/JavaLoggingSystem.java index d48ed3752c3..9726f5c8bd5 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/java/JavaLoggingSystem.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/java/JavaLoggingSystem.java @@ -40,6 +40,7 @@ import org.springframework.util.StringUtils; * * @author Phillip Webb * @author Dave Syer + * @author Andy Wilkinson */ public class JavaLoggingSystem extends AbstractLoggingSystem { @@ -114,4 +115,18 @@ public class JavaLoggingSystem extends AbstractLoggingSystem { logger.setLevel(LEVELS.get(level)); } + @Override + public Runnable getShutdownHandler() { + return new ShutdownHandler(); + } + + private final class ShutdownHandler implements Runnable { + + @Override + public void run() { + LogManager.getLogManager().reset(); + } + + } + } diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/log4j/Log4JLoggingSystem.java b/spring-boot/src/main/java/org/springframework/boot/logging/log4j/Log4JLoggingSystem.java index ad1c3ec2508..727aa70e67c 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/log4j/Log4JLoggingSystem.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/log4j/Log4JLoggingSystem.java @@ -116,4 +116,18 @@ public class Log4JLoggingSystem extends Slf4JLoggingSystem { logger.setLevel(LEVELS.get(level)); } + @Override + public Runnable getShutdownHandler() { + return new ShutdownHandler(); + } + + private static final class ShutdownHandler implements Runnable { + + @Override + public void run() { + LogManager.shutdown(); + } + + } + } diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java b/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java index 5e8fd0a48c1..26ed915265b 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java @@ -197,6 +197,11 @@ public class Log4J2LoggingSystem extends Slf4JLoggingSystem { getLoggerContext().updateLoggers(); } + @Override + public Runnable getShutdownHandler() { + return new ShutdownHandler(); + } + private LoggerConfig getRootLoggerConfig() { return getLoggerContext().getConfiguration().getLoggerConfig(""); } @@ -209,4 +214,12 @@ public class Log4J2LoggingSystem extends Slf4JLoggingSystem { return (LoggerContext) LogManager.getContext(false); } + private final class ShutdownHandler implements Runnable { + + @Override + public void run() { + getLoggerContext().stop(); + } + + } } diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java b/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java index 8d81ab6a0ce..40501d35a8e 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java @@ -199,6 +199,11 @@ public class LogbackLoggingSystem extends Slf4JLoggingSystem { getLogger(loggerName).setLevel(LEVELS.get(level)); } + @Override + public Runnable getShutdownHandler() { + return new ShutdownHandler(); + } + private ch.qos.logback.classic.Logger getLogger(String name) { LoggerContext factory = getLoggerContext(); return factory @@ -233,4 +238,13 @@ public class LogbackLoggingSystem extends Slf4JLoggingSystem { return "unknown location"; } + private final class ShutdownHandler implements Runnable { + + @Override + public void run() { + getLoggerContext().stop(); + } + + } + } diff --git a/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 0fe597b18ee..519c5cd827f 100644 --- a/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -72,6 +72,12 @@ "description": "Location of the log file.", "sourceType": "org.springframework.boot.logging.LoggingApplicationListener" }, + { + "name": "logging.register-shutdown-hook", + "type": "java.lang.Boolean", + "description": "Register a shutdown hook for the logging system when it is initialized.", + "sourceType": "org.springframework.boot.logging.LoggingApplicationListener" + }, { "name": "spring.mandatory-file-encoding", "sourceType": "org.springframework.boot.context.FileEncodingApplicationListener", diff --git a/spring-boot/src/test/java/org/springframework/boot/logging/LoggingApplicationListenerTests.java b/spring-boot/src/test/java/org/springframework/boot/logging/LoggingApplicationListenerTests.java index f9a9a29f8a1..d681efb22ba 100644 --- a/spring-boot/src/test/java/org/springframework/boot/logging/LoggingApplicationListenerTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/logging/LoggingApplicationListenerTests.java @@ -41,6 +41,7 @@ import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.support.GenericApplicationContext; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; @@ -86,6 +87,8 @@ public class LoggingApplicationListenerTests { @After public void clear() { + LoggingSystem.get(getClass().getClassLoader()).cleanUp(); + System.clearProperty(LoggingSystem.class.getName()); System.clearProperty("LOG_FILE"); System.clearProperty("LOG_PATH"); System.clearProperty("PID"); @@ -93,7 +96,6 @@ public class LoggingApplicationListenerTests { if (this.context != null) { this.context.close(); } - LoggingSystem.get(getClass().getClassLoader()).cleanUp(); } private String tmpDir() { @@ -341,6 +343,30 @@ public class LoggingApplicationListenerTests { this.logger.info("Hello world", new RuntimeException("Expected")); } + @Test + public void shutdownHookIsNotRegisteredByDefault() throws Exception { + System.setProperty(LoggingSystem.class.getName(), + NullShutdownHandlerLoggingSystem.class.getName()); + this.initializer.onApplicationEvent( + new ApplicationStartedEvent(new SpringApplication(), NO_ARGS)); + this.initializer.initialize(this.context.getEnvironment(), + this.context.getClassLoader()); + assertThat(NullShutdownHandlerLoggingSystem.shutdownHandlerRequested, is(false)); + } + + @Test + public void shutdownHookCanBeRegistered() throws Exception { + System.setProperty(LoggingSystem.class.getName(), + NullShutdownHandlerLoggingSystem.class.getName()); + EnvironmentTestUtils.addEnvironment(this.context, + "logging.register_shutdown_hook:true"); + this.initializer.onApplicationEvent( + new ApplicationStartedEvent(new SpringApplication(), NO_ARGS)); + this.initializer.initialize(this.context.getEnvironment(), + this.context.getClassLoader()); + assertThat(NullShutdownHandlerLoggingSystem.shutdownHandlerRequested, is(true)); + } + private boolean bridgeHandlerInstalled() { Logger rootLogger = LogManager.getLogManager().getLogger(""); Handler[] handlers = rootLogger.getHandlers(); @@ -351,4 +377,41 @@ public class LoggingApplicationListenerTests { } return false; } + + public static class NullShutdownHandlerLoggingSystem extends AbstractLoggingSystem { + + static boolean shutdownHandlerRequested = false; + + public NullShutdownHandlerLoggingSystem(ClassLoader classLoader) { + super(classLoader); + } + + @Override + protected String[] getStandardConfigLocations() { + return new String[] { "foo.bar" }; + } + + @Override + protected void loadDefaults(LoggingInitializationContext initializationContext, + LogFile logFile) { + } + + @Override + protected void loadConfiguration( + LoggingInitializationContext initializationContext, String location, + LogFile logFile) { + } + + @Override + public void setLogLevel(String loggerName, LogLevel level) { + + } + + @Override + public Runnable getShutdownHandler() { + shutdownHandlerRequested = true; + return null; + } + + } }