Skip to content

Commit 005528d

Browse files
committed
Add support for bold, italic, underline, reverse, and bg_* colors in %clr converter
- 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 Closes #49262 Signed-off-by: mvirole <virolemayank@gmail.com>
1 parent cfee3f1 commit 005528d

5 files changed

Lines changed: 245 additions & 28 deletions

File tree

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ public enum AnsiStyle implements AnsiElement {
3232

3333
ITALIC("3"),
3434

35-
UNDERLINE("4");
35+
UNDERLINE("4"),
36+
37+
REVERSE("7");
3638

3739
private final String code;
3840

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

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.logging.log4j2;
1818

19+
import java.util.ArrayList;
1920
import java.util.Arrays;
2021
import java.util.Collections;
2122
import java.util.HashMap;
@@ -35,15 +36,19 @@
3536
import org.apache.logging.log4j.core.pattern.PatternParser;
3637
import org.jspecify.annotations.Nullable;
3738

39+
import org.springframework.boot.ansi.AnsiBackground;
3840
import org.springframework.boot.ansi.AnsiColor;
3941
import org.springframework.boot.ansi.AnsiElement;
4042
import org.springframework.boot.ansi.AnsiOutput;
4143
import org.springframework.boot.ansi.AnsiStyle;
4244

4345
/**
4446
* Log4j2 {@link LogEventPatternConverter} to color output using the {@link AnsiOutput}
45-
* class. A single option 'styling' can be provided to the converter, or if not specified
46-
* color styling will be picked based on the logging level.
47+
* class. One or more styling options can be provided to the converter, or if not
48+
* specified color styling will be picked based on the logging level. Supported options
49+
* include foreground colors (e.g. {@code red}, {@code bright_blue}), background colors
50+
* (e.g. {@code bg_red}, {@code bg_bright_green}), and text styles (e.g. {@code bold},
51+
* {@code underline}, {@code reverse}).
4752
*
4853
* @author Vladimir Tsanev
4954
* @since 1.3.0
@@ -56,10 +61,17 @@ public final class ColorConverter extends LogEventPatternConverter {
5661

5762
static {
5863
Map<String, AnsiElement> ansiElements = new HashMap<>();
64+
// Foreground colors (e.g. "red", "bright_blue")
5965
Arrays.stream(AnsiColor.values())
6066
.filter((color) -> color != AnsiColor.DEFAULT)
6167
.forEach((color) -> ansiElements.put(color.name().toLowerCase(Locale.ROOT), color));
62-
ansiElements.put("faint", AnsiStyle.FAINT);
68+
// Text styles (e.g. "bold", "italic", "underline", "reverse", "faint", "normal")
69+
Arrays.stream(AnsiStyle.values())
70+
.forEach((style) -> ansiElements.put(style.name().toLowerCase(Locale.ROOT), style));
71+
// Background colors with "bg_" prefix (e.g. "bg_red", "bg_bright_blue")
72+
Arrays.stream(AnsiBackground.values())
73+
.filter((bg) -> bg != AnsiBackground.DEFAULT)
74+
.forEach((bg) -> ansiElements.put("bg_" + bg.name().toLowerCase(Locale.ROOT), bg));
6375
ELEMENTS = Collections.unmodifiableMap(ansiElements);
6476
}
6577

@@ -75,12 +87,12 @@ public final class ColorConverter extends LogEventPatternConverter {
7587

7688
private final List<PatternFormatter> formatters;
7789

78-
private final @Nullable AnsiElement styling;
90+
private final List<AnsiElement> stylings;
7991

80-
private ColorConverter(List<PatternFormatter> formatters, @Nullable AnsiElement styling) {
92+
private ColorConverter(List<PatternFormatter> formatters, List<AnsiElement> stylings) {
8193
super("style", "style");
8294
this.formatters = formatters;
83-
this.styling = styling;
95+
this.stylings = stylings;
8496
}
8597

8698
@Override
@@ -100,20 +112,29 @@ public void format(LogEvent event, StringBuilder toAppendTo) {
100112
formatter.format(event, buf);
101113
}
102114
if (!buf.isEmpty()) {
103-
AnsiElement element = this.styling;
104-
if (element == null) {
115+
if (this.stylings.isEmpty()) {
105116
// Assume highlighting
106-
element = LEVELS.get(event.getLevel().intLevel());
117+
AnsiElement element = LEVELS.get(event.getLevel().intLevel());
107118
element = (element != null) ? element : AnsiColor.GREEN;
119+
appendAnsiString(toAppendTo, buf.toString(), element);
120+
}
121+
else {
122+
appendAnsiString(toAppendTo, buf.toString(), this.stylings.toArray(new AnsiElement[0]));
108123
}
109-
appendAnsiString(toAppendTo, buf.toString(), element);
110124
}
111125
}
112126

113127
protected void appendAnsiString(StringBuilder toAppendTo, String in, AnsiElement element) {
114128
toAppendTo.append(AnsiOutput.toString(element, in));
115129
}
116130

131+
protected void appendAnsiString(StringBuilder toAppendTo, String in, AnsiElement... elements) {
132+
Object[] ansiParams = new Object[elements.length + 1];
133+
System.arraycopy(elements, 0, ansiParams, 0, elements.length);
134+
ansiParams[elements.length] = in;
135+
toAppendTo.append(AnsiOutput.toString(ansiParams));
136+
}
137+
117138
/**
118139
* Creates a new instance of the class. Required by Log4J2.
119140
* @param config the configuration
@@ -131,8 +152,19 @@ protected void appendAnsiString(StringBuilder toAppendTo, String in, AnsiElement
131152
}
132153
PatternParser parser = PatternLayout.createPatternParser(config);
133154
List<PatternFormatter> formatters = parser.parse(options[0]);
134-
AnsiElement element = (options.length != 1) ? ELEMENTS.get(options[1]) : null;
135-
return new ColorConverter(formatters, element);
155+
List<AnsiElement> stylings = new ArrayList<>();
156+
for (int i = 1; i < options.length; i++) {
157+
if (options[i] != null) {
158+
String[] optionParts = options[i].split(",");
159+
for (String optionPart : optionParts) {
160+
AnsiElement element = ELEMENTS.get(optionPart.trim().toLowerCase(Locale.ROOT));
161+
if (element != null) {
162+
stylings.add(element);
163+
}
164+
}
165+
}
166+
}
167+
return new ColorConverter(formatters, stylings);
136168
}
137169

138170
}

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

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,31 @@
1616

1717
package org.springframework.boot.logging.logback;
1818

19+
import java.util.ArrayList;
1920
import java.util.Arrays;
2021
import java.util.Collections;
2122
import java.util.HashMap;
23+
import java.util.List;
2224
import java.util.Locale;
2325
import java.util.Map;
2426

2527
import ch.qos.logback.classic.Level;
2628
import ch.qos.logback.classic.spi.ILoggingEvent;
2729
import ch.qos.logback.core.pattern.CompositeConverter;
2830

31+
import org.springframework.boot.ansi.AnsiBackground;
2932
import org.springframework.boot.ansi.AnsiColor;
3033
import org.springframework.boot.ansi.AnsiElement;
3134
import org.springframework.boot.ansi.AnsiOutput;
3235
import org.springframework.boot.ansi.AnsiStyle;
3336

3437
/**
3538
* Logback {@link CompositeConverter} to color output using the {@link AnsiOutput} class.
36-
* A single 'color' option can be provided to the converter, or if not specified color
37-
* will be picked based on the logging level.
39+
* One or more styling options can be provided to the converter, or if not specified color
40+
* will be picked based on the logging level. Supported options include foreground colors
41+
* (e.g. {@code red}, {@code bright_blue}), background colors (e.g. {@code bg_red},
42+
* {@code bg_bright_green}), and text styles (e.g. {@code bold}, {@code underline},
43+
* {@code reverse}).
3844
*
3945
* @author Phillip Webb
4046
* @since 1.0.0
@@ -45,10 +51,17 @@ public class ColorConverter extends CompositeConverter<ILoggingEvent> {
4551

4652
static {
4753
Map<String, AnsiElement> ansiElements = new HashMap<>();
54+
// Foreground colors (e.g. "red", "bright_blue")
4855
Arrays.stream(AnsiColor.values())
4956
.filter((color) -> color != AnsiColor.DEFAULT)
5057
.forEach((color) -> ansiElements.put(color.name().toLowerCase(Locale.ROOT), color));
51-
ansiElements.put("faint", AnsiStyle.FAINT);
58+
// Text styles (e.g. "bold", "italic", "underline", "reverse", "faint", "normal")
59+
Arrays.stream(AnsiStyle.values())
60+
.forEach((style) -> ansiElements.put(style.name().toLowerCase(Locale.ROOT), style));
61+
// Background colors with "bg_" prefix (e.g. "bg_red", "bg_bright_blue")
62+
Arrays.stream(AnsiBackground.values())
63+
.filter((bg) -> bg != AnsiBackground.DEFAULT)
64+
.forEach((bg) -> ansiElements.put("bg_" + bg.name().toLowerCase(Locale.ROOT), bg));
5265
ELEMENTS = Collections.unmodifiableMap(ansiElements);
5366
}
5467

@@ -63,19 +76,35 @@ public class ColorConverter extends CompositeConverter<ILoggingEvent> {
6376

6477
@Override
6578
protected String transform(ILoggingEvent event, String in) {
66-
AnsiElement color = ELEMENTS.get(getFirstOption());
67-
if (color == null) {
79+
List<String> options = getOptionList();
80+
List<AnsiElement> elements = new ArrayList<>();
81+
if (options != null) {
82+
for (String option : options) {
83+
AnsiElement element = ELEMENTS.get(option.trim().toLowerCase(Locale.ROOT));
84+
if (element != null) {
85+
elements.add(element);
86+
}
87+
}
88+
}
89+
if (elements.isEmpty()) {
6890
// Assume highlighting
69-
color = LEVELS.get(event.getLevel().toInteger());
70-
color = (color != null) ? color : AnsiColor.GREEN;
91+
AnsiElement element = LEVELS.get(event.getLevel().toInteger());
92+
elements.add((element != null) ? element : AnsiColor.GREEN);
7193
}
72-
return toAnsiString(in, color);
94+
return toAnsiString(in, elements.toArray(new AnsiElement[0]));
7395
}
7496

7597
protected String toAnsiString(String in, AnsiElement element) {
7698
return AnsiOutput.toString(element, in);
7799
}
78100

101+
protected String toAnsiString(String in, AnsiElement... elements) {
102+
Object[] ansiParams = new Object[elements.length + 1];
103+
System.arraycopy(elements, 0, ansiParams, 0, elements.length);
104+
ansiParams[elements.length] = in;
105+
return AnsiOutput.toString(ansiParams);
106+
}
107+
79108
static String getName(AnsiElement element) {
80109
return ELEMENTS.entrySet()
81110
.stream()

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

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,48 +173,131 @@ void brightCyan() {
173173
assertThat(output).hasToString("\033[96min\033[0;39m");
174174
}
175175

176+
@Test
177+
void bold() {
178+
StringBuilder output = new StringBuilder();
179+
newConverter("bold").format(this.event, output);
180+
assertThat(output).hasToString("\033[1min\033[0;39m");
181+
}
182+
183+
@Test
184+
void italic() {
185+
StringBuilder output = new StringBuilder();
186+
newConverter("italic").format(this.event, output);
187+
assertThat(output).hasToString("\033[3min\033[0;39m");
188+
}
189+
190+
@Test
191+
void underline() {
192+
StringBuilder output = new StringBuilder();
193+
newConverter("underline").format(this.event, output);
194+
assertThat(output).hasToString("\033[4min\033[0;39m");
195+
}
196+
197+
@Test
198+
void reverse() {
199+
StringBuilder output = new StringBuilder();
200+
newConverter("reverse").format(this.event, output);
201+
assertThat(output).hasToString("\033[7min\033[0;39m");
202+
}
203+
204+
@Test
205+
void bgRed() {
206+
StringBuilder output = new StringBuilder();
207+
newConverter("bg_red").format(this.event, output);
208+
assertThat(output).hasToString("\033[41min\033[0;39m");
209+
}
210+
211+
@Test
212+
void bgGreen() {
213+
StringBuilder output = new StringBuilder();
214+
newConverter("bg_green").format(this.event, output);
215+
assertThat(output).hasToString("\033[42min\033[0;39m");
216+
}
217+
218+
@Test
219+
void bgYellow() {
220+
StringBuilder output = new StringBuilder();
221+
newConverter("bg_yellow").format(this.event, output);
222+
assertThat(output).hasToString("\033[43min\033[0;39m");
223+
}
224+
225+
@Test
226+
void bgBlue() {
227+
StringBuilder output = new StringBuilder();
228+
newConverter("bg_blue").format(this.event, output);
229+
assertThat(output).hasToString("\033[44min\033[0;39m");
230+
}
231+
232+
@Test
233+
void bgBrightRed() {
234+
StringBuilder output = new StringBuilder();
235+
newConverter("bg_bright_red").format(this.event, output);
236+
assertThat(output).hasToString("\033[101min\033[0;39m");
237+
}
238+
239+
@Test
240+
void multipleStylesCommaSeparated() {
241+
StringBuilder output = new StringBuilder();
242+
newConverter("bold, red").format(this.event, output);
243+
assertThat(output).hasToString("\033[1;31min\033[0;39m");
244+
}
245+
246+
@Test
247+
void multipleStylesMultipleOptions() {
248+
StringBuilder output = new StringBuilder();
249+
newConverter("bold", "red").format(this.event, output);
250+
assertThat(output).hasToString("\033[1;31min\033[0;39m");
251+
}
252+
176253
@Test
177254
void highlightFatal() {
178255
this.event.setLevel(Level.FATAL);
179256
StringBuilder output = new StringBuilder();
180-
newConverter(null).format(this.event, output);
257+
newConverter((String) null).format(this.event, output);
181258
assertThat(output).hasToString("\033[31min\033[0;39m");
182259
}
183260

184261
@Test
185262
void highlightError() {
186263
this.event.setLevel(Level.ERROR);
187264
StringBuilder output = new StringBuilder();
188-
newConverter(null).format(this.event, output);
265+
newConverter((String) null).format(this.event, output);
189266
assertThat(output).hasToString("\033[31min\033[0;39m");
190267
}
191268

192269
@Test
193270
void highlightWarn() {
194271
this.event.setLevel(Level.WARN);
195272
StringBuilder output = new StringBuilder();
196-
newConverter(null).format(this.event, output);
273+
newConverter((String) null).format(this.event, output);
197274
assertThat(output).hasToString("\033[33min\033[0;39m");
198275
}
199276

200277
@Test
201278
void highlightDebug() {
202279
this.event.setLevel(Level.DEBUG);
203280
StringBuilder output = new StringBuilder();
204-
newConverter(null).format(this.event, output);
281+
newConverter((String) null).format(this.event, output);
205282
assertThat(output).hasToString("\033[32min\033[0;39m");
206283
}
207284

208285
@Test
209286
void highlightTrace() {
210287
this.event.setLevel(Level.TRACE);
211288
StringBuilder output = new StringBuilder();
212-
newConverter(null).format(this.event, output);
289+
newConverter((String) null).format(this.event, output);
213290
assertThat(output).hasToString("\033[32min\033[0;39m");
214291
}
215292

216-
private ColorConverter newConverter(@Nullable String styling) {
217-
ColorConverter converter = ColorConverter.newInstance(null, new String[] { this.in, styling });
293+
private ColorConverter newConverter(@Nullable String... stylings) {
294+
if (stylings == null) {
295+
stylings = new String[] { null };
296+
}
297+
String[] options = new String[1 + stylings.length];
298+
options[0] = this.in;
299+
System.arraycopy(stylings, 0, options, 1, stylings.length);
300+
ColorConverter converter = ColorConverter.newInstance(null, options);
218301
assertThat(converter).isNotNull();
219302
return converter;
220303
}

0 commit comments

Comments
 (0)