LogEvent.java
package io.jstach.rainbowgum;
import java.lang.StackWalker.Option;
import java.lang.StackWalker.StackFrame;
import java.lang.System.Logger.Level;
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
import io.jstach.rainbowgum.KeyValues.MutableKeyValues;
import io.jstach.rainbowgum.LogEvent.Caller;
import io.jstach.rainbowgum.annotation.EnumAlias;
/**
* A LogEvent is a container for a single call to a logger. An event should not be created
* unless a route or logger is actually enabled.
*
* @author agentgt
* @apiNote LogEvent is currently sealed. The reason there are so many static creation
* methods is for optimization purposes because other than actual outputting creating
* events is generally the most expensive operation (mostly in terms of memory) a logging
* system does.
* @see LogMessageFormatter
*/
public sealed interface LogEvent {
/**
* Creates a log event.
* @param timestamp time of event
* @param threadName or empty string
* @param threadId thread id or 0 if that cannot be resolved.
* @param level the logging level.
* @param loggerName the name of the logger which is usually a class name.
* @param formattedMessage the unformatted message.
* @param keyValues key values that come from MDC or an SLF4J Event Builder.
* @param throwable an exception if passed maybe <code>null</code>.
* @return event
* @see LevelResolver
* @apiNote the message is already assumed to be formatted as no arguments are passed.
*/
public static LogEvent of(Instant timestamp, String threadName, long threadId, System.Logger.Level level,
String loggerName, @Nullable String formattedMessage, KeyValues keyValues, @Nullable Throwable throwable) {
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, formattedMessage, keyValues,
throwable);
}
/**
* Creates a log event.
* @param timestamp time of event
* @param threadName or empty string
* @param threadId thread id or 0 if that cannot be resolved.
* @param level the logging level.
* @param loggerName the name of the logger which is usually a class name.
* @param message the unformatted message.
* @param keyValues key values that come from MDC or an SLF4J Event Builder.
* @param messageFormatter formatter to use for rendering a message when
* #{@link LogEvent#formattedMessage(StringBuilder)} is called.
* @param arg1 argument that will be passed to messageFormatter.
* @return event
* @see LevelResolver
* @see LogMessageFormatter
*/
public static LogEvent ofOneArg(Instant timestamp, String threadName, long threadId, System.Logger.Level level,
String loggerName, @Nullable String message, KeyValues keyValues, LogMessageFormatter messageFormatter,
@Nullable Object arg1) {
if (arg1 instanceof Throwable t) {
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues, t);
}
if (message == null) {
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues, null);
}
return new OneArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
messageFormatter, null, arg1);
}
/**
* Creates a log event.
* @param timestamp time of event
* @param threadName or empty string
* @param threadId thread id or 0 if that cannot be resolved.
* @param level the logging level.
* @param loggerName the name of the logger which is usually a class name.
* @param message the unformatted message.
* @param keyValues key values that come from MDC or an SLF4J Event Builder.
* @param messageFormatter formatter to use for rendering a message when
* #{@link LogEvent#formattedMessage(StringBuilder)} is called.
* @param arg1 argument that will be passed to messageFormatter.
* @param arg2 argument that will be passed to messageFormatter.
* @return event
* @see LevelResolver
* @see LogMessageFormatter
*/
public static LogEvent ofTwoArgs(Instant timestamp, String threadName, long threadId, System.Logger.Level level,
String loggerName, @Nullable String message, KeyValues keyValues, LogMessageFormatter messageFormatter,
@Nullable Object arg1, @Nullable Object arg2) {
if (arg2 instanceof Throwable t) {
if (message == null) {
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues, t);
}
return new OneArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
messageFormatter, t, arg1);
}
if (message == null) {
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues, null);
}
return new TwoArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
messageFormatter, null, arg1, arg2);
}
/**
* Creates a log event with everything specified.
* @param timestamp time of event
* @param threadName or empty string
* @param threadId thread id or 0 if that cannot be resolved.
* @param level the logging level.
* @param loggerName the name of the logger which is usually a class name.
* @param message the unformatted message.
* @param keyValues key values that come from MDC or an SLF4J Event Builder.
* @param throwable or <code>null</code>.
* @param messageFormatter formatter to use for rendering a message when
* #{@link LogEvent#formattedMessage(StringBuilder)} is called.
* @param args an array of arguments that will be passed to messageFormatter. The
* contents maybe null elements but the array itself should not be null.
* @return event
* @see LevelResolver
* @see LogMessageFormatter
*/
public static LogEvent ofAll(Instant timestamp, String threadName, long threadId, System.Logger.Level level,
String loggerName, @Nullable String message, KeyValues keyValues, @Nullable Throwable throwable,
LogMessageFormatter messageFormatter, @SuppressWarnings("exports") @Nullable List<@Nullable Object> args) {
if (args == null) {
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
throwable);
}
int length = args.size();
if (throwable == null && length > 0 && args.get(length - 1) instanceof Throwable t) {
throwable = t;
length = length - 1;
}
if (message == null) {
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
throwable);
}
return switch (length) {
case 0 ->
new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues, throwable);
case 1 -> new OneArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
messageFormatter, throwable, args.get(0));
case 2 -> new TwoArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
messageFormatter, throwable, args.get(0), args.get(1));
default -> new ArrayArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
messageFormatter, throwable, args.toArray(), length);
};
}
/**
* Creates a log event with everything specified.
* @param timestamp time of event
* @param threadName or empty string
* @param threadId thread id or 0 if that cannot be resolved.
* @param level the logging level.
* @param loggerName the name of the logger which is usually a class name.
* @param message the unformatted message.
* @param keyValues key values that come from MDC or an SLF4J Event Builder.
* @param throwable or <code>null</code>.
* @param messageFormatter formatter to use for rendering a message when
* #{@link LogEvent#formattedMessage(StringBuilder)} is called.
* @param args an array of arguments that will be passed to messageFormatter. The
* contents maybe null elements but the array itself should not be null.
* @return event
* @see LevelResolver
* @see LogMessageFormatter
*/
public static LogEvent ofAll(Instant timestamp, String threadName, long threadId, System.Logger.Level level,
String loggerName, @Nullable String message, KeyValues keyValues, @Nullable Throwable throwable,
LogMessageFormatter messageFormatter, @SuppressWarnings("exports") @Nullable Object @Nullable [] args) {
if (args == null) {
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
throwable);
}
int length = args.length;
if (throwable == null && length > 0 && args[length - 1] instanceof Throwable t) {
throwable = t;
length = length - 1;
}
if (message == null) {
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
throwable);
}
return switch (length) {
case 0 ->
new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues, throwable);
case 1 -> new OneArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
messageFormatter, throwable, args[0]);
case 2 -> new TwoArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
messageFormatter, throwable, args[0], args[1]);
default -> new ArrayArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
messageFormatter, throwable, args, length);
};
}
/**
* Creates a new event with the caller info attached. <em> This method always returns
* a new event does not check if the original has caller info. </em>
* @param event original event.
* @param caller caller info.
* @return new log event.
*/
public static LogEvent withCaller(LogEvent event, Caller caller) {
return new StackFrameLogEvent(event, caller);
}
/**
* Timestamp when the event was created.
* @return instant when the event was created.
*/
public Instant timestamp();
/**
* Name of the thread.
* @return thread name.
* @apiNote this maybe empty and often is if virtual threads are used.
*/
public String threadName();
/**
* Thread id.
* @return thread id.
*/
public long threadId();
/**
* The logging level. {@linkplain java.lang.System.Logger.Level#ALL} and
* {@linkplain java.lang.System.Logger.Level#OFF} should not be returned as they have
* special meaning.
* @return level.
*/
public System.Logger.Level level();
/**
* Name of logger.
* @return name of logger.
*/
public String loggerName();
/**
* Unformatted message.
* @return unformatted message
* @see #formattedMessage(StringBuilder)
*/
public String message();
/**
* Unformatted message or <code>null</code> if no message was passed into the event.
* @return unformatted message or <code>null</code>.
* @see #formattedMessage(StringBuilder)
*/
default @Nullable String messageOrNull() {
if (hasMessage()) {
return message();
}
return null;
}
/**
* If the event has a message or not. Because the return of {@link #message()} is non
* null there is no way to detect that a message is missing.
* @return <code>true</code> if this event has a message.
*/
default public boolean hasMessage() {
return true;
}
/**
* Appends the formatted message.
* @param sb string builder to use.
* @see LogMessageFormatter
*/
public void formattedMessage(StringBuilder sb);
/**
* Appends the formatted message.
* @param sb string builder to use.
* @param fallbackMessage string to use if the message was never set.
* @see LogMessageFormatter
* @see #hasMessage()
*/
default void formattedMessage(StringBuilder sb, String fallbackMessage) {
if (hasMessage()) {
formattedMessage(sb);
}
else {
sb.append(fallbackMessage);
}
}
/**
* Throwable at the time of the event passed from the logger.
* @return if the event does not have a throwable <code>null</code> will be returned.
*/
public @Nullable Throwable throwableOrNull();
/**
* Key values that usually come from MDC or an SLF4J Event Builder.
* @return key values.
*/
public KeyValues keyValues();
/**
* Returns info about caller or <code>null</code> if not supported.
* @return caller info
*/
default @Nullable Caller callerOrNull() {
return null;
}
/**
* Freeze will return a LogEvent that is safe to use in a different thread. Usually
* this entails copying the data or checking if it is already immutable. Freeze should
* be called before passing an event to an {@link LogPublisher.AsyncLogPublisher}.
* @return thread safe log event.
*/
public LogEvent freeze();
/**
* Freeze and replace with the given timestamp.
* @param timestamp instant to replace timestamp in this.
* @return a copy of this with the given timestamp.
*/
public LogEvent freeze(Instant timestamp);
/**
* Caller info usually derived from Stack walking.
*/
public sealed interface Caller {
/**
* Creates caller info from a stack frame.
* @param stackFrame stack frame must have
* {@link java.lang.StackWalker.Option#RETAIN_CLASS_REFERENCE}.
* @return caller info.
*/
public static Caller of(StackFrame stackFrame) {
return new StackFrameCallerInfo(stackFrame);
}
/**
* Returns caller from a certain depth or <code>null</code>
* @param depth how deep in the stack to pull stack frame.
* @return caller or <code>null</code>.
*/
public static @Nullable Caller ofDepthOrNull(int depth) {
return StackFrameCallerInfo.stackWalker.<@Nullable Caller>walk(
s -> s.skip(depth + 1).limit(1).map(f -> Caller.of(f)).findFirst().orElse(null));
}
/**
* See {@link StackFrame#getClassName()}.
* @return class name.
*/
public String className();
/**
* See {@link StackFrame#getFileName()}.
* @return file name.
*/
public @Nullable String fileNameOrNull();
/**
* See {@link StackFrame#getLineNumber()}.
* @return line number.
*/
public int lineNumber();
/**
* See {@link StackFrame#getMethodName()}.
* @return method name.
*/
public String methodName();
/**
* Make the caller info immutable.
* @return immutable caller info and if this is already immutable return this.
*/
public Caller freeze();
/**
* Convenience toString for caller.
* @param caller if caller is <code>null</code> the string "null" will be
* returned.
* @return string representation.
*/
public static String toString(@Nullable Caller caller) {
/*
* TODO maybe move this to formatters?
*/
if (caller == null)
return "null";
return caller.fileNameOrNull() + ":" + caller.lineNumber() + "/" + caller.className() + "."
+ caller.methodName();
}
/**
* Caller info strategies, resolved from {@value LogProperties#CALLER_PREFIX}.
* {@link #BASIC} is the only strategy implemented today (a stack-walk down to the
* calling class/method/line) - kept as an enum rather than a boolean so a future,
* richer strategy (matching what some other logging frameworks gather from the
* stack) can be added without a breaking property-format change.
*/
public enum CallerType {
/**
* Caller info is not computed.
*/
@EnumAlias("false")
NONE,
/**
* Caller info is computed as a single stack frame (class, method, line).
*/
@EnumAlias("true")
BASIC;
static CallerType parse(String value) {
String v = value.toUpperCase(Locale.ROOT);
return switch (v) {
case "TRUE" -> BASIC;
case "FALSE" -> NONE;
default -> LogProperty.enumValue(CallerType.class, value, "true", "false");
};
}
}
}
}
record FrozenCallerInfo(String className, @Nullable String fileNameOrNull, int lineNumber,
String methodName) implements LogEvent.Caller {
@Override
public Caller freeze() {
return this;
}
}
record StackFrameCallerInfo(StackFrame stackFrame) implements LogEvent.Caller {
static final StackWalker stackWalker = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE);
@Override
public String className() {
return stackFrame.getClassName();
}
@Override
public @Nullable String fileNameOrNull() {
/*
* TODO checker did not know file name is null.
*/
return stackFrame.getFileName();
}
@Override
public int lineNumber() {
return stackFrame.getLineNumber();
}
@Override
public String methodName() {
return stackFrame.getMethodName();
}
@Override
public Caller freeze() {
return new FrozenCallerInfo(className(), fileNameOrNull(), lineNumber(), methodName());
}
}
record OneArgLogEvent(Instant timestamp, String threadName, long threadId, System.Logger.Level level, String loggerName,
String message, KeyValues keyValues, LogMessageFormatter messageFormatter, @Nullable Throwable throwableOrNull,
@Nullable Object arg1) implements LogEvent {
OneArgLogEvent {
Objects.requireNonNull(timestamp, "timestamp");
Objects.requireNonNull(threadName, "threadName");
Objects.requireNonNull(level, "level");
Objects.requireNonNull(loggerName, "loggerName");
Objects.requireNonNull(message, "message");
Objects.requireNonNull(keyValues, "keyValues");
Objects.requireNonNull(messageFormatter, "messageFormatter");
}
@Override
public void formattedMessage(StringBuilder sb) {
messageFormatter.format(sb, message, arg1);
}
@Override
public LogEvent freeze() {
return freeze(timestamp);
}
@Override
public LogEvent freeze(Instant timestamp) {
StringBuilder sb = new StringBuilder(message.length());
formattedMessage(sb);
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, sb.toString(),
keyValues.freeze(), throwableOrNull);
}
}
record TwoArgLogEvent(Instant timestamp, String threadName, long threadId, System.Logger.Level level, String loggerName,
String message, KeyValues keyValues, LogMessageFormatter messageFormatter, @Nullable Throwable throwableOrNull,
@Nullable Object arg1, @Nullable Object arg2) implements LogEvent {
TwoArgLogEvent {
Objects.requireNonNull(timestamp, "timestamp");
Objects.requireNonNull(threadName, "threadName");
Objects.requireNonNull(level, "level");
Objects.requireNonNull(loggerName, "loggerName");
Objects.requireNonNull(message, "message");
Objects.requireNonNull(keyValues, "keyValues");
Objects.requireNonNull(messageFormatter, "messageFormatter");
}
@Override
public void formattedMessage(StringBuilder sb) {
messageFormatter.format(sb, message, arg1, arg2);
}
@Override
public LogEvent freeze() {
return freeze(timestamp);
}
@Override
public LogEvent freeze(Instant timestamp) {
StringBuilder sb = new StringBuilder(message.length());
formattedMessage(sb);
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, sb.toString(),
keyValues.freeze(), throwableOrNull);
}
}
@SuppressWarnings("ArrayRecordComponent")
record ArrayArgLogEvent(Instant timestamp, String threadName, long threadId, System.Logger.Level level,
String loggerName, String message, KeyValues keyValues, LogMessageFormatter messageFormatter,
@Nullable Throwable throwableOrNull, @Nullable Object[] args, int length) implements LogEvent {
ArrayArgLogEvent {
Objects.requireNonNull(timestamp, "timestamp");
Objects.requireNonNull(threadName, "threadName");
Objects.requireNonNull(level, "level");
Objects.requireNonNull(loggerName, "loggerName");
Objects.requireNonNull(message, "message");
Objects.requireNonNull(keyValues, "keyValues");
Objects.requireNonNull(messageFormatter, "messageFormatter");
}
@Override
public void formattedMessage(StringBuilder sb) {
messageFormatter.formatArray(sb, message, args, length);
}
public int argCount() {
return args.length;
}
@Override
public LogEvent freeze() {
return freeze(timestamp);
}
@Override
public LogEvent freeze(Instant timestamp) {
StringBuilder sb = new StringBuilder(message.length());
formattedMessage(sb);
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, sb.toString(),
keyValues.freeze(), throwableOrNull);
}
}
record DefaultLogEvent(Instant timestamp, String threadName, long threadId, System.Logger.Level level,
String loggerName, @Nullable String formattedMessage, KeyValues keyValues,
@Nullable Throwable throwableOrNull) implements LogEvent {
DefaultLogEvent {
Objects.requireNonNull(timestamp, "timestamp");
Objects.requireNonNull(threadName, "threadName");
Objects.requireNonNull(level, "level");
Objects.requireNonNull(loggerName, "loggerName");
Objects.requireNonNull(keyValues, "keyValues");
}
@Override
public void formattedMessage(StringBuilder sb) {
sb.append(this.formattedMessage);
}
@Override
public String message() {
return "" + this.formattedMessage;
}
@Override
public @Nullable String messageOrNull() {
return this.formattedMessage;
}
@Override
public boolean hasMessage() {
return this.formattedMessage != null;
}
@Override
public LogEvent freeze() {
if (keyValues instanceof MutableKeyValues mkvs) {
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, formattedMessage,
mkvs.freeze(), throwableOrNull);
}
return this;
}
@Override
public LogEvent freeze(Instant timestamp) {
if (keyValues instanceof MutableKeyValues mkvs) {
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, formattedMessage,
mkvs.freeze(), throwableOrNull);
}
return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, formattedMessage, keyValues,
throwableOrNull);
}
}
record StackFrameLogEvent(LogEvent event, Caller callerOrNull) implements LogEvent {
StackFrameLogEvent {
Objects.requireNonNull(callerOrNull, "caller");
}
@Override
public Instant timestamp() {
return event.timestamp();
}
@Override
public String threadName() {
return event.threadName();
}
@Override
public long threadId() {
return event.threadId();
}
@Override
public Level level() {
return event.level();
}
@Override
public String loggerName() {
return event.loggerName();
}
@Override
public String message() {
return event.message();
}
@Override
public void formattedMessage(StringBuilder sb) {
event.formattedMessage(sb);
}
@Override
public @Nullable Throwable throwableOrNull() {
return event.throwableOrNull();
}
@Override
public KeyValues keyValues() {
return event.keyValues();
}
@Override
@SuppressWarnings("ReferenceEquality")
public LogEvent freeze() {
var e = event.freeze();
var info = callerOrNull.freeze();
if (e == event && info == callerOrNull) {
return this;
}
return new StackFrameLogEvent(e, info);
}
@Override
@SuppressWarnings("ReferenceEquality")
public LogEvent freeze(Instant timestamp) {
var e = event.freeze(timestamp);
var info = callerOrNull.freeze();
if (e == event && info == callerOrNull) {
return this;
}
return new StackFrameLogEvent(e, info);
}
}