LogFormatter.java

package io.jstach.rainbowgum;

import java.io.PrintWriter;
import java.lang.System.Logger.Level;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jspecify.annotations.Nullable;

import io.jstach.rainbowgum.KeyValues.KeyValuesConsumer;
import io.jstach.rainbowgum.annotation.CaseChanging;
import io.jstach.rainbowgum.LogFormatter.EventFormatter;
import io.jstach.rainbowgum.LogFormatter.LevelFormatter;
import io.jstach.rainbowgum.LogFormatter.ThrowableFormatter;
import io.jstach.rainbowgum.LogFormatter.TimestampFormatter;
import io.jstach.rainbowgum.format.KeyValuesFormatterBuilder;

/**
 * Formats a log event using a {@link StringBuilder}. <strong>All formatters should be
 * thread-safe!</strong>.
 * <p>
 * The appender will make sure the {@link StringBuilder} is not shared with multiple
 * threads so the formatter does not have to synchronize/lock on and should definitely not
 * do that.
 * <p>
 * Because of various invariants the preferred way to compose formatters is to use the
 * {@linkplain #builder() builder} which will do some optimization like combining static
 * formatters etc.
 *
 * @see #builder()
 * @see LogFormatter.EventFormatter
 * @see LogEncoder#of(LogFormatter)
 * @apiNote This class is sealed. An interface that has the same contract that can be
 * implemented is {@link EventFormatter}.
 */
public sealed interface LogFormatter {

	/**
	 * Formats a log event.
	 * @param output buffer.
	 * @param event log event.
	 * @see EventFormatter
	 */
	public void format(StringBuilder output, LogEvent event);

	/**
	 * See {@link EventFormatter#builder()}.
	 * @return builder.
	 */
	public static Builder builder() {
		return new Builder();
	}

	/**
	 * Create a formatter using event formatter that is a lambda so that
	 * {@code LogFormater.of((o, e) -> ...); } works.
	 * @param e will be returned.
	 * @return passed in formatter.
	 * @apiNote this is for ergonomics because LogFormatter is sealed.
	 */
	public static EventFormatter of(EventFormatter e) {
		return e;
	}

	/**
	 * Ask the formatter if will do anything.
	 * @return true if promises not to write to builder.
	 * @see #noop()
	 */
	default boolean isNoop() {
		return NoopFormatter.INSTANCE == this;
	}

	/**
	 * A special formatter that will do nothing. It is a singleton so identity comparison
	 * can be used.
	 * @return a formatter that implements all formatting interfaces but does nothing.
	 */
	public static NoopFormatter noop() {
		return NoopFormatter.INSTANCE;
	}

	/**
	 * A special formatter that will append static text.
	 *
	 * @param content static text.
	 */
	public record StaticFormatter(String content) implements LogFormatter {
		@Override
		public void format(StringBuilder output, LogEvent event) {
			output.append(content);
		}

		/**
		 * Creates a new formatter by concat the {@link #content()}. This is mainly used
		 * by the {@link EventFormatter#builder()} to coalesce multiple static text.
		 * @param next the text that will follow this formatter.
		 * @return new formatter.
		 */
		public StaticFormatter concat(StaticFormatter next) {
			return new StaticFormatter(this.content + next.content);
		}

		/**
		 * Coalesce formatters that can be such as {@link StaticFormatter}.
		 * @param formatters list of formatters in the order of which they will be
		 * executed.
		 * @return an array of formatters where static formatters next to each other will
		 * be coalesced.
		 */
		private static LogFormatter[] coalesce(List<? extends LogFormatter> formatters) {
			var flattened = CompositeFormatter.flatten(formatters);
			List<LogFormatter> resolved = new ArrayList<>();
			StaticFormatter current = null;
			for (var f : flattened) {
				if (f.isNoop()) {
				}
				else if (current == null && f instanceof StaticFormatter sf) {
					current = sf;
				}
				else if (current != null && f instanceof StaticFormatter sf) {
					current = current.concat(sf);
				}
				else if (current != null) {
					resolved.add(current);
					resolved.add(f);
					current = null;
				}
				else {
					resolved.add(f);
				}
			}
			if (current != null) {
				resolved.add(current);
			}
			return resolved.toArray(new LogFormatter[] {});
		}

		@Override
		public String toString() {
			return "STATIC['" + content + "']";
		}
	}

	/**
	 * Controls how a <code>null</code> key value is written by key values formatters, see
	 * {@link io.jstach.rainbowgum.format.KeyValuesFormatterBuilder#nullStrategy(KeyValueNullStrategy)}.
	 * The examples below are the default percent encoding; other formats write the same
	 * choices in their own syntax.
	 * <p>
	 * A selected key is treated identically whether it is missing from the
	 * {@link KeyValues} entirely or present but mapped to <code>null</code> -
	 * {@link KeyValues#getValueOrNull(String)} cannot tell those two cases apart, so
	 * neither can this.
	 */
	@CaseChanging
	public enum KeyValueNullStrategy {

		/**
		 * The key is omitted entirely (no key, no <code>=</code>) if its value is
		 * <code>null</code>.
		 */
		SKIP,
		/**
		 * A <code>null</code> value is normalized to an empty string, so the key is
		 * always printed as <code>key=</code> (with nothing after the <code>=</code>)
		 * regardless of whether the underlying value was <code>null</code> or actually an
		 * empty string.
		 */
		EMPTY,
		/**
		 * The <code>null</code>/empty-string distinction is preserved: a
		 * <code>null</code> value prints just the key with no <code>=</code>, while an
		 * empty string prints <code>key=</code>. The default; most key values (e.g. MDC)
		 * are never actually mapped to <code>null</code> in practice, so {@link #SKIP}
		 * and {@link #KEEP} behave identically until they are.
		 */
		KEEP;

	}

	/**
	 * Log formatter builder that is composed of other formatters. The
	 * {@link #add(LogFormatter)} are executed in insertion order. <strong> This builder
	 * is smart and will coalesce and consolidate formatters! </strong> For example if
	 * only formatter is added to the builder it will be returned instead of a new
	 * formatter.
	 */
	public final static class Builder {

		private List<LogFormatter> formatters = new ArrayList<>();

		private Builder() {
		}

		/**
		 * Adds a formatter.
		 * @param formatter formatter to be added the list of formatters.
		 * @return this builder.
		 */
		public Builder add(LogFormatter formatter) {
			formatters.add(formatter);
			return this;
		}

		/**
		 * Adds an event formatter.
		 * @param formatter event formatter to be added the list of formatters.
		 * @return this builder.
		 */
		public Builder event(LogFormatter.EventFormatter formatter) {
			formatters.add(formatter);
			return this;
		}

		/**
		 * Append the timestamp in ISO format.
		 * @return this builder.
		 */
		public Builder timeStamp() {
			formatters.add(DefaultInstantFormatter.ISO);
			return this;
		}

		/**
		 * Formatter for {@link LogEvent#timestamp()} derived from standard
		 * {@link DateTimeFormatter}.
		 * @param dateTimeFormatter formatter for {@link LogEvent#timestamp()}
		 * @return this builder.
		 */
		public Builder timeStamp(DateTimeFormatter dateTimeFormatter) {
			formatters.add(TimestampFormatter.of(dateTimeFormatter));
			return this;
		}

		/**
		 * Adds the default level formatter.
		 * @return this builder.
		 * @see LevelFormatter
		 */
		public Builder level() {
			formatters.add(LogFormatter.LevelFormatter.of());
			return this;
		}

		/**
		 * Adds the default logger name formatter.
		 * @return this builder.
		 */
		public Builder loggerName() {
			formatters.add(DefaultNameFormatter.LOGGER_NAME_FORMATTER);
			return this;
		}

		/**
		 * Formats the message by calling
		 * {@link LogEvent#formattedMessage(StringBuilder)}.
		 * @return this builder.
		 */
		public Builder message() {
			formatters.add(DefaultMessageFormatter.MESSAGE_FORMATTER);
			return this;
		}

		/**
		 * Appends static text.
		 * @param content static text.
		 * @return this builder.
		 * @see StaticFormatter
		 */
		public Builder text(String content) {
			formatters.add(new StaticFormatter(content));
			return this;
		}

		/**
		 * Creates a formatter that will print a single key's value, Logback/Log4j2
		 * <code>%X{key}</code> style: just the raw value (or the fallback, or an empty
		 * string if neither is present) - no key prefix, no percent encoding. For the
		 * percent-encoded <code>key=value</code> style instead, see
		 * {@link #encodedKeyValue(String, String)}.
		 * @param key key to select.
		 * @param fallback if the value is null the fallback will be used.
		 * @return this.
		 */
		public Builder keyValue(String key, @Nullable String fallback) {
			return add(new LogbackSingleKeyValueFormatter(key, fallback));
		}

		/**
		 * Adds all of the event's key values in the default key values format, percent
		 * encoded like a URI query: <code>requestId=42&amp;user=Ada%20Lovelace</code>. A
		 * key mapped to <code>null</code> is written alone, without <code>=</code>. This
		 * is {@link io.jstach.rainbowgum.format.KeyValuesFormatterBuilder} with its
		 * defaults; use {@link #keyValues(Consumer)} for another format, a selection of
		 * keys, or other null handling.
		 * @return this.
		 */
		public Builder keyValues() {
			return add(new KeyValuesFormatterBuilder().build());
		}

		/**
		 * Adds the event's key values as configured on a
		 * {@link io.jstach.rainbowgum.format.KeyValuesFormatterBuilder}, which sets the
		 * format (percent encoding by default, JSON, JSON5, or logfmt), the keys, and the
		 * null handling, for example:
		 * {@snippet :
		 * LogFormatter.builder()
		 * 	.keyValues(kv -> kv.format(KeyValuesFormatterBuilder.Format.LOGFMT).keys(List.of("requestId", "user")));
		 * }
		 * @param consumer configures the key values formatter.
		 * @return this.
		 */
		public Builder keyValues(Consumer<KeyValuesFormatterBuilder> consumer) {
			var builder = new KeyValuesFormatterBuilder();
			consumer.accept(builder);
			return add(builder.build());
		}

		/**
		 * Creates a formatter that will print a single key value in percent encoding (RFC
		 * 3986 URI aka the format usually used in {@link URI#getQuery()}). For Logback/
		 * Log4j2's <code>%X{key}</code> style instead, see
		 * {@link #keyValue(String, String)}.
		 * @param key key to select.
		 * @param fallback if the value is null the fallback will be used.
		 * @return this.
		 */
		public Builder encodedKeyValue(String key, @Nullable String fallback) {
			return add(new SingleKeyValueFormatter(key, fallback));
		}

		/**
		 * Appends a space.
		 * @return this builder.
		 */
		public Builder space() {
			formatters.add(new StaticFormatter(" "));
			return this;
		}

		/**
		 * Appends text if throwable is set.
		 * @param text to append if the throwable is set.
		 * @return this builder.
		 */
		public Builder textIfThrowable(String text) {
			formatters.add(of((b, e) -> {
				if (e.throwableOrNull() != null) {
					b.append(text);
				}
			}));
			return this;
		}

		/**
		 * Appends a newline using the platforms line separator.
		 * @return this builder.
		 */
		public Builder newline() {
			text(System.lineSeparator());
			return this;
		}

		/**
		 * Appends a thread name : {@link Thread#getName()}.
		 * @return this builder.
		 */
		public Builder threadName() {
			formatters.add(DefaultThreadFormatter.THREAD_NAME_FORMATTER);
			return this;
		}

		/**
		 * Appends a thread ID : {@link Thread#threadId()}.
		 * @return this builder.
		 */
		public Builder threadId() {
			formatters.add(DefaultThreadFormatter.THREAD_ID_FORMATTER);
			return this;
		}

		/**
		 * Appends the events throwable stack trace.
		 * @return this.
		 */
		public Builder throwable() {
			formatters.add(ThrowableFormatter.of());
			return this;
		}

		/**
		 * Appends the events throwable stack trace using a custom
		 * {@link ThrowableFormatter} (e.g. one created with
		 * {@link ThrowableFormatter#builder()}).
		 * @param throwableFormatter formatter to use.
		 * @return this.
		 */
		public Builder throwable(ThrowableFormatter throwableFormatter) {
			formatters.add(throwableFormatter);
			return this;
		}

		/**
		 * Will create a generic log formatter that has the inner formatters coalesced if
		 * possible and will noop if there are no formatters.
		 * @return flattened formatter.
		 */
		public LogFormatter build() {
			var array = StaticFormatter.coalesce(formatters);
			if (array.length == 0) {
				return NoopFormatter.INSTANCE;
			}
			if (array.length == 1) {
				return array[0];
			}
			return EventFormatter.of(formatters);
		}

		/**
		 * Creates the formatter and starts an encoder builder from it - call
		 * {@link LogEncoder.Builder#build()} to finish, optionally configuring charset,
		 * content type, max buffer size, or initial buffer size first.
		 * @return encoder builder.
		 * @apiNote for ergonomics
		 */
		public LogEncoder.Builder encoder() {
			return LogEncoder.builder(build());
		}

	}

	/**
	 * Generic event formatting that is lambda friendly.
	 */
	@FunctionalInterface
	public non-sealed interface EventFormatter extends LogFormatter {

		@Override
		public void format(StringBuilder output, LogEvent event);

		private static EventFormatter of(List<? extends LogFormatter> formatters) {
			return new CompositeFormatter(StaticFormatter.coalesce(formatters));
		}

	}

	/**
	 * Formats a {@link Level}.
	 */
	public sealed interface LevelFormatter extends LogFormatter {

		/**
		 * Formats the level.
		 * @param output buffer.
		 * @param level level.
		 */
		void formatLevel(StringBuilder output, Level level);

		@Override
		default void format(StringBuilder output, LogEvent event) {
			formatLevel(output, event.level());
		}

		/**
		 * Formats a level.
		 * @param level level
		 * @return formatted level as a string.
		 */
		default String formatLevel(Level level) {
			StringBuilder sb = new StringBuilder();
			formatLevel(sb, level);
			return sb.toString();
		}

		/**
		 * Default implementation calls {@link LevelFormatter#toString(Level)}
		 * @return formatter.
		 */
		public static LevelFormatter of() {
			return DefaultLevelFormatter.LEVEL_FORMATTER;
		}

		/**
		 * Default implementation calls {@link LevelFormatter#rightPadded(Level)}
		 * @return formatter.
		 */
		public static LevelFormatter ofRightPadded() {
			return DefaultLevelFormatter.RIGHT_PAD_LEVEL_FORMATTER;
		}

		/**
		 * Turns a Level into a SLF4J like level String that is all upper case.
		 * {@link Level#ALL} is "<code>TRACE</code>", {@link Level#OFF} is
		 * "<code>ERROR</code>" and {@link Level#WARNING} is "<code>WARN</code>".
		 * @param level system logger level.
		 * @return upper case string of level.
		 */
		public static String toString(Level level) {
			return switch (level) {
				case DEBUG -> "DEBUG";
				case ALL -> "TRACE";
				case ERROR -> "ERROR";
				case INFO -> "INFO";
				case OFF -> "ERROR";
				case TRACE -> "TRACE";
				case WARNING -> "WARN";
			};
		}

		/**
		 * Turns a Level into a SLF4J like level String that is all upper case and same
		 * length with right padding. {@link Level#ALL} is "<code>TRACE</code>",
		 * {@link Level#OFF} is "<code>ERROR</code>" and {@link Level#WARNING} is
		 * "<code>WARN</code>".
		 * @param level system logger level.
		 * @return upper case string of level.
		 */
		public static String rightPadded(Level level) {
			return switch (level) {
				case DEBUG -> /*   */ "DEBUG";
				case ALL -> /*     */ "TRACE";
				case ERROR -> /*   */ "ERROR";
				case INFO -> /*    */ "INFO ";
				case OFF -> /*     */ "ERROR";
				case TRACE -> /*   */ "TRACE";
				case WARNING -> /* */ "WARN ";
			};
		}

	}

	/**
	 * Formats event timestamps.
	 */
	public sealed interface TimestampFormatter extends LogFormatter {

		/**
		 * The default timestamp format used in many logging frameworks which does not
		 * have dates and only time at millisecond precision.
		 * <p>
		 * It is called TTLL as that is the name of the format where it is used in
		 * logback, log4j etc.
		 */
		public static String TTLL_TIME_FORMAT = "HH:mm:ss.SSS";

		/**
		 * Format timestamp.
		 * @param output buffer.
		 * @param instant timestamp.
		 */
		void formatTimestamp(StringBuilder output, Instant instant);

		@Override
		default void format(StringBuilder output, LogEvent event) {
			formatTimestamp(output, event.timestamp());
		}

		/**
		 * Formats timestamp using {@link #TTLL_TIME_FORMAT}.
		 * @return formatter.
		 */
		public static TimestampFormatter of() {
			return DefaultInstantFormatter.TTLL;
		}

		/**
		 * Formats a timestamp using ISO format at fixed millisecond precision (e.g.
		 * {@code 2023-11-14T22:13:20.123Z}, always exactly 3 fractional digits). Pair
		 * with {@link #ofMicros()} to append additional sub-millisecond digits, similar
		 * to Logback's {@code %d{ISO8601}%microsecond}.
		 * @return formatter.
		 */
		public static TimestampFormatter ofISO() {
			return DefaultInstantFormatter.ISO;
		}

		/**
		 * Micro seconds over the events last second. This is for logback compatibility.
		 * @return microseconds zero padded.
		 */
		public static TimestampFormatter ofMicros() {
			return MicrosInstantFormatter.INSTANCE;
		}

		/**
		 * Formats a timestamp using standard JDK date time formatter.
		 * @param dateTimeFormatter date time formatter.
		 * @return timestamp formatter.
		 */
		public static TimestampFormatter of(DateTimeFormatter dateTimeFormatter) {
			return of(dateTimeFormatter, false);
		}

		/**
		 * Formats a timestamp using standard JDK date time formatter.
		 * <p>
		 * Formatting with {@link DateTimeFormatter} is comparatively expensive to do on
		 * every event, so if the caller knows the formatter's finest resolution is
		 * milliseconds (e.g. a pattern using at most {@code SSS}, never
		 * {@code n}/{@code N} nanosecond fields), {@code cacheMillisecondPrecision} can
		 * be set to {@code true} to reuse the previously formatted string whenever
		 * consecutive events land in the same millisecond - the same trick Logback's
		 * {@code CachingDateFormatter} and Log4j2's {@code FixedDateFormat} use. Passing
		 * {@code true} for a formatter that can actually render sub-millisecond precision
		 * will silently truncate that precision, so only pass {@code true} when the
		 * pattern is known to not exceed millisecond resolution.
		 * @param dateTimeFormatter date time formatter.
		 * @param cacheMillisecondPrecision {@code true} if the formatter's output is
		 * fully determined by the instant's millisecond value.
		 * @return timestamp formatter.
		 */
		public static TimestampFormatter of(DateTimeFormatter dateTimeFormatter, boolean cacheMillisecondPrecision) {
			return new DateTimeFormatterInstantFormatter(dateTimeFormatter, cacheMillisecondPrecision);
		}

	}

	/**
	 * Formats a throwable.
	 */
	public non-sealed interface ThrowableFormatter extends LogFormatter {

		/**
		 * Formats a throwable and appends.
		 * @param output buffer.
		 * @param throwable throwable.
		 */
		void formatThrowable(StringBuilder output, Throwable throwable);

		@Override
		default void format(StringBuilder output, LogEvent event) {
			var t = event.throwableOrNull();
			if (t != null) {
				formatThrowable(output, t);
			}
		}

		/**
		 * Default implementation uses {@link Throwable#printStackTrace(PrintWriter)}.
		 * This formatter is equivalent to {@code of(Integer.MAX_VALUE, List.of())} except
		 * that it defers entirely to {@link Throwable#printStackTrace()} and thus will
		 * respect any override of that method a particular {@link Throwable} subclass
		 * might have.
		 * @return formatter.
		 */
		public static ThrowableFormatter of() {
			return DefaultThrowableFormatter.INSTANT;
		}

		/**
		 * Creates a builder for a throwable formatter that can limit the number of stack
		 * frame lines shown for each throwable in the cause/suppressed chain, exclude
		 * frames matching regular expressions, and append packaging data. The output is
		 * otherwise formatted like {@link Throwable#printStackTrace()}: common frames
		 * shared with the enclosing throwable are elided the same way (e.g.
		 * <code>"... 6 more"</code>) as long as the depth limit was not what stopped
		 * printing; if the depth limit is what stopped printing a
		 * <code>"... N frames truncated"</code> line is emitted instead.
		 * @return builder.
		 * @apiNote a builder left at its defaults (no max lines, exclusions, or packaging
		 * data) builds {@link #of()} itself; otherwise the built formatter walks
		 * {@link Throwable#getCause()}/{@link Throwable#getSuppressed()}/
		 * {@link Throwable#getStackTrace()} directly instead of calling
		 * {@link Throwable#printStackTrace()} and thus will not honor a custom override
		 * of that method.
		 */
		public static Builder builder() {
			return new Builder();
		}

		/**
		 * Convenience to append a throwable to string builder.
		 * @param b buffer.
		 * @param t throwable.
		 * @apiNote this call creates garbage.
		 */
		public static void appendThrowable(StringBuilder b, Throwable t) {
			/*
			 * TODO optimize
			 */
			t.printStackTrace(Internal.StringBuilderPrintWriter.of(b));
		}

		/**
		 * Builds a configurable {@link ThrowableFormatter}.
		 *
		 * @see ThrowableFormatter#builder()
		 */
		public final class Builder {

			private int maxLines = Integer.MAX_VALUE;

			private List<String> excludes = List.of();

			private boolean packagingData = false;

			private boolean rootCauseFirst = false;

			private Builder() {
			}

			/**
			 * Maximum number of stack frame ("at ...") lines printed per throwable in the
			 * chain. Use {@link Integer#MAX_VALUE} for no limit (the default, and matches
			 * {@link ThrowableFormatter#of()} for well behaved throwables). {@code 0}
			 * prints just the throwable header lines (class and message) for the whole
			 * chain with no frames at all.
			 * @param maxLines maximum stack frame lines per throwable.
			 * @return this builder.
			 */
			public Builder maxLines(int maxLines) {
				this.maxLines = maxLines;
				return this;
			}

			/**
			 * Regular expressions matched with {@link Matcher#find()} against each
			 * frame's {@link StackTraceElement#toString()}. A frame matching any of these
			 * is omitted entirely and does not count against {@link #maxLines(int)}.
			 * Defaults to empty (no filtering).
			 * @param excludes exclude patterns.
			 * @return this builder.
			 */
			public Builder excludes(List<String> excludes) {
				this.excludes = excludes;
				return this;
			}

			/**
			 * Whether to append packaging data (the jar or module a frame's class was
			 * loaded from, and its version) after each frame line, e.g.
			 * <code>[myapp-1.0.jar:1.0]</code> or <code>[java.base:na]</code>. This
			 * mirrors (but does not byte-for-byte match) logback's
			 * <code>ExtendedThrowableProxyConverter</code> output. Resolution requires
			 * loading the frame's class (without initializing it) and can fail silently
			 * to <code>[na:na]</code> for frames whose class cannot be loaded (e.g.
			 * dynamically generated classes). This has a real per-frame cost so it
			 * defaults to <code>false</code>.
			 * @param packagingData true to append packaging data.
			 * @return this builder.
			 */
			public Builder packagingData(boolean packagingData) {
				this.packagingData = packagingData;
				return this;
			}

			/**
			 * Whether to print the root cause first instead of last, working back up
			 * through each wrapping exception ("Wrapped by: ..." instead of "Caused by:
			 * ..."), mirroring logback's
			 * <code>RootCauseFirstThrowableProxyConverter</code> (<code>%rEx</code>/
			 * <code>%rootException</code>). Frame trimming (common-frame elision against
			 * each throwable's own wrapper, {@link #maxLines(int)},
			 * {@link #excludes(List)}) behaves identically either way - only the print
			 * order and the "Caused by:"/"Wrapped by:" captions differ. Defaults to
			 * <code>false</code>.
			 * @param rootCauseFirst true to print the root cause first.
			 * @return this builder.
			 */
			public Builder rootCauseFirst(boolean rootCauseFirst) {
				this.rootCauseFirst = rootCauseFirst;
				return this;
			}

			/**
			 * Builds the formatter. If none of {@link #maxLines(int)},
			 * {@link #excludes(List)}, {@link #packagingData(boolean)}, or
			 * {@link #rootCauseFirst(boolean)} were set away from their defaults, returns
			 * {@link ThrowableFormatter#of()} (i.e. plain
			 * {@link Throwable#printStackTrace()} behavior) instead of constructing a
			 * formatter that walks the throwable manually for no reason.
			 * @return formatter.
			 */
			public ThrowableFormatter build() {
				if (maxLines == Integer.MAX_VALUE && excludes.isEmpty() && !packagingData && !rootCauseFirst) {
					return ThrowableFormatter.of();
				}
				List<Pattern> compiled = excludes.isEmpty() ? List.of()
						: excludes.stream().map(Pattern::compile).toList();
				var resolver = packagingData ? new PackagingDataResolver() : null;
				var framePrinter = new ThrowableFramePrinter(maxLines, compiled, resolver);
				if (rootCauseFirst) {
					return new RootCauseFirstThrowableFormatter(framePrinter);
				}
				return new StandardThrowableFormatter(framePrinter);
			}

		}

	}

	/**
	 * Tests if the log formatter is noop or is null which will be considered as noop.
	 * @param logFormatter formatter which <strong>can be <code>null</code></strong>!
	 * @return true if the formatter should not be used.
	 */
	public static boolean isNoopOrNull(@Nullable LogFormatter logFormatter) {
		return logFormatter == null || logFormatter.isNoop();
	}

	/**
	 * Pads the right hand side of text with space.
	 * @param sb buffer.
	 * @param s string that will be appended first (left hand) and will not be longer than
	 * the <code>n</code> parameter.
	 * @param n the size of string. If the size is bigger than passed in string the result
	 * will have padding otherwise the passed in string will be cut to the size of this
	 * parameter.
	 */
	public static void padRight(StringBuilder sb, CharSequence s, int n) {
		int length = s.length();
		if (length >= n) {
			sb.append(s, 0, n);
			return;
		}
		sb.append(s);
		spacePad(sb, n - length);
	}

	/**
	 * Pads the left hand side of text with space.
	 * @param sb buffer.
	 * @param s string that will be appended second (right hand) and will not be longer
	 * than the <code>n</code> parameter.
	 * @param n the size of string. If the size is bigger than passed in string the result
	 * will have padding otherwise the passed in string will be cut to the size of this
	 * parameter.
	 */
	public static void padLeft(StringBuilder sb, CharSequence s, int n) {
		int length = s.length();
		if (length >= n) {
			sb.append(s, 0, n);
			return;
		}
		spacePad(sb, n - length);
		sb.append(s);
	}

	/**
	 * Fast space padding method.
	 */
	private static void spacePad(final StringBuilder sbuf, final int length) {
		int l = length;
		while (l >= 32) {
			sbuf.append(CompositeFormatter.SPACES[5]);
			l -= 32;
		}

		for (int i = 4; i >= 0; i--) {
			if ((l & (1 << i)) != 0) {
				sbuf.append(CompositeFormatter.SPACES[i]);
			}
		}
	}

	/**
	 * A special formatter that will do nothing.
	 */
	enum NoopFormatter implements TimestampFormatter, ThrowableFormatter, LevelFormatter {

		/**
		 * instance.
		 */
		INSTANCE;

		@Override
		public void formatThrowable(StringBuilder output, Throwable throwable) {
		}

		@Override
		public void formatTimestamp(StringBuilder output, Instant instant) {
		}

		@Override
		public void formatLevel(StringBuilder output, Level level) {
		}

		@Override
		public void format(StringBuilder output, LogEvent event) {
		}

	}

}

@SuppressWarnings("ArrayRecordComponent")
record CompositeFormatter(LogFormatter[] formatters) implements EventFormatter {

	static String[] SPACES = { " ", "  ", "    ", "        ", // 1,2,4,8 spaces
			"                ", // 16 spaces
			"                                " }; // 32 spaces

	@Override
	public void format(StringBuilder output, LogEvent event) {
		for (var formatter : formatters) {
			formatter.format(output, event);
		}
	}

	@Override
	public String toString() {
		return getClass().getSimpleName() + Arrays.toString(formatters);
	}

	public static List<LogFormatter> flatten(List<? extends LogFormatter> formatters) {
		return List.copyOf(_flatten(formatters));
	}

	private static List<LogFormatter> _flatten(List<? extends LogFormatter> formatters) {
		List<LogFormatter> result = new ArrayList<>();
		for (var f : formatters) {
			if (f instanceof CompositeFormatter cf) {
				result.addAll(_flatten(cf));
			}
			else {
				result.add(f);
			}
		}
		return result;
	}

	private static List<LogFormatter> _flatten(CompositeFormatter formatter) {
		var formatters = formatter.formatters;
		return _flatten(Arrays.asList(formatters));
	}

}

enum DefaultMessageFormatter implements LogFormatter {

	MESSAGE_FORMATTER;

	@Override
	public void format(StringBuilder output, LogEvent event) {
		event.formattedMessage(output);

	}

}

enum DefaultNameFormatter implements LogFormatter {

	LOGGER_NAME_FORMATTER;

	@Override
	public void format(StringBuilder output, LogEvent event) {
		output.append(event.loggerName());

	}

}

enum DefaultLevelFormatter implements LevelFormatter {

	LEVEL_FORMATTER {
		@Override
		public void formatLevel(StringBuilder output, Level level) {
			output.append(LevelFormatter.toString(level));
		}
	},
	RIGHT_PAD_LEVEL_FORMATTER {
		@Override
		public void formatLevel(StringBuilder output, Level level) {
			output.append(LevelFormatter.rightPadded(level));
		}
	}

}

enum DefaultThreadFormatter implements LogFormatter {

	THREAD_NAME_FORMATTER() {
		@Override
		public void format(StringBuilder output, LogEvent event) {
			output.append(event.threadName());
		}
	},
	THREAD_ID_FORMATTER() {
		@Override
		public void format(StringBuilder output, LogEvent event) {
			output.append(event.threadId());
		}
	}

}

/**
 * Not an enum: error-prone's immutable-enum check flags enums holding mutable state, and
 * {@link #cache} is mutable (an {@link java.util.concurrent.atomic.AtomicReference}).
 * {@link #description} exists solely to give each instance a stable identity for
 * {@link #toString()}.
 */
final class DefaultInstantFormatter implements TimestampFormatter {

	static final DefaultInstantFormatter TTLL = new DefaultInstantFormatter("TTLL",
			DateTimeFormatter.ofPattern(TTLL_TIME_FORMAT).withZone(ZoneId.from(ZoneOffset.UTC)));

	/*
	 * appendInstant(3) renders a fixed-width 3-digit millisecond fraction (unlike
	 * DateTimeFormatter.ISO_DATE_TIME, which renders a variable-width fraction of 0-9
	 * digits based on the instant's actual sub-millisecond value) - fixed width matches
	 * Logback/Log4j2's ISO output and is what makes this safe to millis-cache.
	 */
	static final DefaultInstantFormatter ISO = new DefaultInstantFormatter("ISO",
			new DateTimeFormatterBuilder().appendInstant(3).toFormatter());

	private final String description;

	private final DateTimeFormatter formatter;

	private final MillisCache cache = new MillisCache();

	private DefaultInstantFormatter(String description, DateTimeFormatter formatter) {
		this.description = description;
		this.formatter = formatter;
	}

	@Override
	public void formatTimestamp(StringBuilder output, Instant instant) {
		cache.formatTimestamp(output, instant, formatter);
	}

	@Override
	public String toString() {
		return description;
	}

}

/**
 * Micro seconds over the events last second, for Logback compatibility - meant to be
 * composed after a millisecond-precision timestamp (see
 * {@link TimestampFormatter#ofISO()}). Stateless, so a singleton enum works cleanly here
 * (unlike {@link DefaultInstantFormatter}, nothing here trips error-prone's
 * immutable-enum check).
 */
enum MicrosInstantFormatter implements TimestampFormatter {

	INSTANCE;

	@Override
	@SuppressWarnings("JavaInstantGetSecondsGetNano")
	public void formatTimestamp(StringBuilder output, Instant instant) {
		int nanos = instant.getNano();

		int millis_and_micros = nanos / 1000;
		int micros = millis_and_micros % 1000;

		if (micros >= 100) {
			output.append(micros);
		}
		else if (micros >= 10) {
			output.append("0").append(micros);
		}
		else {
			output.append("00").append(micros);
		}
	}

}

final class DateTimeFormatterInstantFormatter implements TimestampFormatter {

	private final DateTimeFormatter dateTimeFormatter;

	private final @Nullable MillisCache cache;

	DateTimeFormatterInstantFormatter(DateTimeFormatter dateTimeFormatter, boolean cacheMillisecondPrecision) {
		this.dateTimeFormatter = dateTimeFormatter;
		this.cache = cacheMillisecondPrecision ? new MillisCache() : null;
	}

	@Override
	public void formatTimestamp(StringBuilder output, Instant instant) {
		var c = cache;
		if (c == null) {
			dateTimeFormatter.formatTo(instant, output);
			return;
		}
		c.formatTimestamp(output, instant, dateTimeFormatter);
	}

}

/**
 * Caches a formatted timestamp string keyed by the instant's millisecond value, the same
 * trick Logback's {@code CachingDateFormatter} and Log4j2's {@code FixedDateFormat} use -
 * reformatting only happens when consecutive events land in different milliseconds. Only
 * correct for formatters whose output is fully determined by millisecond precision
 * (callers are responsible for that guarantee).
 */
final class MillisCache {

	private record Entry(long millis, String formatted) {
	}

	private static final Entry EMPTY = new Entry(Long.MIN_VALUE, "");

	private final AtomicReference<Entry> ref = new AtomicReference<>(EMPTY);

	void formatTimestamp(StringBuilder output, Instant instant, DateTimeFormatter formatter) {
		long millis = instant.toEpochMilli();
		Entry cached = ref.get();
		if (cached.millis() == millis) {
			output.append(cached.formatted());
			return;
		}
		StringBuilder sb = new StringBuilder(32);
		formatter.formatTo(instant, sb);
		String formatted = sb.toString();
		ref.set(new Entry(millis, formatted));
		output.append(formatted);
	}

}

enum DefaultThrowableFormatter implements ThrowableFormatter {

	INSTANT;

	@Override
	public void formatThrowable(StringBuilder output, Throwable throwable) {
		ThrowableFormatter.appendThrowable(output, throwable);
	}

}

/**
 * Prints stack frames trimmed by {@link Builder#maxLines(int) maxLines}/
 * {@link Builder#excludes(List) excludes} and optionally appended with packaging data -
 * shared as-is between {@link StandardThrowableFormatter} and
 * {@link RootCauseFirstThrowableFormatter}, which differ only in traversal order and
 * caption choice, never in how an individual throwable's own frames get printed.
 */
final class ThrowableFramePrinter {

	private final int maxLines;

	private final List<Pattern> excludes;

	private final @Nullable PackagingDataResolver packagingData;

	ThrowableFramePrinter(int maxLines, List<Pattern> excludes, @Nullable PackagingDataResolver packagingData) {
		this.maxLines = maxLines;
		this.excludes = excludes;
		this.packagingData = packagingData;
	}

	/*
	 * Prints trace's frames, trimmed against enclosingTrace's shared trailing frames (a
	 * throwable typically shares a stack-trace suffix with whatever holds it as its
	 * cause, from how the JVM captures traces at throw time) the same way
	 * Throwable#printStackTrace() does - "... N more" if elision fully accounts for the
	 * missing frames, nothing if maxLines/excludes already cut the visible output short
	 * first. Shared verbatim by StandardThrowableFormatter (enclosingTrace = the trace of
	 * whatever this throwable is the cause of) and RootCauseFirstThrowableFormatter
	 * (enclosingTrace = the same relationship, just discovered while recursing the other
	 * direction) - the "which throwable wraps which" relationship this depends on does
	 * not change with print order.
	 */
	void printFramesAgainstEnclosing(StringBuilder output, StackTraceElement[] trace,
			StackTraceElement[] enclosingTrace, String prefix) {
		int m = trace.length - 1;
		int n = enclosingTrace.length - 1;
		while (m >= 0 && n >= 0 && trace[m].equals(enclosingTrace[n])) {
			m--;
			n--;
		}
		int framesInCommon = trace.length - 1 - m;
		boolean truncated = printFrames(output, trace, m + 1, prefix);
		if (!truncated && framesInCommon > 0) {
			output.append(prefix)
				.append("\t... ")
				.append(framesInCommon)
				.append(" more")
				.append(System.lineSeparator());
		}
	}

	/*
	 * Prints frames trace[0..count) filtered by excludes and capped at maxLines. Returns
	 * true if there were excluded-filtered frames beyond maxLines (i.e. the output was
	 * cut short because of maxLines rather than ending naturally).
	 */
	boolean printFrames(StringBuilder output, StackTraceElement[] trace, int count, String prefix) {
		int printed = 0;
		int available = 0;
		for (int i = 0; i < count; i++) {
			var element = trace[i];
			if (isExcluded(element)) {
				continue;
			}
			available++;
			if (printed < maxLines) {
				output.append(prefix).append("\tat ").append(element);
				// Eclipse null analysis is too dumb about final fields.
				var packagingData = this.packagingData;
				if (packagingData != null) {
					output.append(' ').append(packagingData.resolve(element));
				}
				output.append(System.lineSeparator());
				printed++;
			}
		}
		if (available > maxLines) {
			output.append(prefix)
				.append("\t... ")
				.append(available - maxLines)
				.append(" frames truncated")
				.append(System.lineSeparator());
			return true;
		}
		return false;
	}

	private boolean isExcluded(StackTraceElement element) {
		if (excludes.isEmpty()) {
			return false;
		}
		String s = element.toString();
		for (var p : excludes) {
			Matcher matcher = p.matcher(s);
			if (matcher.find()) {
				return true;
			}
		}
		return false;
	}

}

/**
 * Reimplements {@link Throwable#printStackTrace()}'s algorithm (header line, frames,
 * common-frame elision against the enclosing trace, "Caused by:"/"Suppressed:" sections,
 * circular reference guard) so that a max line count and frame exclusion patterns can be
 * applied while printing.
 */
final class StandardThrowableFormatter implements ThrowableFormatter {

	private static final String CAUSE_CAPTION = "Caused by: ";

	private static final String SUPPRESSED_CAPTION = "Suppressed: ";

	private final ThrowableFramePrinter framePrinter;

	StandardThrowableFormatter(ThrowableFramePrinter framePrinter) {
		this.framePrinter = framePrinter;
	}

	@Override
	public void formatThrowable(StringBuilder output, Throwable throwable) {
		Set<Throwable> dejaVu = Collections.newSetFromMap(new IdentityHashMap<>());
		dejaVu.add(throwable);
		var trace = throwable.getStackTrace();
		output.append(throwable).append(System.lineSeparator());
		framePrinter.printFrames(output, trace, trace.length, "");
		for (var suppressed : throwable.getSuppressed()) {
			printEnclosed(output, suppressed, trace, SUPPRESSED_CAPTION, "\t", dejaVu);
		}
		var cause = throwable.getCause();
		if (cause != null) {
			printEnclosed(output, cause, trace, CAUSE_CAPTION, "", dejaVu);
		}
	}

	private void printEnclosed(StringBuilder output, Throwable throwable, StackTraceElement[] enclosingTrace,
			String caption, String prefix, Set<Throwable> dejaVu) {
		if (!dejaVu.add(throwable)) {
			output.append(prefix)
				.append("[CIRCULAR REFERENCE: ")
				.append(throwable)
				.append(']')
				.append(System.lineSeparator());
			return;
		}
		var trace = throwable.getStackTrace();
		output.append(prefix).append(caption).append(throwable).append(System.lineSeparator());
		framePrinter.printFramesAgainstEnclosing(output, trace, enclosingTrace, prefix);
		for (var suppressed : throwable.getSuppressed()) {
			printEnclosed(output, suppressed, trace, SUPPRESSED_CAPTION, prefix + "\t", dejaVu);
		}
		var cause = throwable.getCause();
		if (cause != null) {
			printEnclosed(output, cause, trace, CAUSE_CAPTION, prefix, dejaVu);
		}
	}

}

/**
 * Mirrors logback's <code>RootCauseFirstThrowableProxyConverter</code>: same frame
 * trimming/common-frame elision as {@link StandardThrowableFormatter}, but the root cause
 * prints first and each wrapper prints afterward, captioned "Wrapped by: " instead of
 * "Caused by: ". Common-frame elision for a given throwable is still computed against its
 * own direct wrapper's frames (the throwable that has it as {@link Throwable#getCause()})
 * regardless of print order - that relationship, and therefore the elision counts, do not
 * depend on which order things are printed in.
 * <p>
 * A suppressed exception's own "Suppressed: " caption lands on the first line actually
 * printed for it - the root of <em>its</em> own cause chain if it has one, not the
 * suppressed exception's own header - matching logback's
 * <code>recursiveAppendRootCauseFirst</code> exactly (the caption is threaded down
 * through the recursion and only consumed once, at the bottom).
 */
final class RootCauseFirstThrowableFormatter implements ThrowableFormatter {

	private static final String WRAPPED_BY_CAPTION = "Wrapped by: ";

	private static final String SUPPRESSED_CAPTION = "Suppressed: ";

	private final ThrowableFramePrinter framePrinter;

	RootCauseFirstThrowableFormatter(ThrowableFramePrinter framePrinter) {
		this.framePrinter = framePrinter;
	}

	@Override
	public void formatThrowable(StringBuilder output, Throwable throwable) {
		Set<Throwable> dejaVu = Collections.newSetFromMap(new IdentityHashMap<>());
		print(output, throwable, null, null, "", dejaVu);
	}

	private void print(StringBuilder output, Throwable throwable, StackTraceElement @Nullable [] enclosingTrace,
			@Nullable String caption, String prefix, Set<Throwable> dejaVu) {
		if (!dejaVu.add(throwable)) {
			output.append(prefix);
			if (caption != null) {
				output.append(caption);
			}
			output.append("[CIRCULAR REFERENCE: ").append(throwable).append(']').append(System.lineSeparator());
			return;
		}
		var trace = throwable.getStackTrace();
		var cause = throwable.getCause();
		if (cause != null) {
			print(output, cause, trace, caption, prefix, dejaVu);
			caption = null; // consumed by the recursive (deeper) call above
		}
		output.append(prefix);
		if (caption != null) {
			output.append(caption);
		}
		if (cause != null) {
			output.append(WRAPPED_BY_CAPTION);
		}
		output.append(throwable).append(System.lineSeparator());
		if (enclosingTrace == null) {
			framePrinter.printFrames(output, trace, trace.length, prefix);
		}
		else {
			framePrinter.printFramesAgainstEnclosing(output, trace, enclosingTrace, prefix);
		}
		for (var suppressed : throwable.getSuppressed()) {
			print(output, suppressed, trace, SUPPRESSED_CAPTION, prefix + "\t", dejaVu);
		}
	}

}

/**
 * Resolves and caches which jar/module/directory a stack frame's class was loaded from
 * and its reported version, formatted as <code>[location:version]</code>. Named modules
 * (including JDK frames) are resolved for free from the frame itself or the loaded
 * class's {@link Module} without touching a {@link java.security.CodeSource}. Classpath
 * (unnamed module) frames fall back to the class's {@link java.security.CodeSource} and
 * {@link Package#getImplementationVersion()}. Either "location" or "version" is
 * <code>na</code> when unknown, and the whole result is <code>[na:na]</code> when the
 * frame's class cannot be loaded at all (e.g. dynamically generated classes).
 */
final class PackagingDataResolver {

	private final ConcurrentMap<String, String> cache = new ConcurrentHashMap<>();

	String resolve(StackTraceElement element) {
		String className = element.getClassName();
		String cached = cache.get(className);
		if (cached != null) {
			return cached;
		}
		String computed = compute(element);
		cache.put(className, computed);
		return computed;
	}

	private static String compute(StackTraceElement element) {
		String moduleName = element.getModuleName();
		if (moduleName != null) {
			return format(moduleName, element.getModuleVersion());
		}
		Class<?> type = load(element.getClassName());
		if (type == null) {
			return "[na:na]";
		}
		Module module = type.getModule();
		if (module.isNamed()) {
			var descriptor = module.getDescriptor();
			String version = descriptor == null ? null : descriptor.rawVersion().orElse(null);
			return format(module.getName(), version);
		}
		var codeSource = type.getProtectionDomain().getCodeSource();
		if (codeSource == null) {
			return "[na:na]";
		}
		var location = codeSource.getLocation();
		if (location == null) {
			return "[na:na]";
		}
		String path = location.getPath();
		if (path == null || path.isEmpty()) {
			path = location.toString();
		}
		if (path.endsWith("/")) {
			path = path.substring(0, path.length() - 1);
		}
		int slash = path.lastIndexOf('/');
		String name = slash >= 0 && slash < path.length() - 1 ? path.substring(slash + 1) : path;
		var pkg = type.getPackage();
		String version = pkg == null ? null : pkg.getImplementationVersion();
		return format(name, version);
	}

	private static String format(String location, @Nullable String version) {
		return "[" + location + ":" + (version == null || version.isEmpty() ? "na" : version) + "]";
	}

	private static @Nullable Class<?> load(String className) {
		var contextLoader = Thread.currentThread().getContextClassLoader();
		if (contextLoader != null) {
			var type = loadOrNull(className, contextLoader);
			if (type != null) {
				return type;
			}
		}
		return loadOrNull(className, PackagingDataResolver.class.getClassLoader());
	}

	private static @Nullable Class<?> loadOrNull(String className, @Nullable ClassLoader loader) {
		try {
			return Class.forName(className, false, loader);
		}
		catch (ClassNotFoundException | LinkageError | SecurityException e) {
			return null;
		}
	}

}

record SingleKeyValueFormatter(String key, @Nullable String fallback) implements LogFormatter {
	@Override
	public void format(StringBuilder output, LogEvent event) {
		var kvs = event.keyValues();
		formatKeyValues(output, kvs);
	}

	void formatKeyValues(StringBuilder output, KeyValues keyValues) {
		String v = keyValues.getValueOrNull(key);
		if (v == null) {
			v = fallback;
		}
		PercentCodec.encode(output, key, StandardCharsets.UTF_8);
		if (v != null) {
			output.append('=');
			PercentCodec.encode(output, v, StandardCharsets.UTF_8);
		}
	}

}

/**
 * Logback/Log4j2 <code>%X{key}</code> style: just the raw value (or fallback, or empty
 * string), no key prefix, no percent encoding. See
 * {@link LogFormatter.Builder#keyValue(String, String)}.
 */
record LogbackSingleKeyValueFormatter(String key, @Nullable String fallback) implements LogFormatter {

	@Override
	public void format(StringBuilder output, LogEvent event) {
		var kvs = event.keyValues();
		String v = kvs.getValueOrNull(key);
		if (v == null) {
			v = fallback;
		}
		if (v != null) {
			output.append(v);
		}
	}

}