LogbackJsonEncoder.java
package io.jstach.rainbowgum.json.encoder;
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.LogProperties;
import io.jstach.rainbowgum.LogProvider;
import io.jstach.rainbowgum.annotation.LogConfigurable;
import io.jstach.rainbowgum.json.JsonBuffer;
import io.jstach.rainbowgum.json.JsonBuffer.ExtendedFieldPrefix;
import io.jstach.rainbowgum.json.JsonBuffer.JSONToken;
/**
* A JSON encoder resembling
* <a href="https://logback.qos.ch/manual/encoders.html#JsonEncoder">Logback's own
* opinionated <code>ch.qos.logback.classic.encoder.JsonEncoder</code></a>.
* <p>
* Fields written: <code>timestamp</code> (epoch millis), <code>nanoseconds</code>
* (nanosecond-of-second, for sub-millisecond precision), <code>level</code>,
* <code>threadName</code>, <code>loggerName</code>, <code>mdc</code> (nested object of
* the event's key values), <code>message</code> (already formatted), and
* <code>throwable</code> (nested object with <code>className</code>, <code>message</code>
* and a <code>stepArray</code> of stack frame objects, recursing into <code>cause</code>
* if present).
* <p>
* Unlike Logback's encoder, which is independently configurable per field via
* {@code <withXxx>} elements, <code>sequenceNumber</code> is intentionally not
* implemented (Rainbow Gum has no context-wide sequence number generator, see
* {@code %lsn}/{@code %sn} in the pattern module discussion), <code>context</code> is
* omitted (Rainbow Gum has no equivalent to Logback's {@code LoggerContext}), and
* <code>kvpList</code> is omitted because Rainbow Gum does not distinguish MDC-origin key
* values from SLF4J fluent {@code addKeyValue} ones - both already end up merged into
* {@link LogEvent#keyValues()}, which this encoder writes as <code>mdc</code>.
*/
public final class LogbackJsonEncoder extends LogEncoder.AbstractEncoder<JsonBuffer> {
/**
* Logback JSON encoder URI scheme.
*/
public static final String LOGBACK_SCHEME = "logback";
private final boolean prettyPrint;
private final int maxBufferSize;
LogbackJsonEncoder(boolean prettyPrint, int maxBufferSize) {
super();
this.prettyPrint = prettyPrint;
this.maxBufferSize = maxBufferSize;
}
/**
* Creates a Logback JSON 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<LogbackJsonEncoder> of(Consumer<LogbackJsonEncoderBuilder> consumer) {
return (s, c) -> {
var b = new LogbackJsonEncoderBuilder(s);
consumer.accept(b);
return b.fromProperties(c.properties()).build();
};
}
/**
* Creates a Logback JSON encoder.
* @param name property name prefix.
* @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 + "logback.")
static LogbackJsonEncoder of(@LogConfigurable.KeyParameter String name, @Nullable Boolean prettyPrint,
@Nullable Integer maxBufferSize) {
prettyPrint = prettyPrint == null ? false : prettyPrint;
int _maxBufferSize = maxBufferSize == null ? -1 : maxBufferSize;
return new LogbackJsonEncoder(prettyPrint, _maxBufferSize);
}
@Override
protected JsonBuffer doBuffer(BufferHints hints) {
return new JsonBuffer(this.prettyPrint, ExtendedFieldPrefix.UNDERSCORE, 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.writeLong("timestamp", now.toEpochMilli(), index, 0);
/*
* The sub-second remainder alongside the millisecond "timestamp" field above, not
* total nanoseconds since epoch - getNano() is exactly right here.
*/
@SuppressWarnings("JavaInstantGetSecondsGetNano")
int nanoAdjustment = now.getNano();
index = buffer.writeInt("nanoseconds", nanoAdjustment, index, 0);
index = buffer.write("level", LevelFormatter.toString(event.level()), index);
index = buffer.write("threadName", event.threadName(), index);
index = buffer.write("loggerName", event.loggerName(), index);
int mdcIndex = buffer.writeObjectStart("mdc", index, 0);
var kvs = event.keyValues();
kvs.forEach((src, k, v, idx, buf) -> buf.write(k, v, idx, 0), mdcIndex, buffer);
buffer.writeObjectEnd();
index++;
index = buffer.write("message", formattedMessage.toString(), index);
if (t != null) {
index = writeThrowable("throwable", t, buffer, index);
}
if (index > 0 && prettyPrint) {
buffer.writeLineFeed();
}
buffer.write(JSONToken.OBJECT_END);
buffer.writeLineFeed();
}
private static int writeThrowable(String field, Throwable t, JsonBuffer buffer, int index) {
int throwableIndex = buffer.writeObjectStart(field, index, 0);
throwableIndex = buffer.write("className", t.getClass().getName(), throwableIndex, 0);
throwableIndex = buffer.write("message", t.getMessage(), throwableIndex, 0);
int arrayIndex = buffer.writeArrayStart("stepArray", throwableIndex, 0);
for (var frame : t.getStackTrace()) {
int frameIndex = buffer.writeArrayElementObjectStart(arrayIndex);
frameIndex = buffer.write("className", frame.getClassName(), frameIndex, 0);
frameIndex = buffer.write("methodName", frame.getMethodName(), frameIndex, 0);
frameIndex = buffer.write("fileName", frame.getFileName(), frameIndex, 0);
buffer.writeInt("lineNumber", frame.getLineNumber(), frameIndex, 0);
buffer.writeArrayElementObjectEnd();
arrayIndex++;
}
buffer.writeArrayEnd();
throwableIndex++;
var cause = t.getCause();
/*
* Deliberately identity, not equals(): a self-referential cause (getCause() ==
* this, a real JDK-permitted case) must break the recursion here, not merely a
* value-equal-but-distinct Throwable.
*/
@SuppressWarnings("ReferenceEquality")
boolean selfReferential = cause == t;
if (cause != null && !selfReferential) {
writeThrowable("cause", cause, buffer, throwableIndex);
}
buffer.writeObjectEnd();
return index + 1;
}
}