TTLL.java
package io.jstach.rainbowgum.format;
import io.jstach.rainbowgum.LogFormatter;
import io.jstach.rainbowgum.LogFormatter.LevelFormatter;
import io.jstach.rainbowgum.LogFormatter.TimestampFormatter;
import io.jstach.rainbowgum.annotation.CaseChanging;
import io.jstach.rainbowgum.annotation.EnumAlias;
/**
* The choices for each part of the TTLL (Time, Thread, Level, Logger) format, the default
* text format, configured with {@link TTLLFormatterBuilder} or properties of the
* {@value #SCHEMA} encoder. Property values are the lowercase constant names, for
* example: <pre>
* logging.appender.console.encoder.ttll.timestamp=iso
* logging.appender.console.encoder.ttll.keyValues=json5
* </pre> The defaults produce the classic layout with the event's key values, percent
* encoded, in braces after the logger name:
* <code>12:00:00.123 [main] INFO com.example.App {requestId=42} - hello</code>. When an
* event has no key values the braces are left out, or written as <code>{}</code>,
* according to {@link KeyValuesWhenEmpty}, and {@link KeyValuesFormat#NONE} leaves the
* key values out entirely.
* <p>
* Every part also accepts <code>default</code>, which restores its default without naming
* it, and <code>true</code> and <code>false</code> to show or hide it; the
* {@link io.jstach.rainbowgum.annotation.EnumAlias} on each constant lists them.
*/
public sealed interface TTLL permits TTLLFormatter {
/**
* URI scheme of the TTLL encoder, the default encoder when no other is configured.
*/
String SCHEMA = "ttll";
/**
* Time part. A property value that is not one of these names is used as a
* {@link java.time.format.DateTimeFormatter} pattern in UTC, like slf4j-simple's
* <code>dateTimeFormat</code>.
*/
@CaseChanging
enum TimestampFormat {
/**
* <code>HH:mm:ss.SSS</code> in UTC. The default.
*/
@EnumAlias({ "default", "true" })
TTLL {
@Override
public LogFormatter formatter() {
return TimestampFormatter.of();
}
},
/**
* ISO 8601 UTC instant with milliseconds, <code>2026-10-05T12:00:00.123Z</code>.
*/
ISO {
@Override
public LogFormatter formatter() {
return TimestampFormatter.ofISO();
}
},
/**
* No time.
*/
@EnumAlias("false")
NONE {
@Override
public LogFormatter formatter() {
return LogFormatter.noop();
}
};
/**
* The formatter for this choice.
* @return formatter.
*/
public abstract LogFormatter formatter();
static TimestampFormat parse(String value) {
return switch (value.strip().toLowerCase(java.util.Locale.ROOT)) {
case "default", "true" -> TTLL;
case "false" -> NONE;
default -> io.jstach.rainbowgum.LogProperty.enumValue(TimestampFormat.class, value, "true", "false",
"default");
};
}
}
/**
* Thread part, written in square brackets.
*/
@CaseChanging
enum ThreadFormat {
/**
* Thread name. The default.
*/
@EnumAlias({ "default", "true" })
NAME {
@Override
public LogFormatter formatter() {
return LogFormatter.builder().threadName().build();
}
},
/**
* Thread id.
*/
ID {
@Override
public LogFormatter formatter() {
return LogFormatter.builder().threadId().build();
}
},
/**
* No thread.
*/
@EnumAlias("false")
NONE {
@Override
public LogFormatter formatter() {
return LogFormatter.noop();
}
};
/**
* The formatter for this choice.
* @return formatter.
*/
public abstract LogFormatter formatter();
static ThreadFormat parse(String value) {
return switch (value.strip().toLowerCase(java.util.Locale.ROOT)) {
case "default", "true" -> NAME;
case "false" -> NONE;
default ->
io.jstach.rainbowgum.LogProperty.enumValue(ThreadFormat.class, value, "true", "false", "default");
};
}
}
/**
* Level part.
*/
@CaseChanging
enum LevelFormat {
/**
* Upper case, right padded to five characters so messages line up. The default.
*/
@EnumAlias({ "default", "true" })
PADDED {
@Override
public LogFormatter formatter() {
return LevelFormatter.ofRightPadded();
}
},
/**
* Upper case without padding.
*/
PLAIN {
@Override
public LogFormatter formatter() {
return LevelFormatter.of();
}
},
/**
* Upper case in square brackets, like slf4j-simple's
* <code>levelInBrackets</code>: <code>[INFO]</code>.
*/
BRACKETED {
@Override
public LogFormatter formatter() {
return LogFormatter.builder().text("[").add(LevelFormatter.of()).text("]").build();
}
},
/**
* No level.
*/
@EnumAlias("false")
NONE {
@Override
public LogFormatter formatter() {
return LogFormatter.noop();
}
};
/**
* The formatter for this choice.
* @return formatter.
*/
public abstract LogFormatter formatter();
static LevelFormat parse(String value) {
return switch (value.strip().toLowerCase(java.util.Locale.ROOT)) {
case "default", "true" -> PADDED;
case "false" -> NONE;
default ->
io.jstach.rainbowgum.LogProperty.enumValue(LevelFormat.class, value, "true", "false", "default");
};
}
}
/**
* Logger name part.
*/
@CaseChanging
enum LoggerFormat {
/**
* Full logger name. The default.
*/
@EnumAlias({ "default", "true" })
FULL {
@Override
public LogFormatter formatter() {
return LogFormatter.builder().loggerName().build();
}
},
/**
* Only the last component of the name, like slf4j-simple's
* <code>showShortLogName</code>: <code>App</code> for
* <code>com.example.App</code>.
*/
SHORT {
@Override
public LogFormatter formatter() {
return ShortLoggerNameFormatter.INSTANCE;
}
},
/**
* No logger name.
*/
@EnumAlias("false")
NONE {
@Override
public LogFormatter formatter() {
return LogFormatter.noop();
}
};
/**
* The formatter for this choice.
* @return formatter.
*/
public abstract LogFormatter formatter();
static LoggerFormat parse(String value) {
return switch (value.strip().toLowerCase(java.util.Locale.ROOT)) {
case "default", "true" -> FULL;
case "false" -> NONE;
default ->
io.jstach.rainbowgum.LogProperty.enumValue(LoggerFormat.class, value, "true", "false", "default");
};
}
}
/**
* Key values part, written in braces after the logger name; see
* {@link KeyValuesWhenEmpty} for events with no key values. {@link #NONE} leaves out
* the key values and their braces.
*/
@CaseChanging
enum KeyValuesFormat {
/**
* Key values and their braces are left out.
*/
@EnumAlias("false")
NONE {
@Override
public LogFormatter formatter() {
return LogFormatter.noop();
}
},
/**
* Percent encoded like a URI query:
* <code>requestId=42&user=Ada%20Lovelace</code>. The default.
*/
@EnumAlias({ "true", "default" })
PERCENT {
@Override
public LogFormatter formatter() {
return new KeyValuesFormatterBuilder().format(KeyValuesFormatterBuilder.Format.PERCENT).build();
}
},
/**
* JSON: <code>{"requestId":"42","user":"Ada Lovelace"}</code>. Null values are
* written as {@code null}.
*/
JSON {
@Override
public LogFormatter formatter() {
return new KeyValuesFormatterBuilder().format(KeyValuesFormatterBuilder.Format.JSON).buildUnbraced();
}
},
/**
* JSON5, with valid identifier keys left unquoted:
* <code>{requestId:"42",user:"Ada Lovelace"}</code>. Null values are written as
* {@code null}.
*/
JSON5 {
@Override
public LogFormatter formatter() {
return new KeyValuesFormatterBuilder().format(KeyValuesFormatterBuilder.Format.JSON5).buildUnbraced();
}
};
/**
* The formatter for this choice, without the enclosing braces supplied by TTLL.
* @return formatter.
*/
public abstract LogFormatter formatter();
static KeyValuesFormat parse(String value) {
return switch (value.strip().toLowerCase(java.util.Locale.ROOT)) {
case "true", "default" -> PERCENT;
case "false" -> NONE;
default -> io.jstach.rainbowgum.LogProperty.enumValue(KeyValuesFormat.class, value, "true", "false",
"default");
};
}
}
/**
* Whether the key values braces are written when an event has no key values.
*/
@CaseChanging
enum KeyValuesWhenEmpty {
/**
* Left out for percent encoding and JSON5; <code>{}</code> for JSON, since
* whatever reads JSON key values expects an object on every line. The default.
*/
@EnumAlias("default")
AUTO,
/**
* Left out. Not allowed with {@link KeyValuesFormat#JSON}, whose readers expect
* an object on every line.
*/
@EnumAlias("false")
OMIT,
/**
* Always written, <code>{}</code> when there are no key values.
*/
@EnumAlias("true")
SHOW;
static KeyValuesWhenEmpty parse(String value) {
return switch (value.strip().toLowerCase(java.util.Locale.ROOT)) {
case "default" -> AUTO;
case "false" -> OMIT;
case "true" -> SHOW;
default -> io.jstach.rainbowgum.LogProperty.enumValue(KeyValuesWhenEmpty.class, value, "true", "false",
"default");
};
}
}
/**
* Whether to color, see {@link ColorTheme} for which colors. Color detection uses
* {@link AnsiSupport#isAnsiSupported()}, which only knows about the console, so the
* default encoder of outputs that are not a console is {@link #OFF} unless set. As a
* property <code>true</code> and <code>false</code> are aliases for
* <code>default</code> and <code>off</code>.
*/
@CaseChanging
enum ColorMode {
/**
* No color.
*/
@EnumAlias("false")
OFF,
/**
* The theme, or {@link ColorTheme#RAINBOWGUM} when no theme is set, if ANSI is
* detected. The default.
*/
@EnumAlias("true")
DEFAULT,
/**
* The theme if one is set and ANSI is detected; no color without a theme.
*/
DETECT,
/**
* The theme, or {@link ColorTheme#RAINBOWGUM} when no theme is set, regardless of
* ANSI detection.
*/
FORCE;
static ColorMode parse(String value) {
return switch (value.strip().toLowerCase(java.util.Locale.ROOT)) {
case "true" -> DEFAULT;
case "false" -> OFF;
default -> io.jstach.rainbowgum.LogProperty.enumValue(ColorMode.class, value, "true", "false");
};
}
}
/**
* ANSI color theme, the colors used when {@link ColorMode} decides to color. The
* built in themes are the constants of this interface; others are made with
* {@link #builder(String)}. A theme put in the
* {@link io.jstach.rainbowgum.ServiceRegistry} as a {@code ColorTheme} under its name
* can be chosen by name with the theme property.
*/
sealed interface ColorTheme permits PaletteColorTheme, NamedColorTheme {
/**
* The Rainbow Gum colors, the same as the pattern encoder's default: cyan time,
* faint thread and key values, level highlighted (error and warn bold red, info
* bold blue) and magenta logger name. Used when no theme is set. Property value
* <code>rainbowgum</code> or <code>default</code>.
*/
ColorTheme RAINBOWGUM = PaletteColorTheme.RAINBOWGUM;
/**
* Spring Boot's console colors: faint time and thread, level colored by severity
* (error red, warn yellow, the rest green) and cyan logger name. Property value
* <code>spring</code>.
*/
ColorTheme SPRING = PaletteColorTheme.SPRING;
/**
* Atom One Dark's colors in 24 bit color, so they show the same in any terminal
* that supports it: cyan time, gray thread and key values, magenta logger name,
* and level bold red, yellow, or blue (error, warn, info) with debug and trace
* gray. Meant for a dark terminal background. Property value
* <code>one_dark</code>.
*/
ColorTheme ONE_DARK = PaletteColorTheme.ONE_DARK;
/**
* IntelliJ Darcula's colors in 24 bit color: blue time, gray thread and key
* values, orange logger name, and level bold red, yellow, or green (error, warn,
* info) with debug and trace gray. Meant for a dark terminal background. Property
* value <code>darcula</code>.
*/
ColorTheme DARCULA = PaletteColorTheme.DARCULA;
/**
* The theme's name, which is its property value.
* @return name like <code>one_dark</code>.
*/
String name();
/**
* Starts a theme. Parts not set are not colored.
* @param name theme name: lowercase letters, digits, and underscores, starting
* with a letter, and not the name of a built in theme or <code>default</code>.
* @return builder.
* @throws IllegalArgumentException if the name is not allowed.
*/
static Builder builder(String name) {
return new Builder(PaletteColorTheme.requireThemeName(name));
}
/**
* Builds a {@link ColorTheme}. Colors are ANSI SGR parameters, the part of an
* escape sequence between <code>ESC[</code> and <code>m</code>, such as
* <code>36</code> for cyan, <code>1;31</code> for bold red, or
* <code>38;2;198;120;221</code> for a 24 bit color.
*/
final class Builder {
private final String name;
private String timestamp = "";
private String thread = "";
private String logger = "";
private String keyValues = "";
private String error = "";
private String warn = "";
private String info = "";
private String debug = "";
private String trace = "";
private Builder(String name) {
this.name = name;
}
/**
* Starts from another theme's colors.
* @param theme theme to copy.
* @return this.
*/
public Builder from(ColorTheme theme) {
var p = PaletteColorTheme.paletteOf(theme);
this.timestamp = p.timestamp();
this.thread = p.thread();
this.logger = p.logger();
this.keyValues = p.keyValues();
this.error = p.error();
this.warn = p.warn();
this.info = p.info();
this.debug = p.debug();
this.trace = p.trace();
return this;
}
/**
* Timestamp color.
* @param color SGR parameters.
* @return this.
*/
public Builder timestamp(String color) {
this.timestamp = color;
return this;
}
/**
* Thread color.
* @param color SGR parameters.
* @return this.
*/
public Builder thread(String color) {
this.thread = color;
return this;
}
/**
* Logger name color.
* @param color SGR parameters.
* @return this.
*/
public Builder logger(String color) {
this.logger = color;
return this;
}
/**
* Key values color.
* @param color SGR parameters.
* @return this.
*/
public Builder keyValues(String color) {
this.keyValues = color;
return this;
}
/**
* Color of a level.
* @param level error, warning, info, debug, or trace.
* @param color SGR parameters.
* @return this.
* @throws IllegalArgumentException for any other level.
*/
public Builder level(System.Logger.Level level, String color) {
switch (level) {
case ERROR -> this.error = color;
case WARNING -> this.warn = color;
case INFO -> this.info = color;
case DEBUG -> this.debug = color;
case TRACE -> this.trace = color;
default -> throw new IllegalArgumentException(
"Level " + level + " has no color. Use error, warning, info, debug, or trace.");
}
return this;
}
/**
* Builds the theme.
* @return theme.
*/
public ColorTheme build() {
return new PaletteColorTheme(name,
new Palette(timestamp, thread, logger, keyValues, error, warn, info, debug, trace));
}
}
}
}