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
pull/3652/merge
Andy Wilkinson 9 years ago
parent 67d29c13f5
commit 1a11ed20ba

@ -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") spring.output.ansi.enabled=detect # Configure the ANSI output ("detect", "always", "never")
# LOGGING # LOGGING
logging.path=/var/log
logging.file=myapp.log
logging.config= # location of config file (default classpath:logback.xml for logback) 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.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.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.file= # appender pattern for output to the file (only supported with the default logback setup)

@ -1195,7 +1195,9 @@ These are:
* `${PID}` the current process ID. * `${PID}` the current process ID.
* `${LOG_FILE}` if `logging.file` was set in Boot's external configuration. * `${LOG_FILE}` if `logging.file` was set in Boot's external configuration.
* `${LOG_PATH}` if `logging.path` was set (representing a directory for * `${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 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 a log file) using a custom Logback converter. See the default `base.xml` configuration

@ -1068,6 +1068,10 @@ To help with the customization some other properties are transferred from the Sp
|=== |===
|Spring Environment |System Property |Comments |Spring Environment |System Property |Comments
|`logging.exception-conversion-word`
|`LOG_EXCEPTION_CONVERSION_WORD`
|The conversion word that's used when logging exceptions.
|`logging.file` |`logging.file`
|`LOG_FILE` |`LOG_FILE`
|Used in default log configuration if defined. |Used in default log configuration if defined.

@ -92,6 +92,11 @@ public class LoggingApplicationListener implements GenericApplicationListener {
*/ */
public static final String PID_KEY = "PID"; 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<LogLevel, String> LOG_LEVEL_LOGGERS; private static MultiValueMap<LogLevel, String> LOG_LEVEL_LOGGERS;
static { static {
LOG_LEVEL_LOGGERS = new LinkedMultiValueMap<LogLevel, String>(); LOG_LEVEL_LOGGERS = new LinkedMultiValueMap<LogLevel, String>();
@ -185,6 +190,10 @@ public class LoggingApplicationListener implements GenericApplicationListener {
if (System.getProperty(PID_KEY) == null) { if (System.getProperty(PID_KEY) == null) {
System.setProperty(PID_KEY, new ApplicationPid().toString()); 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); initializeEarlyLoggingLevel(environment);
initializeSystem(environment, this.loggingSystem); initializeSystem(environment, this.loggingSystem);
initializeFinalLoggingLevels(environment, this.loggingSystem); initializeFinalLoggingLevels(environment, this.loggingSystem);

@ -48,10 +48,10 @@ class DefaultLogbackConfiguration {
private static final String CONSOLE_LOG_PATTERN = "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} " 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(%5p) %clr(${PID:- }){magenta} %clr(---){faint} "
+ "%clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} " + "%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 " 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"); private static final Charset UTF8 = Charset.forName("UTF-8");

@ -2,7 +2,8 @@
<Configuration status="WARN"> <Configuration status="WARN">
<Properties> <Properties>
<Property name="PID">????</Property> <Property name="PID">????</Property>
<Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${sys:PID} --- [%t] %-40.40c{1.} : %m%n%rEx</Property> <Property name="LOG_EXCEPTION_CONVERSION_WORD">%rEx</Property>
<Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${sys:PID} --- [%t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
</Properties> </Properties>
<Appenders> <Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true"> <Console name="Console" target="SYSTEM_OUT" follow="true">

@ -2,7 +2,8 @@
<Configuration status="WARN"> <Configuration status="WARN">
<Properties> <Properties>
<Property name="PID">????</Property> <Property name="PID">????</Property>
<Property name="LOG_PATTERN">%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</Property> <Property name="LOG_EXCEPTION_CONVERSION_WORD">%rEx</Property>
<Property name="LOG_PATTERN">%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}</Property>
</Properties> </Properties>
<Appenders> <Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true"> <Console name="Console" target="SYSTEM_OUT" follow="true">

@ -9,8 +9,8 @@ initialization performed by Boot
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" /> <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
<conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" /> <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
<property name="CONSOLE_LOG_PATTERN" value="%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"/> <property name="CONSOLE_LOG_PATTERN" value="%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${LOG_EXCEPTION_CONVERSION_WORD:-%rEx}"/>
<property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${PID:- } --- [%t] %-40.40logger{39} : %m%n%rEx"/> <property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%rEx}"/>
<appender name="DEBUG_LEVEL_REMAPPER" class="org.springframework.boot.logging.logback.LevelRemappingAppender"> <appender name="DEBUG_LEVEL_REMAPPER" class="org.springframework.boot.logging.logback.LevelRemappingAppender">
<destinationLogger>org.springframework.boot</destinationLogger> <destinationLogger>org.springframework.boot</destinationLogger>

@ -89,6 +89,7 @@ public class LoggingApplicationListenerTests {
System.clearProperty("LOG_FILE"); System.clearProperty("LOG_FILE");
System.clearProperty("LOG_PATH"); System.clearProperty("LOG_PATH");
System.clearProperty("PID"); System.clearProperty("PID");
System.clearProperty("LOG_EXCEPTION_CONVERSION_WORD");
if (this.context != null) { if (this.context != null) {
this.context.close(); this.context.close();
} }
@ -109,10 +110,10 @@ public class LoggingApplicationListenerTests {
public void baseConfigLocation() { public void baseConfigLocation() {
this.initializer.initialize(this.context.getEnvironment(), this.initializer.initialize(this.context.getEnvironment(),
this.context.getClassLoader()); this.context.getClassLoader());
this.logger.info("Hello world"); this.outputCapture.expect(containsString("Hello world"));
String output = this.outputCapture.toString().trim(); this.outputCapture.expect(not(containsString("???")));
assertTrue("Wrong output:\n" + output, output.contains("Hello world")); this.outputCapture.expect(containsString("[junit-"));
assertFalse("Wrong output:\n" + output, output.contains("???")); this.logger.info("Hello world", new RuntimeException("Expected"));
assertFalse(new File(tmpDir() + "/spring.log").exists()); assertFalse(new File(tmpDir() + "/spring.log").exists());
} }
@ -328,6 +329,18 @@ public class LoggingApplicationListenerTests {
assertFalse(bridgeHandlerInstalled()); 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() { private boolean bridgeHandlerInstalled() {
Logger rootLogger = LogManager.getLogManager().getLogger(""); Logger rootLogger = LogManager.getLogManager().getLogger("");
Handler[] handlers = rootLogger.getHandlers(); Handler[] handlers = rootLogger.getHandlers();

@ -17,6 +17,7 @@
package org.springframework.boot.logging.log4j2; package org.springframework.boot.logging.log4j2;
import java.io.File; import java.io.File;
import java.io.FileReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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.Logger;
import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.FileConfigurationMonitor; import org.apache.logging.log4j.core.config.FileConfigurationMonitor;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Rule; import org.junit.Rule;
@ -32,6 +35,7 @@ import org.junit.Test;
import org.springframework.boot.logging.AbstractLoggingSystemTests; import org.springframework.boot.logging.AbstractLoggingSystemTests;
import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.test.OutputCapture; import org.springframework.boot.test.OutputCapture;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@ -189,21 +193,49 @@ public class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests {
@Test @Test
public void exceptionsIncludeClassPackaging() throws Exception { public void exceptionsIncludeClassPackaging() throws Exception {
this.loggingSystem.beforeInitialize(); this.loggingSystem.beforeInitialize();
this.logger.info("Hidden"); this.loggingSystem.initialize(null, null, getLogFile(null, tmpDir()));
this.loggingSystem.initialize(null, null, null); Matcher<String> expectedOutput = containsString("[junit-");
this.output.expect(containsString("[junit-")); this.output.expect(expectedOutput);
this.logger.warn("Expected exception", new RuntimeException("Expected")); this.logger.warn("Expected exception", new RuntimeException("Expected"));
String fileContents = FileCopyUtils.copyToString(new FileReader(new File(tmpDir()
+ "/spring.log")));
assertThat(fileContents, is(expectedOutput));
} }
@Test @Test
public void rootCauseIsLoggedFirst() throws Exception { public void rootCauseIsLoggedFirst() throws Exception {
this.loggingSystem.beforeInitialize(); this.loggingSystem.beforeInitialize();
this.logger.info("Hidden"); this.loggingSystem.initialize(null, null, getLogFile(null, tmpDir()));
this.loggingSystem.initialize(null, null, null); Matcher<String> expectedOutput = containsString("Wrapped by: "
this.output.expect(containsString("Wrapped by: " + "java.lang.RuntimeException: Expected");
+ "java.lang.RuntimeException: Expected")); this.output.expect(expectedOutput);
this.logger.warn("Expected exception", new RuntimeException("Expected", this.logger.warn("Expected exception", new RuntimeException("Expected",
new RuntimeException("Cause"))); 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<String> 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 { private static class TestLog4J2LoggingSystem extends Log4J2LoggingSystem {

@ -23,6 +23,8 @@ import java.util.logging.LogManager;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.impl.SLF4JLogFactory; import org.apache.commons.logging.impl.SLF4JLogFactory;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; 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.containsString;
import static org.hamcrest.Matchers.equalTo; 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.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@ -241,21 +245,52 @@ public class LogbackLoggingSystemTests extends AbstractLoggingSystemTests {
@Test @Test
public void exceptionsIncludeClassPackaging() throws Exception { public void exceptionsIncludeClassPackaging() throws Exception {
this.loggingSystem.beforeInitialize(); this.loggingSystem.beforeInitialize();
this.logger.info("Hidden"); this.loggingSystem.initialize(this.initializationContext, null,
this.loggingSystem.initialize(this.initializationContext, null, null); getLogFile(null, tmpDir()));
this.output.expect(containsString("[junit-")); Matcher<String> expectedOutput = containsString("[junit-");
this.output.expect(expectedOutput);
this.logger.warn("Expected exception", new RuntimeException("Expected")); this.logger.warn("Expected exception", new RuntimeException("Expected"));
String fileContents = FileCopyUtils.copyToString(new FileReader(new File(tmpDir()
+ "/spring.log")));
assertThat(fileContents, is(expectedOutput));
} }
@Test @Test
public void rootCauseIsLoggedFirst() throws Exception { public void rootCauseIsLoggedFirst() throws Exception {
this.loggingSystem.beforeInitialize(); this.loggingSystem.beforeInitialize();
this.logger.info("Hidden"); this.loggingSystem.initialize(this.initializationContext, null,
this.loggingSystem.initialize(this.initializationContext, null, null); getLogFile(null, tmpDir()));
this.output.expect(containsString("Wrapped by: " Matcher<String> expectedOutput = containsString("Wrapped by: "
+ "java.lang.RuntimeException: Expected")); + "java.lang.RuntimeException: Expected");
this.output.expect(expectedOutput);
this.logger.warn("Expected exception", new RuntimeException("Expected", this.logger.warn("Expected exception", new RuntimeException("Expected",
new RuntimeException("Cause"))); 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<String> 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 { private String getLineWithText(File file, String outputSearch) throws Exception {

Loading…
Cancel
Save