mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-22 17:35:22 +00:00
Merge pull request #49285 from mayankvirole/feature/extend-clr-ansi-styles
Add more styling support to the Logback and Log4j2 color converters Closes gh-49285
This commit is contained in:
@@ -32,7 +32,9 @@ public enum AnsiStyle implements AnsiElement {
|
||||
|
||||
ITALIC("3"),
|
||||
|
||||
UNDERLINE("4");
|
||||
UNDERLINE("4"),
|
||||
|
||||
REVERSE("7");
|
||||
|
||||
private final String code;
|
||||
|
||||
|
||||
+39
-12
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.boot.logging.log4j2;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -35,6 +36,7 @@ import org.apache.logging.log4j.core.pattern.PatternFormatter;
|
||||
import org.apache.logging.log4j.core.pattern.PatternParser;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.ansi.AnsiBackground;
|
||||
import org.springframework.boot.ansi.AnsiColor;
|
||||
import org.springframework.boot.ansi.AnsiElement;
|
||||
import org.springframework.boot.ansi.AnsiOutput;
|
||||
@@ -42,8 +44,11 @@ import org.springframework.boot.ansi.AnsiStyle;
|
||||
|
||||
/**
|
||||
* Log4j2 {@link LogEventPatternConverter} to color 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.
|
||||
* class. One or more styling options can be provided to the converter, or if not
|
||||
* specified color styling will be picked based on the logging level. Supported options
|
||||
* include foreground colors (e.g. {@code red}, {@code bright_blue}), background colors
|
||||
* (e.g. {@code bg_red}, {@code bg_bright_green}), and text styles (e.g. {@code bold},
|
||||
* {@code underline}, {@code reverse}).
|
||||
*
|
||||
* @author Vladimir Tsanev
|
||||
* @since 1.3.0
|
||||
@@ -59,7 +64,11 @@ public final class ColorConverter extends LogEventPatternConverter {
|
||||
Arrays.stream(AnsiColor.values())
|
||||
.filter((color) -> color != AnsiColor.DEFAULT)
|
||||
.forEach((color) -> ansiElements.put(color.name().toLowerCase(Locale.ROOT), color));
|
||||
ansiElements.put("faint", AnsiStyle.FAINT);
|
||||
Arrays.stream(AnsiStyle.values())
|
||||
.forEach((style) -> ansiElements.put(style.name().toLowerCase(Locale.ROOT), style));
|
||||
Arrays.stream(AnsiBackground.values())
|
||||
.filter((bg) -> bg != AnsiBackground.DEFAULT)
|
||||
.forEach((bg) -> ansiElements.put("bg_" + bg.name().toLowerCase(Locale.ROOT), bg));
|
||||
ELEMENTS = Collections.unmodifiableMap(ansiElements);
|
||||
}
|
||||
|
||||
@@ -75,12 +84,12 @@ public final class ColorConverter extends LogEventPatternConverter {
|
||||
|
||||
private final List<PatternFormatter> formatters;
|
||||
|
||||
private final @Nullable AnsiElement styling;
|
||||
private final List<AnsiElement> stylings;
|
||||
|
||||
private ColorConverter(List<PatternFormatter> formatters, @Nullable AnsiElement styling) {
|
||||
private ColorConverter(List<PatternFormatter> formatters, List<AnsiElement> stylings) {
|
||||
super("style", "style");
|
||||
this.formatters = formatters;
|
||||
this.styling = styling;
|
||||
this.stylings = stylings;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -100,13 +109,15 @@ public final class ColorConverter extends LogEventPatternConverter {
|
||||
formatter.format(event, buf);
|
||||
}
|
||||
if (!buf.isEmpty()) {
|
||||
AnsiElement element = this.styling;
|
||||
if (element == null) {
|
||||
if (this.stylings.isEmpty()) {
|
||||
// Assume highlighting
|
||||
element = LEVELS.get(event.getLevel().intLevel());
|
||||
AnsiElement element = LEVELS.get(event.getLevel().intLevel());
|
||||
element = (element != null) ? element : AnsiColor.GREEN;
|
||||
appendAnsiString(toAppendTo, buf.toString(), element);
|
||||
}
|
||||
else {
|
||||
appendAnsiString(toAppendTo, buf.toString(), this.stylings.toArray(new AnsiElement[0]));
|
||||
}
|
||||
appendAnsiString(toAppendTo, buf.toString(), element);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +125,13 @@ public final class ColorConverter extends LogEventPatternConverter {
|
||||
toAppendTo.append(AnsiOutput.toString(element, in));
|
||||
}
|
||||
|
||||
protected void appendAnsiString(StringBuilder toAppendTo, String in, AnsiElement... elements) {
|
||||
Object[] ansiParams = new Object[elements.length + 1];
|
||||
System.arraycopy(elements, 0, ansiParams, 0, elements.length);
|
||||
ansiParams[elements.length] = in;
|
||||
toAppendTo.append(AnsiOutput.toString(ansiParams));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the class. Required by Log4J2.
|
||||
* @param config the configuration
|
||||
@@ -131,8 +149,17 @@ public final class ColorConverter extends LogEventPatternConverter {
|
||||
}
|
||||
PatternParser parser = PatternLayout.createPatternParser(config);
|
||||
List<PatternFormatter> formatters = parser.parse(options[0]);
|
||||
AnsiElement element = (options.length != 1) ? ELEMENTS.get(options[1]) : null;
|
||||
return new ColorConverter(formatters, element);
|
||||
List<AnsiElement> stylings = new ArrayList<>();
|
||||
if (options.length >= 2 && options[1] != null) {
|
||||
String[] optionParts = options[1].split(",");
|
||||
for (String optionPart : optionParts) {
|
||||
AnsiElement element = ELEMENTS.get(optionPart.trim().toLowerCase(Locale.ROOT));
|
||||
if (element != null) {
|
||||
stylings.add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ColorConverter(formatters, stylings);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+38
-9
@@ -16,9 +16,11 @@
|
||||
|
||||
package org.springframework.boot.logging.logback;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -26,6 +28,7 @@ import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.pattern.CompositeConverter;
|
||||
|
||||
import org.springframework.boot.ansi.AnsiBackground;
|
||||
import org.springframework.boot.ansi.AnsiColor;
|
||||
import org.springframework.boot.ansi.AnsiElement;
|
||||
import org.springframework.boot.ansi.AnsiOutput;
|
||||
@@ -33,8 +36,11 @@ import org.springframework.boot.ansi.AnsiStyle;
|
||||
|
||||
/**
|
||||
* Logback {@link CompositeConverter} to color 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.
|
||||
* One or more styling options can be provided to the converter, or if not specified color
|
||||
* will be picked based on the logging level. Supported options include foreground colors
|
||||
* (e.g. {@code red}, {@code bright_blue}), background colors (e.g. {@code bg_red},
|
||||
* {@code bg_bright_green}), and text styles (e.g. {@code bold}, {@code underline},
|
||||
* {@code reverse}).
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
@@ -48,7 +54,11 @@ public class ColorConverter extends CompositeConverter<ILoggingEvent> {
|
||||
Arrays.stream(AnsiColor.values())
|
||||
.filter((color) -> color != AnsiColor.DEFAULT)
|
||||
.forEach((color) -> ansiElements.put(color.name().toLowerCase(Locale.ROOT), color));
|
||||
ansiElements.put("faint", AnsiStyle.FAINT);
|
||||
Arrays.stream(AnsiStyle.values())
|
||||
.forEach((style) -> ansiElements.put(style.name().toLowerCase(Locale.ROOT), style));
|
||||
Arrays.stream(AnsiBackground.values())
|
||||
.filter((bg) -> bg != AnsiBackground.DEFAULT)
|
||||
.forEach((bg) -> ansiElements.put("bg_" + bg.name().toLowerCase(Locale.ROOT), bg));
|
||||
ELEMENTS = Collections.unmodifiableMap(ansiElements);
|
||||
}
|
||||
|
||||
@@ -63,19 +73,38 @@ public class ColorConverter extends CompositeConverter<ILoggingEvent> {
|
||||
|
||||
@Override
|
||||
protected String transform(ILoggingEvent event, String in) {
|
||||
AnsiElement color = ELEMENTS.get(getFirstOption());
|
||||
if (color == null) {
|
||||
// Assume highlighting
|
||||
color = LEVELS.get(event.getLevel().toInteger());
|
||||
color = (color != null) ? color : AnsiColor.GREEN;
|
||||
List<String> options = getOptionList();
|
||||
List<AnsiElement> elements = new ArrayList<>();
|
||||
if (options != null) {
|
||||
for (String option : options) {
|
||||
String[] optionParts = option.split(",");
|
||||
for (String optionPart : optionParts) {
|
||||
AnsiElement element = ELEMENTS.get(optionPart.trim().toLowerCase(Locale.ROOT));
|
||||
if (element != null) {
|
||||
elements.add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return toAnsiString(in, color);
|
||||
if (elements.isEmpty()) {
|
||||
// Assume highlighting
|
||||
AnsiElement element = LEVELS.get(event.getLevel().toInteger());
|
||||
elements.add((element != null) ? element : AnsiColor.GREEN);
|
||||
}
|
||||
return toAnsiString(in, elements.toArray(new AnsiElement[0]));
|
||||
}
|
||||
|
||||
protected String toAnsiString(String in, AnsiElement element) {
|
||||
return AnsiOutput.toString(element, in);
|
||||
}
|
||||
|
||||
protected String toAnsiString(String in, AnsiElement... elements) {
|
||||
Object[] ansiParams = new Object[elements.length + 1];
|
||||
System.arraycopy(elements, 0, ansiParams, 0, elements.length);
|
||||
ansiParams[elements.length] = in;
|
||||
return AnsiOutput.toString(ansiParams);
|
||||
}
|
||||
|
||||
static String getName(AnsiElement element) {
|
||||
return ELEMENTS.entrySet()
|
||||
.stream()
|
||||
|
||||
+74
-2
@@ -173,6 +173,76 @@ class ColorConverterTests {
|
||||
assertThat(output).hasToString("\033[96min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bold() {
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter("bold").format(this.event, output);
|
||||
assertThat(output).hasToString("\033[1min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void italic() {
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter("italic").format(this.event, output);
|
||||
assertThat(output).hasToString("\033[3min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void underline() {
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter("underline").format(this.event, output);
|
||||
assertThat(output).hasToString("\033[4min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reverse() {
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter("reverse").format(this.event, output);
|
||||
assertThat(output).hasToString("\033[7min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bgRed() {
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter("bg_red").format(this.event, output);
|
||||
assertThat(output).hasToString("\033[41min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bgGreen() {
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter("bg_green").format(this.event, output);
|
||||
assertThat(output).hasToString("\033[42min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bgYellow() {
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter("bg_yellow").format(this.event, output);
|
||||
assertThat(output).hasToString("\033[43min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bgBlue() {
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter("bg_blue").format(this.event, output);
|
||||
assertThat(output).hasToString("\033[44min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bgBrightRed() {
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter("bg_bright_red").format(this.event, output);
|
||||
assertThat(output).hasToString("\033[101min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleStyles() {
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter("bold, red").format(this.event, output);
|
||||
assertThat(output).hasToString("\033[1;31min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void highlightFatal() {
|
||||
this.event.setLevel(Level.FATAL);
|
||||
@@ -213,8 +283,10 @@ class ColorConverterTests {
|
||||
assertThat(output).hasToString("\033[32min\033[0;39m");
|
||||
}
|
||||
|
||||
private ColorConverter newConverter(@Nullable String styling) {
|
||||
ColorConverter converter = ColorConverter.newInstance(null, new String[] { this.in, styling });
|
||||
private ColorConverter newConverter(@Nullable String additionalOptions) {
|
||||
String[] options = (additionalOptions != null) ? new String[] { this.in, additionalOptions }
|
||||
: new String[] { this.in };
|
||||
ColorConverter converter = ColorConverter.newInstance(null, options);
|
||||
assertThat(converter).isNotNull();
|
||||
return converter;
|
||||
}
|
||||
|
||||
+78
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.boot.logging.logback;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
@@ -170,6 +171,83 @@ class ColorConverterTests {
|
||||
assertThat(out).isEqualTo("\033[96min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bold() {
|
||||
this.converter.setOptionList(Collections.singletonList("bold"));
|
||||
String out = this.converter.transform(this.event, this.in);
|
||||
assertThat(out).isEqualTo("\033[1min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void italic() {
|
||||
this.converter.setOptionList(Collections.singletonList("italic"));
|
||||
String out = this.converter.transform(this.event, this.in);
|
||||
assertThat(out).isEqualTo("\033[3min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void underline() {
|
||||
this.converter.setOptionList(Collections.singletonList("underline"));
|
||||
String out = this.converter.transform(this.event, this.in);
|
||||
assertThat(out).isEqualTo("\033[4min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reverse() {
|
||||
this.converter.setOptionList(Collections.singletonList("reverse"));
|
||||
String out = this.converter.transform(this.event, this.in);
|
||||
assertThat(out).isEqualTo("\033[7min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bgRed() {
|
||||
this.converter.setOptionList(Collections.singletonList("bg_red"));
|
||||
String out = this.converter.transform(this.event, this.in);
|
||||
assertThat(out).isEqualTo("\033[41min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bgGreen() {
|
||||
this.converter.setOptionList(Collections.singletonList("bg_green"));
|
||||
String out = this.converter.transform(this.event, this.in);
|
||||
assertThat(out).isEqualTo("\033[42min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bgYellow() {
|
||||
this.converter.setOptionList(Collections.singletonList("bg_yellow"));
|
||||
String out = this.converter.transform(this.event, this.in);
|
||||
assertThat(out).isEqualTo("\033[43min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bgBlue() {
|
||||
this.converter.setOptionList(Collections.singletonList("bg_blue"));
|
||||
String out = this.converter.transform(this.event, this.in);
|
||||
assertThat(out).isEqualTo("\033[44min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bgBrightRed() {
|
||||
this.converter.setOptionList(Collections.singletonList("bg_bright_red"));
|
||||
String out = this.converter.transform(this.event, this.in);
|
||||
assertThat(out).isEqualTo("\033[101min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleStylesCommaSeparated() {
|
||||
this.converter.setOptionList(Collections.singletonList("bold, red"));
|
||||
String out = this.converter.transform(this.event, this.in);
|
||||
assertThat(out).isEqualTo("\033[1;31min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleStyles() {
|
||||
this.converter.setOptionList(Arrays.asList("bold", "red"));
|
||||
String out = this.converter.transform(this.event, this.in);
|
||||
assertThat(out).isEqualTo("\033[1;31min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void highlightError() {
|
||||
this.event.setLevel(Level.ERROR);
|
||||
|
||||
+32
-5
@@ -113,15 +113,15 @@ The following table describes the mapping of log levels to colors:
|
||||
| Green
|
||||
|===
|
||||
|
||||
Alternatively, you can specify the color or style that should be used by providing it as an option to the conversion.
|
||||
For example, to make the text yellow, use the following setting:
|
||||
Alternatively, you can specify the color and styles that should be used by providing them as options to the conversion.
|
||||
For example, to make the text yellow and bold, use the following setting:
|
||||
|
||||
[source]
|
||||
----
|
||||
%clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}){yellow}
|
||||
%clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}){yellow,bold}
|
||||
----
|
||||
|
||||
The following colors and styles are supported:
|
||||
The following text colors are supported:
|
||||
|
||||
* `black`
|
||||
* `blue`
|
||||
@@ -134,13 +134,40 @@ The following colors and styles are supported:
|
||||
* `bright_white`
|
||||
* `bright_yellow`
|
||||
* `cyan`
|
||||
* `faint`
|
||||
* `green`
|
||||
* `magenta`
|
||||
* `red`
|
||||
* `white`
|
||||
* `yellow`
|
||||
|
||||
The following background colors are supported:
|
||||
|
||||
* `bg_black`
|
||||
* `bg_blue`
|
||||
* `bg_bright_black`
|
||||
* `bg_bright_blue`
|
||||
* `bg_bright_cyan`
|
||||
* `bg_bright_green`
|
||||
* `bg_bright_magenta`
|
||||
* `bg_bright_red`
|
||||
* `bg_bright_white`
|
||||
* `bg_bright_yellow`
|
||||
* `bg_cyan`
|
||||
* `bg_green`
|
||||
* `bg_magenta`
|
||||
* `bg_red`
|
||||
* `bg_white`
|
||||
* `bg_yellow`
|
||||
|
||||
The following styles are supported:
|
||||
|
||||
* `bold`
|
||||
* `faint`
|
||||
* `italic`
|
||||
* `normal`
|
||||
* `reverse`
|
||||
* `underline`
|
||||
|
||||
|
||||
|
||||
[[features.logging.file-output]]
|
||||
|
||||
Reference in New Issue
Block a user