PatternFormatterFactory.java
package io.jstach.rainbowgum.pattern.format;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BLACK_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BLUE_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BOLD;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.CYAN_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.DEFAULT_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.GRAY_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.GREEN_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.MAGENTA_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.RED_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.WHITE_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.YELLOW_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.FAINT;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_BLACK;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_RED;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_GREEN;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_YELLOW;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_BLUE;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_MAGENTA;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_CYAN;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_WHITE;
import java.lang.System.Logger.Level;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicLong;
import org.jspecify.annotations.Nullable;
import io.jstach.rainbowgum.LogEvent;
import io.jstach.rainbowgum.LogEvent.Caller;
import io.jstach.rainbowgum.LogFormatter;
import io.jstach.rainbowgum.LogFormatter.EventFormatter;
import io.jstach.rainbowgum.LogFormatter.ThrowableFormatter;
import io.jstach.rainbowgum.pattern.Padding;
import io.jstach.rainbowgum.pattern.PatternKeyword;
import io.jstach.rainbowgum.pattern.format.PatternFormatterFactory.CompositeFactory;
import io.jstach.rainbowgum.pattern.format.PatternFormatterFactory.KeywordFactory;
/**
* Creates formatters from pattern keywords and are registered to a
* {@link PatternRegistry}.
*/
public sealed interface PatternFormatterFactory {
/**
* Whether this factory's formatter renders (or, like <code>%nopex</code>,
* deliberately suppresses) a throwable, and thus counts as the pattern having
* addressed exception rendering itself. {@link PatternCompiler} appends a default
* exception formatter to any pattern where every keyword answers {@code false} here,
* so a logged throwable is never silently dropped just because the pattern forgot to
* ask for it - see the built-in throwable keywords and Spring Boot's
* <code>%wEx</code>/<code>%wex</code> for the keywords that override this.
* @return {@code false} by default.
*/
default boolean isExceptionFormatter() {
return false;
}
/**
* A composite formatter factory expects possible children. For example
* {@code %keyword(child) }.
*/
public non-sealed interface CompositeFactory extends PatternFormatterFactory {
/**
* Creates a formatter from a keyword.
* @param config config to help create formatter.
* @param node current keyword info.
* @param child the embedded child in the keyword or null if non is provided.
* @return formatter.
*/
public LogFormatter create(PatternConfig config, PatternKeyword node, @Nullable LogFormatter child);
}
/**
* A keyword formatter factory expects keywords and option list. It is usually in the
* format of {@code %keyword}.
*/
public non-sealed interface KeywordFactory extends PatternFormatterFactory {
/**
* Creates a formatter from a keyword.
* @param config config to help create formatter.
* @param node current keyword info.
* @return formatter.
*/
public LogFormatter create(PatternConfig config, PatternKeyword node);
/**
* Creates a keyword factory from a formatter that only cares about padding info.
* @param formatter existing formatter.
* @return factory.
*/
static PatternFormatterFactory.KeywordFactory of(LogFormatter formatter) {
return (c, n) -> PadFormatter.of(formatter, n.padding());
}
/**
* Creates a keyword factory from a formatter that only cares about padding info,
* additionally marking it as {@link #isExceptionFormatter()}. Lambdas cannot
* override default methods, hence this overload instead of a plain lambda for
* keywords like <code>%nopex</code> that need to.
* @param formatter existing formatter.
* @param isExceptionFormatter value to return from
* {@link #isExceptionFormatter()}.
* @return factory.
*/
static PatternFormatterFactory.KeywordFactory of(LogFormatter formatter, boolean isExceptionFormatter) {
return new KeywordFactory() {
@Override
public LogFormatter create(PatternConfig config, PatternKeyword node) {
return PadFormatter.of(formatter, node.padding());
}
@Override
public boolean isExceptionFormatter() {
return isExceptionFormatter;
}
};
}
}
/**
* Parses the depth (<code>full</code>/<code>short</code>/a number) and exclude regex
* options shared by throwable keywords such as <code>%ex</code>/<code>%xEx</code> and
* Spring Boot's <code>%wEx</code>.
* @param node keyword node holding the option list.
* @param packagingData whether the resulting formatter should also append packaging
* data (jar/module and version) after each frame; see
* {@link ThrowableFormatter.Builder#packagingData(boolean)}.
* @return throwable formatter configured from the options.
*/
public static ThrowableFormatter throwableFormatter(PatternKeyword node, boolean packagingData) {
return throwableFormatter(node, packagingData, false);
}
/**
* Like {@link #throwableFormatter(PatternKeyword, boolean)} but also able to build a
* root-cause-first formatter for <code>%rEx</code>/<code>%rootException</code>.
* @param node keyword node holding the option list.
* @param packagingData whether the resulting formatter should also append packaging
* data (jar/module and version) after each frame; see
* {@link ThrowableFormatter.Builder#packagingData(boolean)}.
* @param rootCauseFirst whether to print the root cause first; see
* {@link ThrowableFormatter.Builder#rootCauseFirst(boolean)}.
* @return throwable formatter configured from the options.
*/
public static ThrowableFormatter throwableFormatter(PatternKeyword node, boolean packagingData,
boolean rootCauseFirst) {
String depth = node.optOrNull(0);
int maxLines;
if (depth == null || depth.equalsIgnoreCase("full")) {
maxLines = Integer.MAX_VALUE;
}
else if (depth.equalsIgnoreCase("short")) {
maxLines = 0;
}
else {
maxLines = Integer.parseInt(depth);
}
var options = node.optionList();
List<String> excludes = options.size() > 1 ? options.subList(1, options.size()) : List.of();
return ThrowableFormatter.builder()
.maxLines(maxLines)
.excludes(excludes)
.packagingData(packagingData)
.rootCauseFirst(rootCauseFirst)
.build();
}
}
enum CallerInfoFormatter implements EventFormatter {
METHOD() {
@Override
protected void format(StringBuilder output, Caller info) {
output.append(info.methodName());
}
},
CLASS {
@Override
protected void format(StringBuilder output, Caller info) {
output.append(info.className());
}
},
FILE {
@Override
protected void format(StringBuilder output, Caller info) {
output.append(info.fileNameOrNull());
}
},
LINE {
@Override
protected void format(StringBuilder output, Caller info) {
output.append(info.lineNumber());
}
};
@Override
public void format(StringBuilder output, LogEvent event) {
var info = event.callerOrNull();
if (info != null) {
format(output, info);
}
}
protected abstract void format(StringBuilder output, Caller caller);
}
enum StandardKeywordFactory implements KeywordFactory {
DATE() {
@Override
protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
String pattern = node.opt(0, ISO8601_PATTERN);
if (pattern.equals(ISO8601_STR)) {
pattern = ISO8601_PATTERN;
}
ZoneId zoneId = node.opt(1, config.zoneId(), s -> ZoneId.of(s));
@Nullable Locale locale = node.optOrNull(2, s -> Locale.forLanguageTag(s));
var dtf = DateTimeFormatter.ofPattern(pattern).withZone(zoneId);
if (locale != null) {
dtf = dtf.withLocale(locale);
}
return LogFormatter.TimestampFormatter.of(dtf, isMillisOrCoarserPrecision(pattern));
}
},
/**
* <code>%r</code>, <code>%relative</code>: milliseconds elapsed since
* {@link PatternConfig#startTime()}.
*/
RELATIVE() {
@Override
protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
return new RelativeTimeFormatter(config.startTime());
}
},
/**
* <code>%lsn</code>: a counter local to this compiled keyword occurrence, starting at
* {@link PatternConfig#sequenceNumberStart()} and incrementing once per event
* formatted.
*/
LOCAL_SEQUENCE_NUMBER() {
@Override
protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
return new LocalSequenceNumberFormatter(new AtomicLong(config.sequenceNumberStart()));
}
},
LOGGER() {
@Override
protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
Integer length = node.optOrNull(0, Integer::parseInt);
if (length == null) {
return LogFormatter.builder().loggerName().build();
}
Abbreviator abbreviator = Abbreviator.of(length);
if (config.abbreviatorCache() == PatternConfig.CacheType.BASIC) {
abbreviator = Abbreviator.cache(abbreviator);
}
return new LoggerFormatter(abbreviator);
}
},
/**
* Logback/Log4j2 <code>%X</code>/<code>%mdc</code> style: with no key every key value
* as comma space separated <code>key=value</code> pairs, and with a key see
* {@link LogFormatter.Builder#keyValue(String, String)}. For RainbowGum's own
* percent-encoded style instead, see {@link #ENCODED_MDC}.
*/
MDC() {
@Override
protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
String key = node.optOrNull(0);
if (key == null) {
return LogbackKeyValuesFormatter.INSTANCE;
}
var kf = keyAndFallback(key);
return LogFormatter.builder().keyValue(kf.key(), kf.fallback()).build();
}
},
/**
* RainbowGum's percent-encoded (RFC 3986 URI query) MDC style - see
* {@link LogFormatter.Builder#keyValues()} and
* {@link LogFormatter.Builder#encodedKeyValue(String, String)} for the exact format.
* For Logback/Log4j2's <code>%X</code> style instead, see {@link #MDC}.
*/
ENCODED_MDC() {
@Override
protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
String key = node.optOrNull(0);
if (key == null) {
return LogFormatter.builder().keyValues().build();
}
var kf = keyAndFallback(key);
return LogFormatter.builder().encodedKeyValue(kf.key(), kf.fallback()).build();
}
},
PROPERTY() {
@Override
protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
String key = node.optOrNull(0);
if (key == null) {
return LogFormatter.builder().text("Property_HAS_NO_KEY").build();
}
var f = config.propertyFunction();
var v = f.apply(key);
if (v == null) {
return LogFormatter.noop();
}
return LogFormatter.builder().text(v).build();
}
},
/**
* This comes from Spring Boot %esb
*/
ENCLOSED_SQUARE_BRACKET() {
@Override
protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
String key = node.optOrNull(0);
if (key == null) {
return LogFormatter.noop();
}
var f = config.propertyFunction();
var v = f.apply(key);
if (v == null || v.isEmpty()) {
return LogFormatter.noop();
}
return LogFormatter.builder().text("[").text(v).text("]").build();
}
},
/**
* <code>%ex</code>, <code>%ex{full}</code>, <code>%ex{short}</code>,
* <code>%ex{N}</code>, <code>%ex{N, regex1, regex2, ...}</code>.
*/
THROWABLE() {
@Override
protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
return PatternFormatterFactory.throwableFormatter(node, false);
}
@Override
public boolean isExceptionFormatter() {
return true;
}
},
/**
* <code>%xEx</code>, <code>%xEx{full}</code>, <code>%xEx{short}</code>,
* <code>%xEx{N}</code>, <code>%xEx{N, regex1, regex2, ...}</code>. Same options as
* {@link #THROWABLE} but also appends packaging data (jar/module and version) after
* each frame, similar to logback's <code>ExtendedThrowableProxyConverter</code>.
*/
EXTENDED_THROWABLE() {
@Override
protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
return PatternFormatterFactory.throwableFormatter(node, true);
}
@Override
public boolean isExceptionFormatter() {
return true;
}
},
/**
* <code>%rEx</code>, <code>%rEx{full}</code>, <code>%rEx{short}</code>,
* <code>%rEx{N}</code>, <code>%rEx{N, regex1, regex2, ...}</code>. Same options as
* {@link #EXTENDED_THROWABLE} (including packaging data) but the root cause prints
* first and each wrapper prints afterward, similar to logback's
* <code>RootCauseFirstThrowableProxyConverter</code>.
*/
ROOT_EXCEPTION() {
@Override
protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
return PatternFormatterFactory.throwableFormatter(node, true, true);
}
@Override
public boolean isExceptionFormatter() {
return true;
}
};
static final String ISO8601_STR = "ISO8601";
static final String ISO8601_PATTERN = "yyyy-MM-dd HH:mm:ss,SSS";
record KeyAndFallback(String key, @Nullable String fallback) {
}
/**
* Splits a <code>%X{key}</code>/<code>%mdc{key}</code> option on <code>:-</code>,
* Logback's syntax for a default value (e.g. <code>%X{requestId:-none}</code>).
*/
static KeyAndFallback keyAndFallback(String option) {
String[] s = option.split(":-", 0);
if (s.length == 2) {
return new KeyAndFallback(s[0], s[1]);
}
return new KeyAndFallback(option, null);
}
/**
* Whether a {@link DateTimeFormatter} pattern's finest resolution is milliseconds or
* coarser, i.e. it never renders sub-millisecond digits. Used to decide whether the
* formatted string can be safely cached and reused for events landing in the same
* millisecond (see
* {@link LogFormatter.TimestampFormatter#of(DateTimeFormatter, boolean)}).
*/
static boolean isMillisOrCoarserPrecision(String pattern) {
boolean literal = false;
int consecutiveS = 0;
for (int i = 0; i < pattern.length(); i++) {
char c = pattern.charAt(i);
if (c == '\'') {
literal = !literal;
consecutiveS = 0;
continue;
}
if (literal) {
continue;
}
if (c == 'n' || c == 'N') {
return false;
}
if (c == 'S') {
consecutiveS++;
if (consecutiveS > 3) {
return false;
}
}
else {
consecutiveS = 0;
}
}
return true;
}
// MICROS, //
// THREAD, //
// LEVEL, //
// LOGGER, //
// MESSAGE, //
// MDC, //
// THROWABLE, //
// LINESEP;
@Override
public LogFormatter create(PatternConfig config, PatternKeyword node) {
var formatter = _create(config, node);
var formatInfo = node.padding();
return PadFormatter.of(formatter, formatInfo);
}
protected abstract LogFormatter _create(PatternConfig config, PatternKeyword node);
record LoggerFormatter(Abbreviator abbreviator) implements LogFormatter.EventFormatter {
@Override
public void format(StringBuilder output, LogEvent event) {
String out = abbreviator.abbreviate(event.loggerName());
output.append(out);
}
}
record RelativeTimeFormatter(Instant startTime) implements LogFormatter.EventFormatter {
@Override
public void format(StringBuilder output, LogEvent event) {
long millis = Duration.between(startTime, event.timestamp()).toMillis();
output.append(millis);
}
}
record LocalSequenceNumberFormatter(AtomicLong counter) implements LogFormatter.EventFormatter {
@Override
public void format(StringBuilder output, LogEvent event) {
output.append(counter.getAndIncrement());
}
}
}
enum BareCompositeFactory implements CompositeFactory {
BARE() {
@Override
public LogFormatter create(PatternConfig config, PatternKeyword node, @Nullable LogFormatter child) {
if (child == null) {
child = new LogFormatter.StaticFormatter("");
}
return PadFormatter.of(child, node.padding());
}
};
}
final class ANSIConstants {
final static String ESC_START = "\033[";
final static String ESC_END = "m";
final static String BOLD = "1;";
final static String FAINT = "2;";
final static String RESET = "0;";
final static String BLACK_FG = "30";
final static String RED_FG = "31";
final static String GREEN_FG = "32";
final static String YELLOW_FG = "33";
final static String BLUE_FG = "34";
final static String MAGENTA_FG = "35";
final static String CYAN_FG = "36";
final static String WHITE_FG = "37";
final static String DEFAULT_FG = "39";
final static String GRAY_FG = BOLD + BLACK_FG; // Logback is a little weird about gray
final static String SET_DEFAULT_COLOR = ESC_START + RESET + DEFAULT_FG + ESC_END;
/*
* Spring Boot colors
*/
final static String BRIGHT_BLACK = "90";
final static String BRIGHT_RED = "91";
final static String BRIGHT_GREEN = "92";
final static String BRIGHT_YELLOW = "93";
final static String BRIGHT_BLUE = "94";
final static String BRIGHT_MAGENTA = "95";
final static String BRIGHT_CYAN = "96";
final static String BRIGHT_WHITE = "97";
}
/*
* Colors the child by the event's level. With no options the colors are Logback's: error
* bold red, warn red, info blue, everything else the default color. Options are Log4j2
* style LEVEL=style pairs, for example {ERROR=red bold, INFO=blue bold}, where a style is
* a color and optionally bold or faint; levels not listed keep their default.
*/
record HighlightFormatter(@Nullable LogFormatter child, String error, String warn, String info, String debug,
String trace) implements LogFormatter.EventFormatter {
static final HighlightFormatter LOGBACK_COLORS = new HighlightFormatter(null, BOLD + RED_FG, RED_FG, BLUE_FG,
DEFAULT_FG, DEFAULT_FG);
HighlightFormatter(@Nullable LogFormatter child) {
this(child, LOGBACK_COLORS.error, LOGBACK_COLORS.warn, LOGBACK_COLORS.info, LOGBACK_COLORS.debug,
LOGBACK_COLORS.trace);
}
@Override
public void format(StringBuilder output, LogEvent event) {
/*
* TODO should a null child be a noop? Need to see what logback does for compat.
*/
String code = switch (event.level()) {
case ERROR -> error;
case WARNING -> warn;
case INFO -> info;
case DEBUG -> debug;
case TRACE -> trace;
default -> DEFAULT_FG;
};
output.append(ANSIConstants.ESC_START);
output.append(code);
output.append(ANSIConstants.ESC_END);
if (child != null) {
child.format(output, event);
}
output.append(ANSIConstants.SET_DEFAULT_COLOR);
}
static HighlightFormatter of(@Nullable LogFormatter child, List<String> options) {
String error = LOGBACK_COLORS.error;
String warn = LOGBACK_COLORS.warn;
String info = LOGBACK_COLORS.info;
String debug = LOGBACK_COLORS.debug;
String trace = LOGBACK_COLORS.trace;
for (String option : options) {
if (option.isBlank()) {
continue;
}
int eq = option.indexOf('=');
if (eq < 0) {
throw new IllegalArgumentException(
"highlight option '" + option.strip() + "' should be LEVEL=style, for example ERROR=red bold");
}
String level = option.substring(0, eq).strip().toUpperCase(Locale.ROOT);
String code = parseStyle(option.substring(eq + 1));
switch (level) {
case "ERROR" -> error = code;
case "WARN", "WARNING" -> warn = code;
case "INFO" -> info = code;
case "DEBUG" -> debug = code;
case "TRACE" -> trace = code;
default -> throw new IllegalArgumentException("highlight option '" + option.strip()
+ "' has an unknown level. Valid levels: ERROR, WARN, INFO, DEBUG, TRACE");
}
}
return new HighlightFormatter(child, error, warn, info, debug, trace);
}
/*
* A style is space separated words: at most one color plus bold and/or faint.
*/
static String parseStyle(String style) {
String attributes = "";
String color = null;
for (String word : words(style)) {
String w = word.toLowerCase(Locale.ROOT);
switch (w) {
case "bold" -> attributes += BOLD;
case "faint" -> attributes += FAINT;
default -> {
if (color != null) {
throw new IllegalArgumentException(
"highlight style '" + style.strip() + "' has more than one color");
}
color = colorCode(w, style);
}
}
}
return attributes + (color == null ? DEFAULT_FG : color);
}
private static List<String> words(String text) {
var words = new java.util.ArrayList<String>();
int start = -1;
for (int i = 0; i <= text.length(); i++) {
boolean space = i == text.length() || Character.isWhitespace(text.charAt(i));
if (space && start >= 0) {
words.add(text.substring(start, i));
start = -1;
}
else if (!space && start < 0) {
start = i;
}
}
return words;
}
private static String colorCode(String color, String style) {
return switch (color) {
case "black" -> BLACK_FG;
case "red" -> RED_FG;
case "green" -> GREEN_FG;
case "yellow" -> YELLOW_FG;
case "blue" -> BLUE_FG;
case "magenta" -> MAGENTA_FG;
case "cyan" -> CYAN_FG;
case "white" -> WHITE_FG;
case "default" -> DEFAULT_FG;
case "bright_black" -> BRIGHT_BLACK;
case "bright_red" -> BRIGHT_RED;
case "bright_green" -> BRIGHT_GREEN;
case "bright_yellow" -> BRIGHT_YELLOW;
case "bright_blue" -> BRIGHT_BLUE;
case "bright_magenta" -> BRIGHT_MAGENTA;
case "bright_cyan" -> BRIGHT_CYAN;
case "bright_white" -> BRIGHT_WHITE;
default -> throw new IllegalArgumentException("highlight style '" + style.strip()
+ "' has an unknown word '" + color + "'. Use a color (black, red, green, yellow, blue, magenta, "
+ "cyan, white, default, or bright_ variants) and optionally bold or faint");
};
}
}
/*
* From Spring Boot
*/
record ClrLevelFormatter(@Nullable LogFormatter child) implements LogFormatter.EventFormatter {
@Override
public void format(StringBuilder output, LogEvent event) {
/*
* TODO should a null child be a noop? Need to see what logback does for compat.
*/
var level = event.level();
String code = levelToANSI(level);
output.append(ANSIConstants.ESC_START);
output.append(code);
output.append(ANSIConstants.ESC_END);
if (child != null) {
child.format(output, event);
}
output.append(ANSIConstants.SET_DEFAULT_COLOR);
}
private static String levelToANSI(Level level) {
return switch (level) {
case ERROR -> RED_FG;
case WARNING -> YELLOW_FG;
case INFO -> GREEN_FG;
case DEBUG -> GREEN_FG;
case TRACE -> GREEN_FG;
default -> DEFAULT_FG;
};
}
}
record ClrStaticFormatter(@Nullable LogFormatter child, String code) implements LogFormatter.EventFormatter {
@Override
public void format(StringBuilder output, LogEvent event) {
output.append(ANSIConstants.ESC_START);
output.append(code);
output.append(ANSIConstants.ESC_END);
if (child != null) {
child.format(output, event);
}
output.append(ANSIConstants.SET_DEFAULT_COLOR);
}
}
enum HighlightCompositeFactory implements CompositeFactory {
HIGHTLIGHT() {
@Override
public LogFormatter create(PatternConfig config, PatternKeyword node, @Nullable LogFormatter child) {
if (config.ansiDisabled()) {
if (child == null) {
return LogFormatter.noop();
}
return child;
}
return HighlightFormatter.of(child, node.optionList());
}
},
/**
* This style come from Spring Boot.
*/
CLR() {
@Override
public LogFormatter create(PatternConfig config, PatternKeyword node, @Nullable LogFormatter child) {
if (config.ansiDisabled()) {
if (child == null) {
return LogFormatter.noop();
}
return child;
}
String color = node.optOrNull(0, HighlightCompositeFactory::clrParseColor);
if (color == null) {
return new ClrLevelFormatter(child);
}
return new ClrStaticFormatter(child, color);
}
};
static String clrParseColor(String color) {
color = color.toLowerCase(Locale.ROOT);
return switch (color) {
case "black" -> BLACK_FG;
case "white" -> WHITE_FG;
case "blue" -> BLUE_FG;
case "cyan" -> CYAN_FG;
case "faint" -> FAINT + DEFAULT_FG;
case "green" -> GREEN_FG;
case "magenta" -> MAGENTA_FG;
case "red" -> RED_FG;
case "yellow" -> YELLOW_FG;
case "bright_black" -> BRIGHT_BLACK;
case "bright_red" -> BRIGHT_RED;
case "bright_green" -> BRIGHT_GREEN;
case "bright_yellow" -> BRIGHT_YELLOW;
case "bright_blue" -> BRIGHT_BLUE;
case "bright_magenta" -> BRIGHT_MAGENTA;
case "bright_cyan" -> BRIGHT_CYAN;
case "bright_white" -> BRIGHT_WHITE;
default -> throw new IllegalArgumentException("Bad color:" + color);
};
}
}
/**
* This style of colors largely comes from logback
*/
enum ColorCompositeFactory implements CompositeFactory {
BLACK(BLACK_FG, "black"), //
RED(RED_FG, "red"), //
GREEN(GREEN_FG, "green"), //
YELLOW(YELLOW_FG, "yellow"), //
BLUE(BLUE_FG, "blue"), //
MAGENTA(MAGENTA_FG, "magenta"), //
CYAN(CYAN_FG, "cyan"), //
WHITE(WHITE_FG, "white"), //
GRAY(GRAY_FG, "gray"), //
BOLD_RED(BOLD + RED_FG, "boldRed"), //
BOLD_GREEN(BOLD + GREEN_FG, "boldGreen"), //
BOLD_YELLOW(BOLD + YELLOW_FG, "boldYellow"), //
BOLD_BLUE(BOLD + BLUE_FG, "boldBlue"), //
BOLD_MAGENTA(BOLD + MAGENTA_FG, "boldMagenta"), //
BOLD_CYAN(BOLD + CYAN_FG, "boldCyan"), //
BOLD_WHITE(BOLD + WHITE_FG, "boldWhite");
private final String fg;
private final String name;
private ColorCompositeFactory(String fg, String name) {
this.fg = fg;
this.name = name;
}
@Override
public LogFormatter create(PatternConfig config, PatternKeyword node, @Nullable LogFormatter child) {
return create(node, child, !config.ansiDisabled());
}
LogFormatter create(PatternKeyword node, @Nullable LogFormatter child, boolean ansi) {
var b = LogFormatter.builder();
if (ansi) {
b.text(ANSIConstants.ESC_START);
b.text(fg);
b.text(ANSIConstants.ESC_END);
}
if (child != null) {
b.add(child);
}
if (ansi) {
b.text(ANSIConstants.SET_DEFAULT_COLOR);
}
return b.build();
}
public String alias() {
return name;
}
}
record PadFormatter(Padding padding, LogFormatter formatter) implements LogFormatter.EventFormatter {
public static LogFormatter of(LogFormatter formatter, @Nullable Padding padding) {
if (padding == null) {
return formatter;
}
if (formatter instanceof LogFormatter.StaticFormatter sf) {
StringBuilder b = new StringBuilder();
padding.format(b, sf.content());
return new LogFormatter.StaticFormatter(b.toString());
}
return new PadFormatter(padding, formatter);
}
@Override
public void format(StringBuilder output, LogEvent event) {
/*
* TODO revisit formatting without using a temp buffer
*/
StringBuilder buf = new StringBuilder();
formatter.format(buf, event);
padding.format(output, buf);
}
}