mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Add more styling support to the Logback and Log4j2 color converters
- Add REVERSE (ANSI code 7) to AnsiStyle enum - Expand ColorConverter ELEMENTS map to include all AnsiStyle values (bold, italic, underline, normal, faint, reverse) and all AnsiBackground colors with the bg_ prefix (bg_red, bg_bright_green, etc.) - Update tests for both logback and log4j2 converters See gh-49285 Signed-off-by: mvirole <virolemayank@gmail.com>
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;
|
||||
|
||||
|
||||
+44
-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
|
||||
@@ -56,10 +61,17 @@ public final class ColorConverter extends LogEventPatternConverter {
|
||||
|
||||
static {
|
||||
Map<String, AnsiElement> ansiElements = new HashMap<>();
|
||||
// Foreground colors (e.g. "red", "bright_blue")
|
||||
Arrays.stream(AnsiColor.values())
|
||||
.filter((color) -> color != AnsiColor.DEFAULT)
|
||||
.forEach((color) -> ansiElements.put(color.name().toLowerCase(Locale.ROOT), color));
|
||||
ansiElements.put("faint", AnsiStyle.FAINT);
|
||||
// Text styles (e.g. "bold", "italic", "underline", "reverse", "faint", "normal")
|
||||
Arrays.stream(AnsiStyle.values())
|
||||
.forEach((style) -> ansiElements.put(style.name().toLowerCase(Locale.ROOT), style));
|
||||
// Background colors with "bg_" prefix (e.g. "bg_red", "bg_bright_blue")
|
||||
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 +87,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 +112,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 +128,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 +152,19 @@ 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<>();
|
||||
for (int i = 1; i < options.length; i++) {
|
||||
if (options[i] != null) {
|
||||
String[] optionParts = options[i].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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+41
-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
|
||||
@@ -45,10 +51,17 @@ public class ColorConverter extends CompositeConverter<ILoggingEvent> {
|
||||
|
||||
static {
|
||||
Map<String, AnsiElement> ansiElements = new HashMap<>();
|
||||
// Foreground colors (e.g. "red", "bright_blue")
|
||||
Arrays.stream(AnsiColor.values())
|
||||
.filter((color) -> color != AnsiColor.DEFAULT)
|
||||
.forEach((color) -> ansiElements.put(color.name().toLowerCase(Locale.ROOT), color));
|
||||
ansiElements.put("faint", AnsiStyle.FAINT);
|
||||
// Text styles (e.g. "bold", "italic", "underline", "reverse", "faint", "normal")
|
||||
Arrays.stream(AnsiStyle.values())
|
||||
.forEach((style) -> ansiElements.put(style.name().toLowerCase(Locale.ROOT), style));
|
||||
// Background colors with "bg_" prefix (e.g. "bg_red", "bg_bright_blue")
|
||||
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 +76,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()
|
||||
|
||||
+90
-7
@@ -173,11 +173,88 @@ 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 multipleStylesCommaSeparated() {
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter("bold, red").format(this.event, output);
|
||||
assertThat(output).hasToString("\033[1;31min\033[0;39m");
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleStylesMultipleOptions() {
|
||||
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);
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter(null).format(this.event, output);
|
||||
newConverter((String) null).format(this.event, output);
|
||||
assertThat(output).hasToString("\033[31min\033[0;39m");
|
||||
}
|
||||
|
||||
@@ -185,7 +262,7 @@ class ColorConverterTests {
|
||||
void highlightError() {
|
||||
this.event.setLevel(Level.ERROR);
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter(null).format(this.event, output);
|
||||
newConverter((String) null).format(this.event, output);
|
||||
assertThat(output).hasToString("\033[31min\033[0;39m");
|
||||
}
|
||||
|
||||
@@ -193,7 +270,7 @@ class ColorConverterTests {
|
||||
void highlightWarn() {
|
||||
this.event.setLevel(Level.WARN);
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter(null).format(this.event, output);
|
||||
newConverter((String) null).format(this.event, output);
|
||||
assertThat(output).hasToString("\033[33min\033[0;39m");
|
||||
}
|
||||
|
||||
@@ -201,7 +278,7 @@ class ColorConverterTests {
|
||||
void highlightDebug() {
|
||||
this.event.setLevel(Level.DEBUG);
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter(null).format(this.event, output);
|
||||
newConverter((String) null).format(this.event, output);
|
||||
assertThat(output).hasToString("\033[32min\033[0;39m");
|
||||
}
|
||||
|
||||
@@ -209,12 +286,18 @@ class ColorConverterTests {
|
||||
void highlightTrace() {
|
||||
this.event.setLevel(Level.TRACE);
|
||||
StringBuilder output = new StringBuilder();
|
||||
newConverter(null).format(this.event, output);
|
||||
newConverter((String) null).format(this.event, output);
|
||||
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... stylings) {
|
||||
if (stylings == null) {
|
||||
stylings = new String[] { null };
|
||||
}
|
||||
String[] options = new String[1 + stylings.length];
|
||||
options[0] = this.in;
|
||||
System.arraycopy(stylings, 0, options, 1, stylings.length);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user