PatternRegistry.java
package io.jstach.rainbowgum.pattern.format;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.jspecify.annotations.Nullable;
import io.jstach.rainbowgum.LogFormatter;
import io.jstach.rainbowgum.annotation.CaseChanging;
import io.jstach.rainbowgum.pattern.format.DefaultPatternRegistry.CustomConverterKey;
import io.jstach.rainbowgum.pattern.format.PatternFormatterFactory.KeywordFactory;
/**
* Pattern registry to register custom keywords.
*/
public sealed interface PatternRegistry {
/*
* TODO this is a mutable component!
*/
/**
* Gets the formatter factory for the pattern key.
* @param key keyword.
* @return factory or null.
*/
public @Nullable PatternFormatterFactory getOrNull(String key);
/**
* Registers a pattern key with a formatter factory. You may replace the built-in
* pattern formatters with {@link KeywordKey} or {@link ColorKey}s or create a new key
* with {@link PatternKey#of(String)}.
* @param key pattern keys.
* @param factory factory to create formatters from pattern keywords.
*/
public void register(PatternKey key, PatternFormatterFactory factory);
/**
* Convenience to register factories that have keys associated with them by
* implementing {@link PatternKeyProvider}.
* @param <P> pattern formatter that has keys associated with it.
* @param factory factory that also has a key.
* @see #register(PatternKey, PatternFormatterFactory)
*/
default <P extends PatternKeyProvider & PatternFormatterFactory> void register(P factory) {
register(factory.key(), factory);
}
/**
* Convenience function for {@link #register(PatternKey, PatternFormatterFactory)} as
* keyword factory is a lambda.
* @param key pattern keys.
* @param factory factory create formatter from a simple keyword.
*/
default void keyword(PatternKey key, KeywordFactory factory) {
register(key, factory);
}
/**
* Creates a pattern registry with the defaults.
* @return default registry.
*/
public static PatternRegistry of() {
return DefaultPatternRegistry.registerDefaults(new DefaultPatternRegistry());
}
/**
* Pattern keywords.
*
* @see KeywordKey
* @see ColorKey
*/
public sealed interface PatternKey {
/**
* Keyword names.
* @return keywords should not be empty.
*/
List<String> aliases();
/**
* Creates a pattern key of one key name.
* @param key key name.
* @return pattern key.
*/
public static PatternKey of(String key) {
return new CustomConverterKey(List.of(key));
}
/**
* Creates a pattern key of one key and aliases.
* @param key key name.
* @param aliases other names.
* @return pattern key.
*/
public static PatternKey of(String key, String... aliases) {
List<String> keys = new ArrayList<>();
keys.add(Objects.requireNonNull(key));
for (var a : aliases) {
keys.add(Objects.requireNonNull(a));
}
return new CustomConverterKey(List.copyOf(keys));
}
}
/**
* Implement this interface along with the formatter for easier registration.
*
* @see PatternRegistry#register(PatternKeyProvider)
*/
public interface PatternKeyProvider {
/**
* Key
* @return key.
*/
PatternKey key();
}
/**
* Built-in supported pattern keys.
*/
@CaseChanging
/*
* aliases is always assigned via List.of(...) in the constructor - genuinely
* immutable, just not through a type that says so statically.
*/
@SuppressWarnings("ImmutableEnumChecker")
enum KeywordKey implements PatternKey {
/**
* <a href="https://logback.qos.ch/manual/layouts.html#date">Date keywords</a>
*/
DATE("d", "date"), //
/**
* <a href="https://logback.qos.ch/manual/layouts.html#micros">Micros keywords</a>
*/
MICROS("ms", "micros"), //
/**
* <a href="https://logback.qos.ch/manual/layouts.html#relative">Relative time
* keywords</a>. Milliseconds elapsed since {@link PatternConfig#startTime()}.
*/
RELATIVE("r", "relative"), //
/**
* <a href="https://logback.qos.ch/manual/layouts.html#file">File</a>
*/
FILE("f", "file"),
/**
* <a href="https://logback.qos.ch/manual/layouts.html#line">Line no</a>
*/
LINE("L", "line"),
/**
* <a href="https://logback.qos.ch/manual/layouts.html#class">Calling class</a>
*/
CLASS("C", "class"),
/**
* <a href="https://logback.qos.ch/manual/layouts.html#line">Line no</a>
*/
METHOD("M", "method"),
/**
* <a href="https://logback.qos.ch/manual/layouts.html#thread">Thread keywords</a>
*/
THREAD("t", "thread"), //
/**
* <a href="https://logback.qos.ch/manual/layouts.html#level">Level keywords</a>
*/
LEVEL("level", "le", "p"), //
/**
* <a href="https://logback.qos.ch/manual/layouts.html#logger">Logger keywords</a>
*/
LOGGER("lo", "logger", "c"), //
/**
* <a href="https://logback.qos.ch/manual/layouts.html#message">Message
* keywords</a>
*/
MESSAGE("m", "msg", "message"), //
/**
* <a href="https://logback.qos.ch/manual/layouts.html#mdc">MDC keywords</a>
*/
MDC("X", "mdc"), //
/**
* RainbowGum's own percent-encoded (RFC 3986 URI query) MDC style, e.g.
* <code>key1=value1&key2=value2</code> - unlike {@link #MDC}, which matches
* Logback/Log4j2's <code>%X</code> output exactly.
*/
ENCODED_MDC("encodedMdc"), //
/**
* <a href="https://logback.qos.ch/manual/layouts.html#mdc">Property keywords</a>
* @see PatternConfig#propertyFunction()
*/
PROPERTY("property"), //
/**
* <a href="https://logback.qos.ch/manual/layouts.html#ex">Throwable keywords</a>
*/
THROWABLE("ex", "exception", "throwable"), //
/**
* <a href= "https://logback.qos.ch/manual/layouts.html#xThrowable">Extended
* throwable keywords</a>. Same options as {@link #THROWABLE} but also appends
* packaging data (jar/module and version) after each frame.
*/
EXTENDED_THROWABLE("xEx", "xException", "xThrowable"), //
/**
* <a href="https://logback.qos.ch/manual/layouts.html#rootException">Root
* exception keywords</a>. Same options as {@link #EXTENDED_THROWABLE} (including
* packaging data) but the root cause prints first and each wrapper prints
* afterward, captioned "Wrapped by: " instead of "Caused by: ".
*/
ROOT_EXCEPTION("rEx", "rootException"), //
/**
* <a href="https://logback.qos.ch/manual/layouts.html#nopex">No exception
* keywords</a>. Renders nothing but, like {@link #THROWABLE},
* {@link #EXTENDED_THROWABLE}, and {@link #ROOT_EXCEPTION}, counts as the pattern
* having addressed exception rendering - suppressing the automatic exception
* formatter that is otherwise appended to any pattern that mentions none of these
* keywords.
*/
NO_EXCEPTION("nopex", "nopexception"), //
/**
* <a href="https://logback.qos.ch/manual/layouts.html#lsn">Local sequence number
* keywords</a>. Unlike logback's <code>%sn</code>/ <code>%sequenceNumber</code>
* (which requires configuring a context-wide
* <code>SequenceNumberGenerator</code>, a feature not implemented here) this is a
* simple counter local to each compiled use of <code>%lsn</code>, starting at
* {@link PatternConfig#sequenceNumberStart()} and incrementing once per event
* formatted.
*/
LOCAL_SEQUENCE_NUMBER("lsn"), //
/**
* <a href="https://logback.qos.ch/manual/layouts.html#newline">Newline
* keywords</a>
*/
LINESEP("n");
private final List<String> aliases;
private KeywordKey(List<String> aliases) {
this.aliases = aliases;
}
private KeywordKey(String... others) {
this(List.of(others));
}
@Override
public List<String> aliases() {
return this.aliases;
}
}
/**
* Built-in supported color pattern keys.
*/
@CaseChanging
/*
* aliases is always assigned via List.of(...) in the constructor - genuinely
* immutable, just not through a type that says so statically.
*/
@SuppressWarnings("ImmutableEnumChecker")
enum ColorKey implements PatternKey {
/**
* ANSI black color
*/
BLACK("black"), //
/**
* ANSI red color
*/
RED("red"), //
/**
* ANSI green color
*/
GREEN("green"), //
/**
* ANSI yellow color
*/
YELLOW("yellow"), //
/**
* ANSI blue color
*/
BLUE("blue"), //
/**
* ANSI magenta color
*/
MAGENTA("magenta"), //
/**
* ANSI cyan color
*/
CYAN("cyan"), //
/**
* ANSI white color
*/
WHITE("white"), //
/**
* ANSI gray color
*/
GRAY("gray"), //
/**
* ANSI boldRed color
*/
BOLD_RED("boldRed"), //
/**
* ANSI boldGreen color
*/
BOLD_GREEN("boldGreen"), //
/**
* ANSI boldYellow color
*/
BOLD_YELLOW("boldYellow"), //
/**
* ANSI boldBlue color
*/
BOLD_BLUE("boldBlue"), //
/**
* ANSI boldMagenta color
*/
BOLD_MAGENTA("boldMagenta"), //
/**
* ANSI boldCyan color
*/
BOLD_CYAN("boldCyan"), //
/**
* ANSI boldWhite color
*/
BOLD_WHITE("boldWhite"), //
/**
* A special color composite key that will highlight based on logger level.
*/
HIGHLIGHT("highlight"), //
;
private final List<String> aliases;
private ColorKey(List<String> aliases) {
this.aliases = aliases;
}
private ColorKey(String... others) {
this(List.of(others));
}
@Override
public List<String> aliases() {
return this.aliases;
}
}
// config.conversionRule("clr", ColorConverter.class);
// config.conversionRule("correlationId", CorrelationIdConverter.class);
// config.conversionRule("esb", EnclosedInSquareBracketsConverter.class);
// config.conversionRule("wex", WhitespaceThrowableProxyConverter.class);
// config.conversionRule("wEx", ExtendedWhitespaceThrowableProxyConverter.class)
/**
* Spring Boot patterns.
*/
@CaseChanging
/*
* aliases is always assigned via List.of(...) in the constructor - genuinely
* immutable, just not through a type that says so statically.
*/
@SuppressWarnings("ImmutableEnumChecker")
enum SpringBootKey implements PatternKey {
/*
* TODO maybe this should be in the spring boot module only?
*/
/**
* <a href=
* "https://docs.spring.io/spring-boot/reference/features/logging.html#features.logging.console-output.color-coded">
* Spring Boot color pattern key</a>
*
*/
CLR("clr"),
/**
* Spring Boot converter that surrounds property in square brackets.
*/
ESB("esb");
private final List<String> aliases;
private SpringBootKey(List<String> aliases) {
this.aliases = aliases;
}
private SpringBootKey(String... others) {
this(List.of(others));
}
@Override
public List<String> aliases() {
return this.aliases;
}
}
}
final class DefaultPatternRegistry implements PatternRegistry {
private final Map<String, PatternKey> keys = new HashMap<>();
private final Map<PatternKey, PatternFormatterFactory> converters = new HashMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public DefaultPatternRegistry() {
super();
}
@Override
public void register(PatternKey key, PatternFormatterFactory factory) {
lock.writeLock().lock();
try {
var f = converters.get(key);
if (f == null) {
for (var a : key.aliases()) {
if (keys.containsKey(a)) {
throw new IllegalArgumentException("Key already registered: " + a);
}
keys.put(a, key);
}
}
converters.put(key, factory);
}
finally {
lock.writeLock().unlock();
}
}
@Override
public @Nullable PatternFormatterFactory getOrNull(String key) {
lock.readLock().lock();
try {
var ck = keys.get(key);
if (ck == null)
return null;
return converters.get(ck);
}
finally {
lock.readLock().unlock();
}
}
static PatternRegistry registerDefaults(PatternRegistry registry) {
for (KeywordKey c : KeywordKey.values()) {
KeywordFactory keyword = switch (c) {
case MESSAGE -> KeywordFactory.of(LogFormatter.builder().message().build());
case DATE -> StandardKeywordFactory.DATE;
case LEVEL -> KeywordFactory.of(LogFormatter.LevelFormatter.of());
case LINESEP -> (fc, n) -> new LogFormatter.StaticFormatter(fc.lineSeparator());
case LOCAL_SEQUENCE_NUMBER -> StandardKeywordFactory.LOCAL_SEQUENCE_NUMBER;
case LOGGER -> StandardKeywordFactory.LOGGER;
case MDC -> StandardKeywordFactory.MDC;
case ENCODED_MDC -> StandardKeywordFactory.ENCODED_MDC;
case MICROS -> KeywordFactory.of(LogFormatter.TimestampFormatter.ofMicros());
case RELATIVE -> StandardKeywordFactory.RELATIVE;
case THREAD -> KeywordFactory.of(LogFormatter.builder().threadName().build());
case THROWABLE -> StandardKeywordFactory.THROWABLE;
case EXTENDED_THROWABLE -> StandardKeywordFactory.EXTENDED_THROWABLE;
case ROOT_EXCEPTION -> StandardKeywordFactory.ROOT_EXCEPTION;
case NO_EXCEPTION -> KeywordFactory.of(LogFormatter.noop(), true);
case CLASS -> KeywordFactory.of(CallerInfoFormatter.CLASS);
case FILE -> KeywordFactory.of(CallerInfoFormatter.FILE);
case LINE -> KeywordFactory.of(CallerInfoFormatter.LINE);
case METHOD -> KeywordFactory.of(CallerInfoFormatter.METHOD);
case PROPERTY -> StandardKeywordFactory.PROPERTY;
};
registry.register(c, keyword);
}
for (ColorKey c : ColorKey.values()) {
var factory = switch (c) {
case RED -> ColorCompositeFactory.RED;
case BLACK -> ColorCompositeFactory.BLACK;
case BLUE -> ColorCompositeFactory.BLUE;
case CYAN -> ColorCompositeFactory.CYAN;
case GRAY -> ColorCompositeFactory.GRAY;
case GREEN -> ColorCompositeFactory.GREEN;
case MAGENTA -> ColorCompositeFactory.MAGENTA;
case WHITE -> ColorCompositeFactory.WHITE;
case YELLOW -> ColorCompositeFactory.YELLOW;
case BOLD_BLUE -> ColorCompositeFactory.BOLD_BLUE;
case BOLD_CYAN -> ColorCompositeFactory.BOLD_CYAN;
case BOLD_GREEN -> ColorCompositeFactory.BOLD_GREEN;
case BOLD_MAGENTA -> ColorCompositeFactory.BOLD_MAGENTA;
case BOLD_RED -> ColorCompositeFactory.BOLD_RED;
case BOLD_WHITE -> ColorCompositeFactory.BOLD_WHITE;
case BOLD_YELLOW -> ColorCompositeFactory.BOLD_YELLOW;
case HIGHLIGHT -> HighlightCompositeFactory.HIGHTLIGHT;
};
registry.register(c, factory);
}
for (SpringBootKey sk : SpringBootKey.values()) {
var factory = switch (sk) {
case CLR -> HighlightCompositeFactory.CLR;
case ESB -> StandardKeywordFactory.ENCLOSED_SQUARE_BRACKET;
};
registry.register(sk, factory);
}
registry.register(BareKey.BARE, BareCompositeFactory.BARE);
return registry;
}
record CustomConverterKey(List<String> aliases) implements PatternKey {
}
enum BareKey implements PatternKey {
BARE() {
@Override
public List<String> aliases() {
return List.of("BARE");
}
}
}
// DEFAULT_CONVERTER_MAP.putAll(Parser.DEFAULT_COMPOSITE_CONVERTER_MAP);
//
//
//
// DEFAULT_CONVERTER_MAP.put("C", ClassOfCallerConverter.class.getName());
// DEFAULT_CONVERTER_MAP.put("class", ClassOfCallerConverter.class.getName());
// CONVERTER_CLASS_TO_KEY_MAP.put(ClassOfCallerConverter.class.getName(), "class");
//
// DEFAULT_CONVERTER_MAP.put("M", MethodOfCallerConverter.class.getName());
// DEFAULT_CONVERTER_MAP.put("method", MethodOfCallerConverter.class.getName());
// CONVERTER_CLASS_TO_KEY_MAP.put(MethodOfCallerConverter.class.getName(), "method");
//
// DEFAULT_CONVERTER_MAP.put("L", LineOfCallerConverter.class.getName());
// DEFAULT_CONVERTER_MAP.put("line", LineOfCallerConverter.class.getName());
// CONVERTER_CLASS_TO_KEY_MAP.put(LineOfCallerConverter.class.getName(), "line");
//
// DEFAULT_CONVERTER_MAP.put("F", FileOfCallerConverter.class.getName());
// DEFAULT_CONVERTER_MAP.put("file", FileOfCallerConverter.class.getName());
// CONVERTER_CLASS_TO_KEY_MAP.put(FileOfCallerConverter.class.getName(), "file");
//
// DEFAULT_CONVERTER_MAP.put("X", MDCConverter.class.getName());
// DEFAULT_CONVERTER_MAP.put("mdc", MDCConverter.class.getName());
//
// DEFAULT_CONVERTER_MAP.put("nopex",
// NopThrowableInformationConverter.class.getName());
// DEFAULT_CONVERTER_MAP.put("nopexception",
// NopThrowableInformationConverter.class.getName());
//
// DEFAULT_CONVERTER_MAP.put("cn", ContextNameConverter.class.getName());
// DEFAULT_CONVERTER_MAP.put("contextName", ContextNameConverter.class.getName());
// CONVERTER_CLASS_TO_KEY_MAP.put(ContextNameConverter.class.getName(),
// "contextName");
//
// DEFAULT_CONVERTER_MAP.put("caller", CallerDataConverter.class.getName());
// CONVERTER_CLASS_TO_KEY_MAP.put(CallerDataConverter.class.getName(), "caller");
//
// DEFAULT_CONVERTER_MAP.put("marker", MarkerConverter.class.getName());
// CONVERTER_CLASS_TO_KEY_MAP.put(MarkerConverter.class.getName(), "marker");
//
// DEFAULT_CONVERTER_MAP.put("kvp", KeyValuePairConverter.class.getName());
// CONVERTER_CLASS_TO_KEY_MAP.put(KeyValuePairConverter.class.getName(), "kvp");
//
// DEFAULT_CONVERTER_MAP.put("property", PropertyConverter.class.getName());
//
//
//
// DEFAULT_CONVERTER_MAP.put("sn", SequenceNumberConverter.class.getName());
// DEFAULT_CONVERTER_MAP.put("sequenceNumber",
// SequenceNumberConverter.class.getName());
// CONVERTER_CLASS_TO_KEY_MAP.put(SequenceNumberConverter.class.getName(),
// "sequenceNumber");
//
// DEFAULT_CONVERTER_MAP.put("prefix", PrefixCompositeConverter.class.getName());
}