Commit 632d78b6 authored by Vladimir Tsanev's avatar Vladimir Tsanev Committed by Andy Wilkinson

Prettify Log4J 2's default output

This commit updates the default output produced when using Log2J 2 to
be similar to the output produced by Logback. It introduces a new
converter for throwables that adds some whitespace around stacktraces
and a color converter that produces ANSI-colored output when enabled
via spring.output.ansi.enabled.

Closes gh-3548
parent 65efd172
......@@ -2,7 +2,7 @@
<Configuration status="WARN" monitorInterval="30">
<Properties>
<Property name="PID">????</Property>
<Property name="LOG_PATTERN">[%d{yyyy-MM-dd HH:mm:ss.SSS}] log4j2%X{context} - ${sys:PID} %5p [%t] --- %c{1}: %m%n</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%wEx</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
......
/*
* Copyright 2012-2015 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.boot.logging.log4j2;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.apache.logging.log4j.core.pattern.ConverterKeys;
import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
import org.apache.logging.log4j.core.pattern.PatternConverter;
import org.apache.logging.log4j.core.pattern.PatternFormatter;
import org.apache.logging.log4j.core.pattern.PatternParser;
import org.springframework.boot.ansi.AnsiColor;
import org.springframework.boot.ansi.AnsiElement;
import org.springframework.boot.ansi.AnsiOutput;
import org.springframework.boot.ansi.AnsiStyle;
/**
* Log4j2 {@link LogEventPatternConverter} colors output using the {@link AnsiOutput}
* class. A single option 'styling' can be provided to the converter, or if not specified
* color styling will be picked based on the logging level.
*
* @author Vladimir Tsanev
* @since 1.3.0
*/
@Plugin(name = "color", category = PatternConverter.CATEGORY)
@ConverterKeys(value = { "clr", "color" })
public class ColorConverter extends LogEventPatternConverter {
private static final Map<String, AnsiElement> ELEMENTS;
static {
Map<String, AnsiElement> elements = new HashMap<String, AnsiElement>();
elements.put("faint", AnsiStyle.FAINT);
elements.put("red", AnsiColor.RED);
elements.put("green", AnsiColor.GREEN);
elements.put("yellow", AnsiColor.YELLOW);
elements.put("blue", AnsiColor.BLUE);
elements.put("magenta", AnsiColor.MAGENTA);
elements.put("cyan", AnsiColor.CYAN);
ELEMENTS = Collections.unmodifiableMap(elements);
}
private static final Map<Integer, AnsiElement> LEVELS;
static {
Map<Integer, AnsiElement> levels = new HashMap<Integer, AnsiElement>();
levels.put(Level.FATAL.intLevel(), AnsiColor.RED);
levels.put(Level.ERROR.intLevel(), AnsiColor.RED);
levels.put(Level.WARN.intLevel(), AnsiColor.YELLOW);
LEVELS = Collections.unmodifiableMap(levels);
}
private final List<PatternFormatter> formatters;
private final AnsiElement styling;
private ColorConverter(List<PatternFormatter> formatters, AnsiElement styling) {
super("style", "style");
this.formatters = formatters;
this.styling = styling;
}
/**
* Creates a new instance of the class. Required by Log4J2.
*
* @param config the configuration
* @param options the options
* @return a new instance, or {@code null} if the options are invalid
*/
public static ColorConverter newInstance(Configuration config, String[] options) {
if (options.length < 1) {
LOGGER.error(
"Incorrect number of options on style. Expected at least 1, received {}",
options.length);
return null;
}
if (options[0] == null) {
LOGGER.error("No pattern supplied on style");
return null;
}
PatternParser parser = PatternLayout.createPatternParser(config);
List<PatternFormatter> formatters = parser.parse(options[0]);
AnsiElement element = options.length == 1 ? null : ELEMENTS.get(options[1]);
return new ColorConverter(formatters, element);
}
@Override
public boolean handlesThrowable() {
for (PatternFormatter formatter : this.formatters) {
if (formatter.handlesThrowable()) {
return true;
}
}
return super.handlesThrowable();
}
@Override
public void format(LogEvent event, StringBuilder toAppendTo) {
StringBuilder buf = new StringBuilder();
for (PatternFormatter formatter : this.formatters) {
formatter.format(event, buf);
}
if (buf.length() > 0) {
AnsiElement element = this.styling;
if (element == null) {
// Assume highlighting
element = LEVELS.get(event.getLevel().intLevel());
element = (element == null ? AnsiColor.GREEN : element);
}
appendAnsiString(toAppendTo, buf.toString(), element);
}
}
protected void appendAnsiString(StringBuilder toAppendTo, String in,
AnsiElement element) {
toAppendTo.append(AnsiOutput.toString(element, in));
}
}
/*
* Copyright 2012-2015 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.boot.logging.log4j2;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.pattern.ConverterKeys;
import org.apache.logging.log4j.core.pattern.PatternConverter;
import org.apache.logging.log4j.core.pattern.ThrowablePatternConverter;
/**
* {@link ThrowablePatternConverter} that adds some additional whitespace around the stack
* trace.
*
* @author Vladimir Tsanev
* @since 1.3.0
*/
@Plugin(name = "WhitespaceThrowablePatternConverter", category = PatternConverter.CATEGORY)
@ConverterKeys({ "wEx", "wThrowable", "wException" })
public class WhitespaceThrowablePatternConverter extends ThrowablePatternConverter {
private WhitespaceThrowablePatternConverter(String[] options) {
super("WhitespaceThrowable", "throwable", options);
}
/**
* Creates a new instance of the class. Required by Log4J2.
*
* @param options pattern options, may be null. If first element is "short", only the
* first line of the throwable will be formatted.
* @return a new {@code WhitespaceThrowablePatternConverter}
*/
public static WhitespaceThrowablePatternConverter newInstance(String[] options) {
return new WhitespaceThrowablePatternConverter(options);
}
@Override
public void format(LogEvent event, StringBuilder buffer) {
if (event.getThrown() != null) {
buffer.append(this.options.getSeparator());
super.format(event, buffer);
buffer.append(this.options.getSeparator());
}
}
}
......@@ -2,7 +2,7 @@
<Configuration status="WARN">
<Properties>
<Property name="PID">????</Property>
<Property name="LOG_PATTERN">[%d{yyyy-MM-dd HH:mm:ss.SSS}] boot%X{context} - ${sys:PID} %5p [%t] --- %c{1}: %m%n</Property>
<Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${sys:PID} --- [%t] %-40.40c{1.} : %m%n%wEx</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
......
......@@ -2,7 +2,7 @@
<Configuration status="WARN">
<Properties>
<Property name="PID">????</Property>
<Property name="LOG_PATTERN">[%d{yyyy-MM-dd HH:mm:ss.SSS}] boot%X{context} - ${sys:PID} %5p [%t] --- %c{1}: %m%n</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%wEx</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
......
/*
* Copyright 2012-2015 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.boot.logging.log4j2;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.AbstractLogEvent;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.boot.ansi.AnsiOutput;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link ColorConverter}.
*
* @author Vladimir Tsanev
*/
public class ColorConverterTests {
private final String in = "in";
private TestLogEvent event;
@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.event = new TestLogEvent();
}
private ColorConverter newConverter(String styling) {
return ColorConverter.newInstance(null, new String[] { this.in, styling });
}
@Test
public void faint() {
StringBuilder output = new StringBuilder();
newConverter("faint").format(this.event, output);
assertThat(output.toString(), equalTo("\033[2min\033[0;39m"));
}
@Test
public void red() {
StringBuilder output = new StringBuilder();
newConverter("red").format(this.event, output);
assertThat(output.toString(), equalTo("\033[31min\033[0;39m"));
}
@Test
public void green() throws Exception {
StringBuilder output = new StringBuilder();
newConverter("green").format(this.event, output);
assertThat(output.toString(), equalTo("\033[32min\033[0;39m"));
}
@Test
public void yellow() throws Exception {
StringBuilder output = new StringBuilder();
newConverter("yellow").format(this.event, output);
assertThat(output.toString(), equalTo("\033[33min\033[0;39m"));
}
@Test
public void blue() throws Exception {
StringBuilder output = new StringBuilder();
newConverter("blue").format(this.event, output);
assertThat(output.toString(), equalTo("\033[34min\033[0;39m"));
}
@Test
public void magenta() throws Exception {
StringBuilder output = new StringBuilder();
newConverter("magenta").format(this.event, output);
assertThat(output.toString(), equalTo("\033[35min\033[0;39m"));
}
@Test
public void cyan() throws Exception {
StringBuilder output = new StringBuilder();
newConverter("cyan").format(this.event, output);
assertThat(output.toString(), equalTo("\033[36min\033[0;39m"));
}
@Test
public void highlightFatal() throws Exception {
this.event.setLevel(Level.FATAL);
StringBuilder output = new StringBuilder();
newConverter(null).format(this.event, output);
assertThat(output.toString(), equalTo("\033[31min\033[0;39m"));
}
@Test
public void highlightError() throws Exception {
this.event.setLevel(Level.ERROR);
StringBuilder output = new StringBuilder();
newConverter(null).format(this.event, output);
assertThat(output.toString(), equalTo("\033[31min\033[0;39m"));
}
@Test
public void highlightWarn() throws Exception {
this.event.setLevel(Level.WARN);
StringBuilder output = new StringBuilder();
newConverter(null).format(this.event, output);
assertThat(output.toString(), equalTo("\033[33min\033[0;39m"));
}
@Test
public void highlightDebug() throws Exception {
this.event.setLevel(Level.DEBUG);
StringBuilder output = new StringBuilder();
newConverter(null).format(this.event, output);
assertThat(output.toString(), equalTo("\033[32min\033[0;39m"));
}
@Test
public void highlightTrace() throws Exception {
this.event.setLevel(Level.TRACE);
StringBuilder output = new StringBuilder();
newConverter(null).format(this.event, output);
assertThat(output.toString(), equalTo("\033[32min\033[0;39m"));
}
private static class TestLogEvent extends AbstractLogEvent {
private Level level;
@Override
public Level getLevel() {
return this.level;
}
public void setLevel(Level level) {
this.level = level;
}
}
}
package org.springframework.boot.logging.log4j2;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.impl.Log4jLogEvent;
import org.junit.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
/**
* Tests for {@link WhitespaceThrowablePatternConverter}.
*
* @author Vladimir Tsanev
*/
public class WhitespaceThrowablePatternConverterTests {
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
private final WhitespaceThrowablePatternConverter converter = WhitespaceThrowablePatternConverter
.newInstance(new String[] {});
@Test
public void noStackTrace() throws Exception {
LogEvent event = Log4jLogEvent.newBuilder().build();
StringBuilder builder = new StringBuilder();
this.converter.format(event, builder);
assertThat(builder.toString(), equalTo(""));
}
@Test
public void withStackTrace() throws Exception {
LogEvent event = Log4jLogEvent.newBuilder().setThrown(new Exception()).build();
StringBuilder builder = new StringBuilder();
this.converter.format(event, builder);
assertThat(builder.toString(), startsWith(LINE_SEPARATOR));
assertThat(builder.toString(), endsWith(LINE_SEPARATOR));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment