LogfmtFormatterBuilder.java

package io.jstach.rainbowgum.format;

import io.jstach.rainbowgum.LogProperties;
import io.jstach.rainbowgum.LogProperty;

/**
 * Builder to create {@link io.jstach.rainbowgum.LogFormatter }.
 * Formats events as logfmt, one line of key=value pairs per event like Go's log/slog
 * text handler, selected with the logfmt encoder scheme.
 * <p>
 * Each event is written as one line, for example:
 * <code>time=2026-10-05T15:04:05.123Z level=INFO logger=com.example.App thread=main msg="hello world" requestId=42</code>
 * <ul>
 * <li><code>time</code> is the UTC instant with milliseconds.</li>
 * <li><code>level</code> is written by the level formatter, by default
 * <code>TRACE</code>, <code>DEBUG</code>, <code>INFO</code>, <code>WARN</code>, or
 * <code>ERROR</code>. logfmt does not define level names, and tools differ (Go's slog
 * is upper case, logrus lower case).</li>
 * <li>Every key value follows <code>msg</code>, in order. A key value named like one
 * of these fields is prefixed with <code>keyValues.</code>.</li>
 * <li>A throwable adds <code>error</code> (its <code>toString()</code>) and
 * <code>stacktrace</code>.</li>
 * </ul>
 * A value is quoted when it is empty or contains a space, <code>=</code>,
 * <code>"</code>, or a control character. Inside quotes <code>"</code>,
 * <code>\</code>, newline, carriage return, and tab are backslash escaped and other
 * control characters are written as <code>\</code><code>uXXXX</code>, so every event
 * stays on one line. Characters logfmt does not allow in keys, and <code>%</code>,
 * are percent-encoded (<code>a b</code> is <code>a%20b</code>), and an empty key is
 * <code>%</code>, so different keys stay different.
 * <p>
 * Values can be colored like the TTLL formatter; keys, <code>msg</code>,
 * <code>error</code>, and <code>stacktrace</code> are not. Padding from a padding
 * level formatter is written after the level's color.
 * <table class="table">
 * <caption><strong>Properties retrieved from LogProperties</strong></caption>
 * <tr>
 * <th>Property Pattern</th>
 * <th>Type</th>
 * <th>Required</th>
 * <th>Default</th>
 * <th>Description</th>
 * </tr>
 * <tr>
 * <td><code>{@value PROPERTY_levelFormatter }</code></td>
 * <td><code>String (converted)</code></td>
 * <td><code>true</code></td>
 * <td>{@link io.jstach.rainbowgum.LogFormatter.LevelFormatter#of() level_formatter}</td>
 * <td>Level formatter; as a property, a level formatter constant name.</td>
 * </tr>
 * <tr>
 * <td><code>{@value PROPERTY_color }</code></td>
 * <td><code>String (converted)</code></td>
 * <td><code>false</code></td>
 * <td><code>null</code></td>
 * <td>Whether to color values, see {@link TTLL.ColorMode}.</td>
 * </tr>
 * <tr>
 * <td><code>{@value PROPERTY_theme }</code></td>
 * <td><code>String (converted)</code></td>
 * <td><code>false</code></td>
 * <td><code>null</code></td>
 * <td>Which colors, see {@link TTLL.ColorTheme}.</td>
 * </tr>
 * </table>
 */
public final class LogfmtFormatterBuilder implements io.jstach.rainbowgum.LogBuilder<LogfmtFormatterBuilder,io.jstach.rainbowgum.LogFormatter> {

	/**
	 * The properties to be retrieved from config will have
	 * this prefix.
	 */
	static final String PROPERTY_PREFIX = "logging.appender.{name}.encoder.logfmt.";
	
	/**
	 * <code>{@value #PROPERTY_levelFormatter } = io.jstach.rainbowgum.LogFormatter.LevelFormatter</code> Level formatter; as a property, a level formatter constant name.
	 */
	static final String PROPERTY_levelFormatter = PROPERTY_PREFIX + "levelFormatter";
	/**
	 * <code>{@value #PROPERTY_color } = io.jstach.rainbowgum.format.TTLL.ColorMode</code> Whether to color values, see {@link TTLL.ColorMode}.
	 */
	static final String PROPERTY_color = PROPERTY_PREFIX + "color";
	/**
	 * <code>{@value #PROPERTY_theme } = io.jstach.rainbowgum.format.TTLL.ColorTheme</code> Which colors, see {@link TTLL.ColorTheme}.
	 */
	static final String PROPERTY_theme = PROPERTY_PREFIX + "theme";
	
	private final String propertyPrefix;
	final String property_levelFormatter;
	final String property_color;
	final String property_theme;
	
	private final java.lang.String name;
	private io.jstach.rainbowgum.LogFormatter.LevelFormatter levelFormatter = io.jstach.rainbowgum.format.LogfmtFormatter.DEFAULT_LEVEL_FORMATTER;
	private io.jstach.rainbowgum.format.TTLL.@org.jspecify.annotations.Nullable ColorMode color = null;
	private io.jstach.rainbowgum.format.TTLL.@org.jspecify.annotations.Nullable ColorTheme theme = null;
	private io.jstach.rainbowgum.@org.jspecify.annotations.Nullable ServiceRegistry serviceRegistry;
	
	/**
	 * Create a builder for {@code io.jstach.rainbowgum.LogFormatter }.
	 * @param name will fill <code>{name}</code> in <code>logging.appender.{name}.encoder.logfmt.</code>. Encoder name, used for property lookup.
	 */
	public LogfmtFormatterBuilder(
			java.lang.String name
			) {
		try {
			java.util.Map<String,String> prefixParameters = java.util.Map.of(
					"name", name
				);
			this.propertyPrefix = LogProperties.interpolateKey(PROPERTY_PREFIX, prefixParameters);
			this.name = name;
			property_levelFormatter = LogProperties.interpolateKey(PROPERTY_levelFormatter, prefixParameters);
			property_color = LogProperties.interpolateKey(PROPERTY_color, prefixParameters);
			property_theme = LogProperties.interpolateKey(PROPERTY_theme, prefixParameters);
		}
		catch (IllegalArgumentException e) {
			throw LogProperty.ValidationException.of(this.getClass(), e);
		}
	}


	/**
	 * Sets serviceRegistry.
	 * Where themes chosen by name in properties are looked up.
	 * Default is <code>null</code>.
	 * @param serviceRegistry not configurable through properties Where themes chosen by name in properties are looked up.
	 * @return this builder.
	 */
	public LogfmtFormatterBuilder serviceRegistry(io.jstach.rainbowgum.@org.jspecify.annotations.Nullable ServiceRegistry serviceRegistry) {
		this.serviceRegistry = serviceRegistry;
		return this;
	}

	
	/**
	 * Sets <strong>required</strong> levelFormatter.
	 * Level formatter; as a property, a level formatter constant name.
	 * Default is {@link io.jstach.rainbowgum.LogFormatter.LevelFormatter#of() level_formatter}.
	 * @param levelFormatter <code>{@value #PROPERTY_levelFormatter } = io.jstach.rainbowgum.LogFormatter.LevelFormatter</code> Level formatter; as a property, a level formatter constant name.
	 * @return this builder.
	 */
	public LogfmtFormatterBuilder levelFormatter(io.jstach.rainbowgum.LogFormatter.LevelFormatter levelFormatter) {
		this.levelFormatter = levelFormatter;
		return this;
	}
	
	/**
	 * Sets color.
	 * Whether to color values, see {@link TTLL.ColorMode}.
	 * Default is <code>null</code>.
	 * @param color <code>{@value #PROPERTY_color } = io.jstach.rainbowgum.format.TTLL.ColorMode</code> Whether to color values, see {@link TTLL.ColorMode}.
	 * @return this builder.
	 */
	public LogfmtFormatterBuilder color(io.jstach.rainbowgum.format.TTLL.@org.jspecify.annotations.Nullable ColorMode color) {
		this.color = color;
		return this;
	}
	
	/**
	 * Sets theme.
	 * Which colors, see {@link TTLL.ColorTheme}.
	 * Default is <code>null</code>.
	 * @param theme <code>{@value #PROPERTY_theme } = io.jstach.rainbowgum.format.TTLL.ColorTheme</code> Which colors, see {@link TTLL.ColorTheme}.
	 * @return this builder.
	 */
	public LogfmtFormatterBuilder theme(io.jstach.rainbowgum.format.TTLL.@org.jspecify.annotations.Nullable ColorTheme theme) {
		this.theme = theme;
		return this;
	}
	
	/**
	 * Creates {@code io.jstach.rainbowgum.LogFormatter } from this builder.
	 * @return {@code io.jstach.rainbowgum.LogFormatter }.
	 */
	public io.jstach.rainbowgum.LogFormatter build() {
		try {
			return io.jstach.rainbowgum.format.LogfmtFormatter.of(
					this.name
					, io.jstach.rainbowgum.LogProperty.require(property_levelFormatter, this.levelFormatter)
					, this.color
					, this.theme
					, this.serviceRegistry
					);
		}
		catch (IllegalArgumentException | LogProperty.PropertyMissingException e) {
			throw LogProperty.ValidationException.of(this.getClass(), e);
		}
	}
	
	@Override
	public LogfmtFormatterBuilder fromProperties(LogProperties properties) {
		var __v = LogProperty.Validator.of(this.getClass());
		var _levelFormatter = properties.forKey(property_levelFormatter).ofString().map(_v -> io.jstach.rainbowgum.format.LogfmtFormatter.convertLevelFormatter(_v)).or(this.levelFormatter).validate(__v);
		var _color = properties.forKey(property_color).ofString().map(_v -> io.jstach.rainbowgum.format.LogfmtFormatter.convertColor(_v)).or(this.color).validateIfError(__v);
		var _theme = properties.forKey(property_theme).ofString().map(_v -> io.jstach.rainbowgum.format.LogfmtFormatter.convertTheme(_v)).or(this.theme).validateIfError(__v);
		__v.validate();
		this.levelFormatter = _levelFormatter.value();
		this.color = _color.valueOrNull();
		this.theme = _theme.valueOrNull();
		return this;
	}
	
	/**
	 * The interpolated property prefix: {@value #PROPERTY_PREFIX}.
	 * @return resolved prefix which should end with a "<code>.</code>".
	 */
	@Override
	public String propertyPrefix() {
		return this.propertyPrefix;
	}
	
}