From 1a11ed20ba28e66640071d991181dcfe7387f89f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 6 Aug 2015 11:34:45 +0100 Subject: [PATCH] Provide a property to configure conversion word used to log exceptions This commit adds a new property, logging.exception-conversion-word, that can be used to configure the conversion word that is used when logging exceptions. The default value, %rEx, will log exceptions with the root cause first and include class packaging information in the stack trace. The new property is supported when using either Logback or Log4J2. Closes gh-3684 --- .../appendix-application-properties.adoc | 5 +- spring-boot-docs/src/main/asciidoc/howto.adoc | 4 +- .../main/asciidoc/spring-boot-features.adoc | 4 ++ .../logging/LoggingApplicationListener.java | 9 ++++ .../logback/DefaultLogbackConfiguration.java | 4 +- .../boot/logging/log4j2/log4j2-file.xml | 3 +- .../boot/logging/log4j2/log4j2.xml | 3 +- .../boot/logging/logback/defaults.xml | 4 +- .../LoggingApplicationListenerTests.java | 21 ++++++-- .../log4j2/Log4J2LoggingSystemTests.java | 46 ++++++++++++++--- .../logback/LogbackLoggingSystemTests.java | 49 ++++++++++++++++--- 11 files changed, 125 insertions(+), 27 deletions(-) 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 772f216d3d..1783d80da1 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -56,10 +56,11 @@ content into your application; rather pick only the properties that you need. spring.output.ansi.enabled=detect # Configure the ANSI output ("detect", "always", "never") # LOGGING - logging.path=/var/log - logging.file=myapp.log logging.config= # location of config file (default classpath:logback.xml for logback) + logging.exception-conversion-word=%rEx # conversion word used when logging exceptions + logging.file=myapp.log logging.level.*= # levels for loggers, e.g. "logging.level.org.springframework=DEBUG" (TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF) + logging.path=/var/log 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) diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc index e0880fa5d2..277ffe7971 100644 --- a/spring-boot-docs/src/main/asciidoc/howto.adoc +++ b/spring-boot-docs/src/main/asciidoc/howto.adoc @@ -1195,7 +1195,9 @@ These are: * `${PID}` the current process ID. * `${LOG_FILE}` if `logging.file` was set in Boot's external configuration. * `${LOG_PATH}` if `logging.path` was set (representing a directory for - log files to live in). + log files to live in). +* `${LOG_EXCEPTION_CONVERSION_WORD}` if `logging.exception-conversion-word` was set in + Boot's external configuration. Spring Boot also provides some nice ANSI colour terminal output on a console (but not in a log file) using a custom Logback converter. See the default `base.xml` configuration diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 8b3cc3da24..2faa6e9b9a 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -1068,6 +1068,10 @@ To help with the customization some other properties are transferred from the Sp |=== |Spring Environment |System Property |Comments +|`logging.exception-conversion-word` +|`LOG_EXCEPTION_CONVERSION_WORD` +|The conversion word that's used when logging exceptions. + |`logging.file` |`LOG_FILE` |Used in default log configuration if defined. 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 30f00a6488..60ec5fa87b 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 @@ -92,6 +92,11 @@ public class LoggingApplicationListener implements GenericApplicationListener { */ public static final String PID_KEY = "PID"; + /** + * The name of the System property that contains the exception conversion word. + */ + public static final String EXCEPTION_CONVERSION_WORD = "LOG_EXCEPTION_CONVERSION_WORD"; + private static MultiValueMap LOG_LEVEL_LOGGERS; static { LOG_LEVEL_LOGGERS = new LinkedMultiValueMap(); @@ -185,6 +190,10 @@ public class LoggingApplicationListener implements GenericApplicationListener { if (System.getProperty(PID_KEY) == null) { System.setProperty(PID_KEY, new ApplicationPid().toString()); } + if (System.getProperty(EXCEPTION_CONVERSION_WORD) == null) { + System.setProperty(EXCEPTION_CONVERSION_WORD, + environment.getProperty("logging.exception-conversion-word", "%rEx")); + } initializeEarlyLoggingLevel(environment); initializeSystem(environment, this.loggingSystem); initializeFinalLoggingLevels(environment, this.loggingSystem); diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java b/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java index d57a408a2f..2786d31bef 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java @@ -48,10 +48,10 @@ class DefaultLogbackConfiguration { private static final String CONSOLE_LOG_PATTERN = "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} " + "%clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} " + "%clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} " - + "%clr(:){faint} %m%n%rEx"; + + "%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%rEx}"; private static final String FILE_LOG_PATTERN = "%d{yyyy-MM-dd HH:mm:ss.SSS} %5p " - + "${PID:- } --- [%t] %-40.40logger{39} : %m%n%rEx"; + + "${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%rEx}"; private static final Charset UTF8 = Charset.forName("UTF-8"); diff --git a/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml b/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml index c455428d01..948991c554 100644 --- a/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml +++ b/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml @@ -2,7 +2,8 @@ ???? - %d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${sys:PID} --- [%t] %-40.40c{1.} : %m%n%rEx + %rEx + %d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${sys:PID} --- [%t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} diff --git a/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml b/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml index f27221050c..d0a04a3910 100644 --- a/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml +++ b/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml @@ -2,7 +2,8 @@ ???? - %clr{%d{yyyy-MM-dd HH:mm:ss.SSS}}{faint} %clr{%5p} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n%rEx + %rEx + %clr{%d{yyyy-MM-dd HH:mm:ss.SSS}}{faint} %clr{%5p} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} diff --git a/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml b/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml index f9fe0237f0..7654847b2e 100644 --- a/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml +++ b/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml @@ -9,8 +9,8 @@ initialization performed by Boot - - + + org.springframework.boot 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 005188f2fe..f223e81d6a 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 @@ -89,6 +89,7 @@ public class LoggingApplicationListenerTests { System.clearProperty("LOG_FILE"); System.clearProperty("LOG_PATH"); System.clearProperty("PID"); + System.clearProperty("LOG_EXCEPTION_CONVERSION_WORD"); if (this.context != null) { this.context.close(); } @@ -109,10 +110,10 @@ public class LoggingApplicationListenerTests { public void baseConfigLocation() { this.initializer.initialize(this.context.getEnvironment(), this.context.getClassLoader()); - this.logger.info("Hello world"); - String output = this.outputCapture.toString().trim(); - assertTrue("Wrong output:\n" + output, output.contains("Hello world")); - assertFalse("Wrong output:\n" + output, output.contains("???")); + this.outputCapture.expect(containsString("Hello world")); + this.outputCapture.expect(not(containsString("???"))); + this.outputCapture.expect(containsString("[junit-")); + this.logger.info("Hello world", new RuntimeException("Expected")); assertFalse(new File(tmpDir() + "/spring.log").exists()); } @@ -328,6 +329,18 @@ public class LoggingApplicationListenerTests { assertFalse(bridgeHandlerInstalled()); } + @Test + public void overrideExceptionConversionWord() throws Exception { + EnvironmentTestUtils.addEnvironment(this.context, + "logging.exception-conversion-word:%ex"); + this.initializer.initialize(this.context.getEnvironment(), + this.context.getClassLoader()); + this.outputCapture.expect(containsString("Hello world")); + this.outputCapture.expect(not(containsString("???"))); + this.outputCapture.expect(not(containsString("[junit-"))); + this.logger.info("Hello world", new RuntimeException("Expected")); + } + private boolean bridgeHandlerInstalled() { Logger rootLogger = LogManager.getLogManager().getLogger(""); Handler[] handlers = rootLogger.getHandlers(); diff --git a/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java b/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java index fb8fcd7f8f..b64acae05c 100644 --- a/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.logging.log4j2; import java.io.File; +import java.io.FileReader; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -25,6 +26,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.FileConfigurationMonitor; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; @@ -32,6 +35,7 @@ import org.junit.Test; import org.springframework.boot.logging.AbstractLoggingSystemTests; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.test.OutputCapture; +import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; import com.fasterxml.jackson.databind.ObjectMapper; @@ -189,21 +193,49 @@ public class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @Test public void exceptionsIncludeClassPackaging() throws Exception { this.loggingSystem.beforeInitialize(); - this.logger.info("Hidden"); - this.loggingSystem.initialize(null, null, null); - this.output.expect(containsString("[junit-")); + this.loggingSystem.initialize(null, null, getLogFile(null, tmpDir())); + Matcher expectedOutput = containsString("[junit-"); + this.output.expect(expectedOutput); this.logger.warn("Expected exception", new RuntimeException("Expected")); + String fileContents = FileCopyUtils.copyToString(new FileReader(new File(tmpDir() + + "/spring.log"))); + assertThat(fileContents, is(expectedOutput)); } @Test public void rootCauseIsLoggedFirst() throws Exception { this.loggingSystem.beforeInitialize(); - this.logger.info("Hidden"); - this.loggingSystem.initialize(null, null, null); - this.output.expect(containsString("Wrapped by: " - + "java.lang.RuntimeException: Expected")); + this.loggingSystem.initialize(null, null, getLogFile(null, tmpDir())); + Matcher expectedOutput = containsString("Wrapped by: " + + "java.lang.RuntimeException: Expected"); + this.output.expect(expectedOutput); this.logger.warn("Expected exception", new RuntimeException("Expected", new RuntimeException("Cause"))); + String fileContents = FileCopyUtils.copyToString(new FileReader(new File(tmpDir() + + "/spring.log"))); + assertThat(fileContents, is(expectedOutput)); + } + + @Test + public void customExceptionConversionWord() throws Exception { + System.setProperty("LOG_EXCEPTION_CONVERSION_WORD", "%ex"); + try { + this.loggingSystem.beforeInitialize(); + this.logger.info("Hidden"); + this.loggingSystem.initialize(null, null, getLogFile(null, tmpDir())); + Matcher expectedOutput = Matchers.allOf( + containsString("java.lang.RuntimeException: Expected"), + not(containsString("Wrapped by:"))); + this.output.expect(expectedOutput); + this.logger.warn("Expected exception", new RuntimeException("Expected", + new RuntimeException("Cause"))); + String fileContents = FileCopyUtils.copyToString(new FileReader(new File( + tmpDir() + "/spring.log"))); + assertThat(fileContents, is(expectedOutput)); + } + finally { + System.clearProperty("LOG_EXCEPTION_CONVERSION_WORD"); + } } private static class TestLog4J2LoggingSystem extends Log4J2LoggingSystem { diff --git a/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java b/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java index ff074e2c40..9fce396d2c 100644 --- a/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java @@ -23,6 +23,8 @@ import java.util.logging.LogManager; import org.apache.commons.logging.Log; import org.apache.commons.logging.impl.SLF4JLogFactory; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -44,6 +46,8 @@ import ch.qos.logback.classic.LoggerContext; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -241,21 +245,52 @@ public class LogbackLoggingSystemTests extends AbstractLoggingSystemTests { @Test public void exceptionsIncludeClassPackaging() throws Exception { this.loggingSystem.beforeInitialize(); - this.logger.info("Hidden"); - this.loggingSystem.initialize(this.initializationContext, null, null); - this.output.expect(containsString("[junit-")); + this.loggingSystem.initialize(this.initializationContext, null, + getLogFile(null, tmpDir())); + Matcher expectedOutput = containsString("[junit-"); + this.output.expect(expectedOutput); this.logger.warn("Expected exception", new RuntimeException("Expected")); + String fileContents = FileCopyUtils.copyToString(new FileReader(new File(tmpDir() + + "/spring.log"))); + assertThat(fileContents, is(expectedOutput)); } @Test public void rootCauseIsLoggedFirst() throws Exception { this.loggingSystem.beforeInitialize(); - this.logger.info("Hidden"); - this.loggingSystem.initialize(this.initializationContext, null, null); - this.output.expect(containsString("Wrapped by: " - + "java.lang.RuntimeException: Expected")); + this.loggingSystem.initialize(this.initializationContext, null, + getLogFile(null, tmpDir())); + Matcher expectedOutput = containsString("Wrapped by: " + + "java.lang.RuntimeException: Expected"); + this.output.expect(expectedOutput); this.logger.warn("Expected exception", new RuntimeException("Expected", new RuntimeException("Cause"))); + String fileContents = FileCopyUtils.copyToString(new FileReader(new File(tmpDir() + + "/spring.log"))); + assertThat(fileContents, is(expectedOutput)); + } + + @Test + public void customExceptionConversionWord() throws Exception { + System.setProperty("LOG_EXCEPTION_CONVERSION_WORD", "%ex"); + try { + this.loggingSystem.beforeInitialize(); + this.logger.info("Hidden"); + this.loggingSystem.initialize(this.initializationContext, null, + getLogFile(null, tmpDir())); + Matcher expectedOutput = Matchers.allOf( + containsString("java.lang.RuntimeException: Expected"), + not(containsString("Wrapped by:"))); + this.output.expect(expectedOutput); + this.logger.warn("Expected exception", new RuntimeException("Expected", + new RuntimeException("Cause"))); + String fileContents = FileCopyUtils.copyToString(new FileReader(new File( + tmpDir() + "/spring.log"))); + assertThat(fileContents, is(expectedOutput)); + } + finally { + System.clearProperty("LOG_EXCEPTION_CONVERSION_WORD"); + } } private String getLineWithText(File file, String outputSearch) throws Exception {