LogstashEncoder.java
package io.jstach.rainbowgum.json.encoder;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Set;
import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
import io.jstach.rainbowgum.LogEncoder;
import io.jstach.rainbowgum.LogEvent;
import io.jstach.rainbowgum.LogFormatter.LevelFormatter;
import io.jstach.rainbowgum.LogFormatter.ThrowableFormatter;
import io.jstach.rainbowgum.LogProperties;
import io.jstach.rainbowgum.LogProvider;
import io.jstach.rainbowgum.annotation.LogConfigurable;
import io.jstach.rainbowgum.annotation.LogConfigurable.ConvertParameter;
import io.jstach.rainbowgum.json.JsonBuffer;
import io.jstach.rainbowgum.json.JsonBuffer.ExtendedFieldPrefix;
import io.jstach.rainbowgum.json.JsonBuffer.JSONToken;
/**
* A JSON encoder in the format produced by
* <a href="https://github.com/logfellow/logstash-logback-encoder">
* logstash-logback-encoder</a>'s <code>LogstashEncoder</code>.
* <p>
* Fields written: <code>@timestamp</code> (<code>ISO_OFFSET_DATE_TIME</code>, using
* {@link #zoneId()} - defaulting to the system default zone, matching the reference
* implementation), <code>@version</code> (constant <code>"1"</code>),
* <code>message</code>, <code>logger_name</code>, <code>thread_name</code>,
* <code>level</code>, <code>level_value</code> (Logback's conventional level integers:
* TRACE=5000, DEBUG=10000, INFO=20000, WARN=30000, ERROR=40000), and - only if a
* throwable was logged - <code>stack_trace</code> (the full stack trace as a single
* string).
* <p>
* <code>tags</code> (from SLF4J markers) is not implemented since Rainbow Gum core has no
* built-in {@code org.slf4j.Marker} support. Key values are written as top-level fields
* using their own key name, matching the reference implementation, except a key value
* named like a field this encoder writes, which goes in a nested <code>keyValues</code>
* object so no field is written twice.
*/
public final class LogstashEncoder extends LogEncoder.AbstractEncoder<JsonBuffer> {
/**
* Logstash encoder URI scheme.
*/
public static final String LOGSTASH_SCHEME = "logstash";
private static final KeyValuesWriter KEY_VALUES = new KeyValuesWriter(KeyValuesWriter.Placement.NESTED, Set
.of("@timestamp", "@version", "message", "logger_name", "thread_name", "level", "level_value", "stack_trace"),
0);
private final ZoneId zoneId;
private final boolean prettyPrint;
private final DateTimeFormatter timeFormatter;
private final int maxBufferSize;
LogstashEncoder(ZoneId zoneId, boolean prettyPrint, int maxBufferSize) {
super();
this.zoneId = zoneId;
this.prettyPrint = prettyPrint;
this.timeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(zoneId);
this.maxBufferSize = maxBufferSize;
}
/**
* The zone used to render <code>@timestamp</code>.
* @return zone id.
*/
public ZoneId zoneId() {
return this.zoneId;
}
/**
* Creates a Logstash encoder using a lambda for easier registration. The builder will
* have properties loaded after the consumer has configured the builder.
* @param consumer lambda to configure builder.
* @return encoder provider.
*/
public static LogProvider<LogstashEncoder> of(Consumer<LogstashEncoderBuilder> consumer) {
return (s, c) -> {
var b = new LogstashEncoderBuilder(s);
consumer.accept(b);
return b.fromProperties(c.properties()).build();
};
}
/**
* Creates a Logstash encoder.
* @param name property name prefix.
* @param zoneId zone used for <code>@timestamp</code>, defaults to the system default
* zone.
* @param prettyPrint <code>true</code> will pretty print the JSON, default is false.
* @param maxBufferSize maximum buffer size - a soft ceiling checked between events,
* not a hard cap enforced on any single event (see
* {@link io.jstach.rainbowgum.LogEncoder.Buffer#isOversized()}). A negative value
* (the default) disables this entirely.
* @return encoder.
*/
@LogConfigurable(prefix = LogProperties.ENCODER_PREFIX + "logstash.")
static LogstashEncoder of(@LogConfigurable.KeyParameter String name,
@ConvertParameter("convertZoneId") @Nullable ZoneId zoneId, @Nullable Boolean prettyPrint,
@Nullable Integer maxBufferSize) {
prettyPrint = prettyPrint == null ? false : prettyPrint;
zoneId = zoneId == null ? ZoneId.systemDefault() : zoneId;
int _maxBufferSize = maxBufferSize == null ? -1 : maxBufferSize;
return new LogstashEncoder(zoneId, prettyPrint, _maxBufferSize);
}
static ZoneId convertZoneId(@Nullable String zoneId) {
return zoneId == null ? ZoneId.systemDefault() : ZoneId.of(zoneId);
}
@Override
protected JsonBuffer doBuffer(BufferHints hints) {
return new JsonBuffer(this.prettyPrint, ExtendedFieldPrefix.AT, maxBufferSize);
}
@Override
protected void doEncode(LogEvent event, JsonBuffer buffer) {
buffer.clear();
var formattedMessage = buffer.getFormattedMessageBuilder();
event.formattedMessage(formattedMessage);
var now = event.timestamp();
var t = event.throwableOrNull();
buffer.write(JSONToken.OBJECT_START);
int index = 0;
index = buffer.write("@timestamp", timeFormatter.format(now), index);
index = buffer.write("@version", "1", index);
index = buffer.write("message", formattedMessage.toString(), index);
index = buffer.write("logger_name", event.loggerName(), index);
index = buffer.write("thread_name", event.threadName(), index);
index = buffer.write("level", LevelFormatter.toString(event.level()), index);
index = buffer.writeInt("level_value", levelValue(event.level()), index, 0);
if (t != null) {
var stackTrace = new StringBuilder();
ThrowableFormatter.appendThrowable(stackTrace, t);
index = buffer.write("stack_trace", stackTrace.toString(), index);
}
index = KEY_VALUES.write(event.keyValues(), buffer, index);
if (index > 0 && prettyPrint) {
buffer.writeLineFeed();
}
buffer.write(JSONToken.OBJECT_END);
buffer.writeLineFeed();
}
private static int levelValue(java.lang.System.Logger.Level level) {
/*
* Logback's conventional level integers, which is what logstash-logback-encoder
* writes as level_value.
*/
return switch (level) {
case TRACE -> 5000;
case DEBUG -> 10000;
case INFO -> 20000;
case WARNING -> 30000;
case ERROR -> 40000;
case ALL -> Integer.MIN_VALUE;
case OFF -> Integer.MAX_VALUE;
};
}
}