Support color log output

Rework logback formatting to include ansi color output. Also added
support for JUL over SLF4J to ensure that tomcat logging looks OK.

Issue: #53249833
This commit is contained in:
Phillip Webb 2013-07-13 16:08:41 -07:00
parent 5c46a39494
commit dc1b787a01
21 changed files with 391 additions and 28 deletions

View File

@ -446,6 +446,11 @@
<artifactId>slf4j-api</artifactId>
<version>1.7.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>

View File

@ -63,6 +63,11 @@
<artifactId>slf4j-api</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>

View File

@ -25,7 +25,7 @@ import org.springframework.util.ClassUtils;
* @author Phillip Webb
* @author Dave Syer
*/
abstract class AbstractLoggingSystem extends LoggingSystem {
public abstract class AbstractLoggingSystem extends LoggingSystem {
private final ClassLoader classLoader;

View File

@ -16,6 +16,10 @@
package org.springframework.bootstrap.logging;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.util.ClassUtils;
/**
@ -26,6 +30,17 @@ import org.springframework.util.ClassUtils;
*/
public abstract class LoggingSystem {
private static final Map<String, String> SYSTEMS;
static {
Map<String, String> systems = new LinkedHashMap<String, String>();
String pkg = LoggingSystem.class.getPackage().getName();
systems.put("ch.qos.logback.core.Appender", pkg + ".logback.LogbackLoggingSystem");
systems.put("org.apache.log4j.PropertyConfigurator", pkg
+ ".log4j.Log4JLoggingSystem");
systems.put("java.util.logging.LogManager", pkg + ".java.JavaLoggingSystem");
SYSTEMS = Collections.unmodifiableMap(systems);
}
/**
* Reset the logging system to be limit output. This method may be called before
* {@link #initialize()} to reduce logging noise until the systems has been full
@ -51,13 +66,20 @@ public abstract class LoggingSystem {
* @return The logging system
*/
public static LoggingSystem get(ClassLoader classLoader) {
if (ClassUtils.isPresent("ch.qos.logback.core.Appender", classLoader)) {
return new LogbackLoggingSystem(classLoader);
for (Map.Entry<String, String> entry : SYSTEMS.entrySet()) {
if (ClassUtils.isPresent(entry.getKey(), classLoader)) {
try {
Class<?> systemClass = ClassUtils.forName(entry.getValue(),
classLoader);
return (LoggingSystem) systemClass.getConstructor(ClassLoader.class)
.newInstance(classLoader);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
if (ClassUtils.isPresent("org.apache.log4j.PropertyConfigurator", classLoader)) {
return new Log4JLoggingSystem(classLoader);
}
return new JavaLoggingSystem(classLoader);
throw new IllegalStateException("No suitable logging system located");
}
}

View File

@ -14,11 +14,13 @@
* limitations under the License.
*/
package org.springframework.bootstrap.logging;
package org.springframework.bootstrap.logging.java;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import org.springframework.bootstrap.logging.AbstractLoggingSystem;
import org.springframework.bootstrap.logging.LoggingSystem;
import org.springframework.util.ResourceUtils;
import org.springframework.util.SystemPropertyUtils;
@ -28,7 +30,7 @@ import org.springframework.util.SystemPropertyUtils;
* @author Phillip Webb
* @author Dave Syer
*/
class JavaLoggingSystem extends AbstractLoggingSystem {
public class JavaLoggingSystem extends AbstractLoggingSystem {
public JavaLoggingSystem(ClassLoader classLoader) {
super(classLoader, "logging.properties");

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.bootstrap.logging;
package org.springframework.bootstrap.logging.java;
import java.io.PrintWriter;
import java.io.StringWriter;
@ -27,7 +27,7 @@ import java.util.logging.LogRecord;
*
* @author Phillip Webb
*/
public class JavaLoggingFormatter extends Formatter {
public class SimpleFormatter extends Formatter {
private static final String FORMAT = "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL - [%7$s] %4$s - %3$s : %5$s%6$s%n";

View File

@ -14,8 +14,10 @@
* limitations under the License.
*/
package org.springframework.bootstrap.logging;
package org.springframework.bootstrap.logging.log4j;
import org.springframework.bootstrap.logging.AbstractLoggingSystem;
import org.springframework.bootstrap.logging.LoggingSystem;
import org.springframework.util.Log4jConfigurer;
/**
@ -24,7 +26,7 @@ import org.springframework.util.Log4jConfigurer;
* @author Phillip Webb
* @author Dave Syer
*/
class Log4JLoggingSystem extends AbstractLoggingSystem {
public class Log4JLoggingSystem extends AbstractLoggingSystem {
public Log4JLoggingSystem(ClassLoader classLoader) {
super(classLoader, "log4j.xml", "log4j.properties");

View File

@ -0,0 +1,74 @@
/*
* Copyright 2012-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.bootstrap.logging.logback;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.bootstrap.ansi.AnsiElement;
import org.springframework.bootstrap.ansi.AnsiOutput;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.pattern.CompositeConverter;
/**
* Logback {@link CompositeConverter} colors output using the {@link AnsiOutput} class. A
* single 'color' option can be provided to the converter, or if not specified color will
* be picked based on the logging level.
*
* @author Phillip Webb
*/
public class ColorConverter extends CompositeConverter<ILoggingEvent> {
private static final Map<String, AnsiElement> ELEMENTS;
static {
Map<String, AnsiElement> elements = new HashMap<String, AnsiElement>();
elements.put("faint", AnsiElement.FAINT);
elements.put("red", AnsiElement.RED);
elements.put("green", AnsiElement.GREEN);
elements.put("yellow", AnsiElement.YELLOW);
elements.put("blue", AnsiElement.BLUE);
elements.put("magenta", AnsiElement.MAGENTA);
elements.put("cyan", AnsiElement.CYAN);
ELEMENTS = Collections.unmodifiableMap(elements);
}
private static final Map<Integer, AnsiElement> LEVELS;
static {
Map<Integer, AnsiElement> levels = new HashMap<Integer, AnsiElement>();
levels.put(Level.ERROR_INTEGER, AnsiElement.RED);
levels.put(Level.WARN_INTEGER, AnsiElement.YELLOW);
LEVELS = Collections.unmodifiableMap(levels);
}
@Override
protected String transform(ILoggingEvent event, String in) {
AnsiElement element = ELEMENTS.get(getFirstOption());
if (element == null) {
// Assume highlighting
element = LEVELS.get(event.getLevel().toInteger());
element = (element == null ? AnsiElement.GREEN : element);
}
return toAnsiString(in, element);
}
protected String toAnsiString(String in, AnsiElement element) {
return AnsiOutput.toString(element, in);
}
}

View File

@ -14,13 +14,17 @@
* limitations under the License.
*/
package org.springframework.bootstrap.logging;
package org.springframework.bootstrap.logging.logback;
import java.net.URL;
import org.slf4j.ILoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import org.slf4j.impl.StaticLoggerBinder;
import org.springframework.bootstrap.logging.AbstractLoggingSystem;
import org.springframework.bootstrap.logging.LoggingSystem;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.SystemPropertyUtils;
@ -33,12 +37,21 @@ import ch.qos.logback.classic.util.ContextInitializer;
* @author Phillip Webb
* @author Dave Syer
*/
class LogbackLoggingSystem extends AbstractLoggingSystem {
public class LogbackLoggingSystem extends AbstractLoggingSystem {
public LogbackLoggingSystem(ClassLoader classLoader) {
super(classLoader, "logback.xml");
}
@Override
public void beforeInitialize() {
super.beforeInitialize();
if (ClassUtils.isPresent("org.slf4j.bridge.SLF4JBridgeHandler", getClassLoader())) {
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
}
}
@Override
public void initialize(String configLocation) {
String resolvedLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);

View File

@ -0,0 +1,37 @@
/*
* Copyright 2012-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.bootstrap.logging.logback;
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.core.CoreConstants;
/**
* {@link ThrowableProxyConverter} that adds some additional whitespace around the stack
* trace.
*
* @author Phillip Webb
*/
public class WhitespaceThrowableProxyConverter extends ThrowableProxyConverter {
@Override
protected String throwableProxyToString(IThrowableProxy tp) {
return CoreConstants.LINE_SEPARATOR + super.throwableProxyToString(tp)
+ CoreConstants.LINE_SEPARATOR;
}
}

View File

@ -3,9 +3,9 @@ handlers = java.util.logging.FileHandler, java.util.logging.ConsoleHandler
# File Logging
java.util.logging.FileHandler.pattern = %t/service.log
java.util.logging.FileHandler.formatter = org.springframework.bootstrap.logging.JavaLoggingFormatter
java.util.logging.FileHandler.formatter = org.springframework.bootstrap.logging.java.SimpleFormatter
java.util.logging.FileHandler.level = INFO
java.util.logging.FileHandler.limit = 10485760
java.util.logging.FileHandler.count = 10
java.util.logging.ConsoleHandler.formatter = org.springframework.bootstrap.logging.JavaLoggingFormatter
java.util.logging.ConsoleHandler.formatter = org.springframework.bootstrap.logging.java.SimpleFormatter

View File

@ -1,16 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] - ${PID:-????} %5p [%t] --- %c{1}: %m%n"/>
<property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-/tmp/}spring.log}"/>
<property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${PID:- } --- %-40.40logger{39} : %m [%t]%n%wex"/>
<property name="CONSOLE_LOG_PATTERN" value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m %clr([%t]){faint}%n%wex"/>
<conversionRule conversionWord="clr" converterClass="org.springframework.bootstrap.logging.logback.ColorConverter" />
<conversionRule conversionWord="wex" converterClass="org.springframework.bootstrap.logging.logback.WhitespaceThrowableProxyConverter" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<file>${LOG_FILE}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
@ -21,6 +27,7 @@
<MaxFileSize>10MB</MaxFileSize>
</triggeringPolicy>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.bootstrap.logging;
package org.springframework.bootstrap.context.initializer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -29,7 +29,7 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.bootstrap.context.initializer.LoggingApplicationContextInitializer;
import org.springframework.bootstrap.logging.java.JavaLoggingSystem;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.env.PropertySource;
@ -60,7 +60,7 @@ public class LoggingApplicationContextInitializerTests {
this.output = new ByteArrayOutputStream();
System.setOut(new PrintStream(this.output));
LogManager.getLogManager().readConfiguration(
getClass().getResourceAsStream("logging.properties"));
JavaLoggingSystem.class.getResourceAsStream("logging.properties"));
}
@After
@ -135,7 +135,7 @@ public class LoggingApplicationContextInitializerTests {
}
if ("logging.file".equals(name)) {
return "foo.log";
}
}
return null;
}
});

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.bootstrap.logging;
package org.springframework.bootstrap.logging.java;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -25,6 +25,7 @@ import org.apache.commons.logging.impl.Jdk14Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.bootstrap.logging.java.JavaLoggingSystem;
import static org.junit.Assert.assertTrue;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.bootstrap.logging;
package org.springframework.bootstrap.logging.java;
import java.util.logging.Formatter;
import java.util.logging.LogRecord;

View File

@ -0,0 +1,136 @@
/*
* Copyright 2012-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.bootstrap.logging.logback;
import java.util.Collections;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.bootstrap.ansi.AnsiOutput;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.LoggingEvent;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link ColorConverter}.
*
* @author Phillip Webb
*/
public class ColorConverterTests {
private ColorConverter converter;
private LoggingEvent event;
private String in = "in";
@BeforeClass
public static void setupAnsi() {
AnsiOutput.setEnabled(AnsiOutput.Enabled.ALWAYS);
}
@AfterClass
public static void resetAnsi() {
AnsiOutput.setEnabled(AnsiOutput.Enabled.DETECT);
}
@Before
public void setup() {
this.converter = new ColorConverter();
this.event = new LoggingEvent();
}
@Test
public void faint() throws Exception {
this.converter.setOptionList(Collections.singletonList("faint"));
String out = this.converter.transform(this.event, this.in);
assertThat(out, equalTo("\033[2min\033[0;39m"));
}
@Test
public void red() throws Exception {
this.converter.setOptionList(Collections.singletonList("red"));
String out = this.converter.transform(this.event, this.in);
assertThat(out, equalTo("\033[31min\033[0;39m"));
}
@Test
public void green() throws Exception {
this.converter.setOptionList(Collections.singletonList("green"));
String out = this.converter.transform(this.event, this.in);
assertThat(out, equalTo("\033[32min\033[0;39m"));
}
@Test
public void yellow() throws Exception {
this.converter.setOptionList(Collections.singletonList("yellow"));
String out = this.converter.transform(this.event, this.in);
assertThat(out, equalTo("\033[33min\033[0;39m"));
}
@Test
public void blue() throws Exception {
this.converter.setOptionList(Collections.singletonList("blue"));
String out = this.converter.transform(this.event, this.in);
assertThat(out, equalTo("\033[34min\033[0;39m"));
}
@Test
public void magenta() throws Exception {
this.converter.setOptionList(Collections.singletonList("magenta"));
String out = this.converter.transform(this.event, this.in);
assertThat(out, equalTo("\033[35min\033[0;39m"));
}
@Test
public void cyan() throws Exception {
this.converter.setOptionList(Collections.singletonList("cyan"));
String out = this.converter.transform(this.event, this.in);
assertThat(out, equalTo("\033[36min\033[0;39m"));
}
@Test
public void highlightError() throws Exception {
this.event.setLevel(Level.ERROR);
String out = this.converter.transform(this.event, this.in);
assertThat(out, equalTo("\033[31min\033[0;39m"));
}
@Test
public void highlightWarn() throws Exception {
this.event.setLevel(Level.WARN);
String out = this.converter.transform(this.event, this.in);
assertThat(out, equalTo("\033[33min\033[0;39m"));
}
@Test
public void highlightDebug() throws Exception {
this.event.setLevel(Level.DEBUG);
String out = this.converter.transform(this.event, this.in);
assertThat(out, equalTo("\033[32min\033[0;39m"));
}
@Test
public void highlightTrace() throws Exception {
this.event.setLevel(Level.TRACE);
String out = this.converter.transform(this.event, this.in);
assertThat(out, equalTo("\033[32min\033[0;39m"));
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.bootstrap.logging;
package org.springframework.bootstrap.logging.logback;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
@ -24,6 +24,7 @@ import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.bootstrap.logging.logback.LogbackLoggingSystem;
import static org.junit.Assert.assertTrue;

View File

@ -0,0 +1,54 @@
/*
* Copyright 2012-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.bootstrap.logging.logback;
import org.junit.Test;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.spi.ThrowableProxy;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link WhitespaceThrowableProxyConverter}.
*
* @author Phillip Webb
*/
public class WhitespaceThrowableProxyConverterTests {
private WhitespaceThrowableProxyConverter converter = new WhitespaceThrowableProxyConverter();
private LoggingEvent event = new LoggingEvent();
@Test
public void noStackTrace() throws Exception {
String s = this.converter.convert(this.event);
assertThat(s, equalTo(""));
}
@Test
public void withStackTrace() throws Exception {
this.event.setThrowableProxy(new ThrowableProxy(new RuntimeException()));
String s = this.converter.convert(this.event);
assertThat(s, startsWith("\n"));
assertThat(s, endsWith("\n"));
}
}

View File

@ -5,5 +5,5 @@ java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = org.springframework.bootstrap.logging.StandardFormatter
java.util.logging.ConsoleHandler.formatter = org.springframework.bootstrap.logging.java.SimpleFormatter
java.util.logging.SimpleFormatter.format = %4$s: %5$s [%1$tc]%n

View File

@ -17,6 +17,10 @@
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>