diff --git a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java index c30a88c2bfb..0d2a734f37b 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java @@ -32,7 +32,9 @@ public enum AnsiStyle implements AnsiElement { ITALIC("3"), - UNDERLINE("4"); + UNDERLINE("4"), + + REVERSE("7"); private final String code; diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java index af4a4d64921..ba2bc58b35b 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java @@ -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 formatters; - private final @Nullable AnsiElement styling; + private final List stylings; - private ColorConverter(List formatters, @Nullable AnsiElement styling) { + private ColorConverter(List formatters, List 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 formatters = parser.parse(options[0]); - AnsiElement element = (options.length != 1) ? ELEMENTS.get(options[1]) : null; - return new ColorConverter(formatters, element); + List 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); } } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java index 5a9bc415239..4a16f7784b9 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java @@ -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 { 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 { @Override protected String transform(ILoggingEvent event, String in) { - AnsiElement color = ELEMENTS.get(getFirstOption()); - if (color == null) { + List options = getOptionList(); + List 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() diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java index a699e7aaef1..fe76b5a144e 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ColorConverterTests.java @@ -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; } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/ColorConverterTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/ColorConverterTests.java index 83720c1dc1f..736595756ca 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/ColorConverterTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/ColorConverterTests.java @@ -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); diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc index 69ffa2d30cf..53760e83f54 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc +++ b/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: | 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]]