diff --git a/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/main/resources/log4j2.xml b/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/main/resources/log4j2.xml index 501a92243b..519e37c7c1 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/main/resources/log4j2.xml +++ b/spring-boot-samples/spring-boot-sample-actuator-log4j2/src/main/resources/log4j2.xml @@ -2,7 +2,7 @@ ???? - [%d{yyyy-MM-dd HH:mm:ss.SSS}] log4j2%X{context} - ${sys:PID} %5p [%t] --- %c{1}: %m%n + %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 diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java b/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java new file mode 100644 index 0000000000..5a5e50c6a7 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java @@ -0,0 +1,140 @@ +/* + * 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 ELEMENTS; + static { + Map elements = new HashMap(); + 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 LEVELS; + static { + Map levels = new HashMap(); + 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 formatters; + + private final AnsiElement styling; + + private ColorConverter(List 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 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)); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/WhitespaceThrowablePatternConverter.java b/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/WhitespaceThrowablePatternConverter.java new file mode 100644 index 0000000000..b7e3f2f168 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/WhitespaceThrowablePatternConverter.java @@ -0,0 +1,60 @@ +/* + * 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()); + } + } + +} 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 9171b146c2..aae5083f0b 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,7 @@ ???? - [%d{yyyy-MM-dd HH:mm:ss.SSS}] boot%X{context} - ${sys:PID} %5p [%t] --- %c{1}: %m%n + %d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${sys:PID} --- [%t] %-40.40c{1.} : %m%n%wEx 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 45e09bc402..d650fceb1a 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,7 @@ ???? - [%d{yyyy-MM-dd HH:mm:ss.SSS}] boot%X{context} - ${sys:PID} %5p [%t] --- %c{1}: %m%n + %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 diff --git a/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java b/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java new file mode 100644 index 0000000000..4aebbb38b5 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java @@ -0,0 +1,164 @@ +/* + * 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; + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/WhitespaceThrowablePatternConverterTests.java b/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/WhitespaceThrowablePatternConverterTests.java new file mode 100644 index 0000000000..dd4ba2a1e1 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/WhitespaceThrowablePatternConverterTests.java @@ -0,0 +1,41 @@ +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)); + } + +}