Browse Source

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
pull/49261/head
Andy Wilkinson 2 weeks ago committed by GitHub
parent
commit
bf1b0f6e32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java
  2. 51
      core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java
  3. 45
      core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java
  4. 76
      core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java
  5. 78
      core/spring-boot/src/test/java/org/springframework/boot/logging/logback/ColorConverterTests.java
  6. 37
      documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc

4
core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java

@ -32,7 +32,9 @@ public enum AnsiStyle implements AnsiElement { @@ -32,7 +32,9 @@ public enum AnsiStyle implements AnsiElement {
ITALIC("3"),
UNDERLINE("4");
UNDERLINE("4"),
REVERSE("7");
private final String code;

51
core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java

@ -16,6 +16,7 @@ @@ -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; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);
}
}

45
core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java

@ -16,9 +16,11 @@ @@ -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; @@ -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; @@ -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> { @@ -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> { @@ -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) {
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);
}
}
}
}
if (elements.isEmpty()) {
// Assume highlighting
color = LEVELS.get(event.getLevel().toInteger());
color = (color != null) ? color : AnsiColor.GREEN;
AnsiElement element = LEVELS.get(event.getLevel().toInteger());
elements.add((element != null) ? element : AnsiColor.GREEN);
}
return toAnsiString(in, color);
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()

76
core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java

@ -173,6 +173,76 @@ class ColorConverterTests { @@ -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 { @@ -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
core/spring-boot/src/test/java/org/springframework/boot/logging/logback/ColorConverterTests.java

@ -16,6 +16,7 @@ @@ -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 { @@ -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);

37
documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc

@ -113,15 +113,15 @@ The following table describes the mapping of log levels to colors: @@ -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: @@ -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]]

Loading…
Cancel
Save