AbstractFilteringLogger.java
package io.jstach.rainbowgum.slf4j.spi;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.Marker;
import org.slf4j.event.Level;
import org.slf4j.spi.LoggingEventBuilder;
import org.slf4j.spi.NOPLoggingEventBuilder;
import io.jstach.rainbowgum.RainbowGum;
import io.jstach.rainbowgum.slf4j.spi.LoggerDecoratorService.DepthAwareEventBuilder;
import io.jstach.rainbowgum.slf4j.spi.LoggerDecoratorService.DepthAwareLogger;
/**
* A base class for {@link LoggerDecoratorService} implementations that need to filter or
* enrich log calls without depending on {@link org.slf4j.helpers.AbstractLogger}'s
* internal dispatch, whose stack depth to the real caller is an unversioned SLF4J
* implementation detail rather than something Rainbow Gum controls or tests.
* <p>
* Every convenience method ({@code info(String)}, {@code error(String,Object)}, marker
* variants, ...) funnels through exactly one internal dispatch method before
* {@link #isEnabled(Level, Marker)} and {@link #decorate(LoggingEventBuilder, Marker)}
* run, so the frame count from any convenience method to caller-info capture is fixed and
* verified once by this class's own tests. Subclasses never need to reason about stack
* depth. Non-marker convenience methods pass a {@code null} marker to both hooks.
* <p>
* The fluent {@code atInfo()} style API ({@link #makeLoggingEventBuilder(Level)}) is a
* transparent pass-through to {@link #delegate()} and does <strong>not</strong> invoke
* either hook, since there is no populated event (or marker) to filter or decorate yet at
* that point.
*
* @see LoggerDecoratorService LoggerDecoratorService for a full example and how to
* register a decorator.
*/
/*
* The trace/debug/info/warn/error(Marker, String, Object, Object) overrides below match
* org.slf4j.Logger's own real parameter names (marker, format, arg1, arg2) exactly - the
* slf4j-api jar just isn't compiled with -parameters, so errorprone falls back to
* synthetic arg0..arg3 names and flags a mismatch that isn't actually there.
*/
@SuppressWarnings("OverridingMethodInconsistentArgumentNamesChecker")
public abstract class AbstractFilteringLogger implements Logger, DepthAwareLogger {
/*
* Frames between builder.log() and the real caller: this class's own normalizedLog(),
* plus the public convenience method (info(), error(), marker variant, ...) that
* called it. Both are declared in this class - never delegate one convenience method
* to another, or that adds an untracked frame. Fixed and verified by
* AbstractFilteringLoggerTest.
*/
private static final int DEPTH = 2;
private final DepthAwareLogger delegate;
/**
* Creates a filtering logger wrapping the given delegate.
* @param delegate logger to wrap.
*/
protected AbstractFilteringLogger(DepthAwareLogger delegate) {
this.delegate = delegate;
}
/**
* The wrapped logger that ultimately receives log calls.
* @return delegate.
*/
public DepthAwareLogger delegate() {
return delegate;
}
/**
* Cheap pre-check run before any {@link LoggingEventBuilder} is built, analogous to
* {@code isInfoEnabled()}/{@code isInfoEnabled(Marker)}. Override to filter out
* events (sampling, per-logger overrides, per-marker rules, ...) without paying for
* message formatting or key-value collection.
* @param level level of the event.
* @param marker marker passed to the convenience method that was called, or null if a
* non-marker overload was used.
* @return true if enabled and {@link #decorate(LoggingEventBuilder, Marker)} may run.
*/
protected boolean isEnabled(Level level, @Nullable Marker marker) {
return true;
}
/**
* Called with the already-populated builder (message, arguments and cause already set
* from whatever convenience method arity the caller used). Override to add key
* values, rewrite the message, etc. The marker is passed here rather than added to
* the builder automatically, since there is no default meaningful behavior for it
* (e.g. a decorator can call
* {@code builder.addKeyValue("_marker", marker.toString())} if it wants markers
* surfaced at all).
* <p>
* {@code builder} also implements {@link DepthAwareEventBuilder}, whose
* {@code message()}/{@code throwable()}/{@code arguments()}/{@code keyValues()}
* expose what has already been set - needed for read-modify-write overrides (e.g.
* prefixing the message, redacting an argument) since plain
* {@link LoggingEventBuilder} has no such accessors. Use
* {@link DepthAwareEventBuilder}'s static {@code message(LoggingEventBuilder)}-style
* helpers rather than casting directly, in case a future delegate ever passes through
* a plain {@link LoggingEventBuilder} instead.
* <p>
* <strong>Do not call {@code builder.log()} directly from this method</strong> -
* doing so adds an untracked stack frame and caller info will point at the wrong
* line. To mutate and let it log normally, just mutate {@code builder} and return
* {@code true}. To log it yourself (e.g. conditionally, or more than once) use
* {@link #log(LoggingEventBuilder)} instead of {@code builder.log()}, and return
* {@code false} so this class does not log it again. To drop the event entirely, do
* neither and return {@code false}.
* @param builder builder about to be logged.
* @param marker marker passed to the convenience method that was called, or null if a
* non-marker overload was used.
* @return true to have this class call {@code builder.log()} after this method
* returns; false if you called {@link #log(LoggingEventBuilder)} yourself or want to
* drop the event.
* @see DepthAwareEventBuilder
*/
protected boolean decorate(LoggingEventBuilder builder, @Nullable Marker marker) {
return true;
}
/**
* Logs a builder obtained from a {@link #decorate(LoggingEventBuilder, Marker)}
* override that returned {@code false}, with caller-info depth adjusted for the extra
* frames both {@link #decorate(LoggingEventBuilder, Marker)} and this method itself
* add. Never call {@code builder.log()} directly from
* {@link #decorate(LoggingEventBuilder, Marker)}.
* @param builder builder to log, previously handed to
* {@link #decorate(LoggingEventBuilder, Marker)}.
*/
protected final void log(LoggingEventBuilder builder) {
/*
* +2, not +1: the call chain here is normalizedLog() -> decorate() -> log() ->
* builder.log(), i.e. both decorate() and this method are extra frames beyond the
* normalizedLog()+convenience-method pair DEPTH already accounts for.
*/
DepthAwareEventBuilder.setDepth(builder, DEPTH + 2);
builder.log();
}
@Override
public String getName() {
return delegate.getName();
}
/**
* A no-op: unlike {@link org.slf4j.helpers.AbstractLogger}-based decorators, this
* class's caller-info depth is a fixed internal constant rather than derived from the
* wrap-count
* {@link LoggerDecoratorService#decorate(RainbowGum, DepthAwareLogger, int)} passes
* in, so there is nothing to recreate.
* @param depth ignored.
* @return this.
*/
@Override
public Logger withDepth(int depth) {
return this;
}
@Override
public boolean isEnabledForLevel(Level level) {
return isEnabledForLevel(level, null);
}
private boolean isEnabledForLevel(Level level, @Nullable Marker marker) {
return delegate.isEnabledForLevel(level) && isEnabled(level, marker);
}
@Override
public LoggingEventBuilder makeLoggingEventBuilder(Level level) {
if (!delegate.isEnabledForLevel(level)) {
return NOPLoggingEventBuilder.singleton();
}
return delegate.makeLoggingEventBuilder(level);
}
/*
* LoggingEventBuilder mutates itself and returns this, so a builder call is never
* actually "unused" just because its return value is discarded here - the real
* mistake to guard against is forgetting the terminal log()/decorate() call, which
* this method never does.
*/
@SuppressWarnings("CheckReturnValue")
private void normalizedLog(Level level, @Nullable Marker marker, @Nullable String message, @Nullable Object arg1,
@Nullable Object arg2, @Nullable Object @Nullable [] argArray, @Nullable Throwable throwable) {
if (!isEnabledForLevel(level, marker)) {
return;
}
var builder = DepthAwareEventBuilder.setDepth(delegate.makeLoggingEventBuilder(level), DEPTH);
if (message != null) {
builder.setMessage(message);
}
if (arg1 != null) {
builder.addArgument(arg1);
}
if (arg2 != null) {
builder.addArgument(arg2);
}
if (argArray != null) {
for (var a : argArray) {
builder.addArgument(a);
}
}
if (throwable != null) {
builder.setCause(throwable);
}
if (decorate(builder, marker)) {
builder.log();
}
}
@Override
public boolean isTraceEnabled() {
return isEnabledForLevel(Level.TRACE);
}
@Override
public boolean isTraceEnabled(Marker marker) {
return isEnabledForLevel(Level.TRACE, marker);
}
@Override
public void trace(String msg) {
normalizedLog(Level.TRACE, null, msg, null, null, null, null);
}
@Override
public void trace(String format, Object arg) {
normalizedLog(Level.TRACE, null, format, arg, null, null, null);
}
@Override
public void trace(String format, Object arg1, Object arg2) {
normalizedLog(Level.TRACE, null, format, arg1, arg2, null, null);
}
@Override
public void trace(String format, Object... arguments) {
normalizedLog(Level.TRACE, null, format, null, null, arguments, null);
}
@Override
public void trace(String msg, Throwable t) {
normalizedLog(Level.TRACE, null, msg, null, null, null, t);
}
@Override
public void trace(Marker marker, String msg) {
normalizedLog(Level.TRACE, marker, msg, null, null, null, null);
}
@Override
public void trace(Marker marker, String format, Object arg) {
normalizedLog(Level.TRACE, marker, format, arg, null, null, null);
}
@Override
public void trace(Marker marker, String format, Object arg1, Object arg2) {
normalizedLog(Level.TRACE, marker, format, arg1, arg2, null, null);
}
@Override
public void trace(Marker marker, String format, Object... argArray) {
normalizedLog(Level.TRACE, marker, format, null, null, argArray, null);
}
@Override
public void trace(Marker marker, String msg, Throwable t) {
normalizedLog(Level.TRACE, marker, msg, null, null, null, t);
}
@Override
public LoggingEventBuilder atTrace() {
return makeLoggingEventBuilder(Level.TRACE);
}
@Override
public boolean isDebugEnabled() {
return isEnabledForLevel(Level.DEBUG);
}
@Override
public boolean isDebugEnabled(Marker marker) {
return isEnabledForLevel(Level.DEBUG, marker);
}
@Override
public void debug(String msg) {
normalizedLog(Level.DEBUG, null, msg, null, null, null, null);
}
@Override
public void debug(String format, Object arg) {
normalizedLog(Level.DEBUG, null, format, arg, null, null, null);
}
@Override
public void debug(String format, Object arg1, Object arg2) {
normalizedLog(Level.DEBUG, null, format, arg1, arg2, null, null);
}
@Override
public void debug(String format, Object... arguments) {
normalizedLog(Level.DEBUG, null, format, null, null, arguments, null);
}
@Override
public void debug(String msg, Throwable t) {
normalizedLog(Level.DEBUG, null, msg, null, null, null, t);
}
@Override
public void debug(Marker marker, String msg) {
normalizedLog(Level.DEBUG, marker, msg, null, null, null, null);
}
@Override
public void debug(Marker marker, String format, Object arg) {
normalizedLog(Level.DEBUG, marker, format, arg, null, null, null);
}
@Override
public void debug(Marker marker, String format, Object arg1, Object arg2) {
normalizedLog(Level.DEBUG, marker, format, arg1, arg2, null, null);
}
@Override
public void debug(Marker marker, String format, Object... argArray) {
normalizedLog(Level.DEBUG, marker, format, null, null, argArray, null);
}
@Override
public void debug(Marker marker, String msg, Throwable t) {
normalizedLog(Level.DEBUG, marker, msg, null, null, null, t);
}
@Override
public LoggingEventBuilder atDebug() {
return makeLoggingEventBuilder(Level.DEBUG);
}
@Override
public boolean isInfoEnabled() {
return isEnabledForLevel(Level.INFO);
}
@Override
public boolean isInfoEnabled(Marker marker) {
return isEnabledForLevel(Level.INFO, marker);
}
@Override
public void info(String msg) {
normalizedLog(Level.INFO, null, msg, null, null, null, null);
}
@Override
public void info(String format, Object arg) {
normalizedLog(Level.INFO, null, format, arg, null, null, null);
}
@Override
public void info(String format, Object arg1, Object arg2) {
normalizedLog(Level.INFO, null, format, arg1, arg2, null, null);
}
@Override
public void info(String format, Object... arguments) {
normalizedLog(Level.INFO, null, format, null, null, arguments, null);
}
@Override
public void info(String msg, Throwable t) {
normalizedLog(Level.INFO, null, msg, null, null, null, t);
}
@Override
public void info(Marker marker, String msg) {
normalizedLog(Level.INFO, marker, msg, null, null, null, null);
}
@Override
public void info(Marker marker, String format, Object arg) {
normalizedLog(Level.INFO, marker, format, arg, null, null, null);
}
@Override
public void info(Marker marker, String format, Object arg1, Object arg2) {
normalizedLog(Level.INFO, marker, format, arg1, arg2, null, null);
}
@Override
public void info(Marker marker, String format, Object... argArray) {
normalizedLog(Level.INFO, marker, format, null, null, argArray, null);
}
@Override
public void info(Marker marker, String msg, Throwable t) {
normalizedLog(Level.INFO, marker, msg, null, null, null, t);
}
@Override
public LoggingEventBuilder atInfo() {
return makeLoggingEventBuilder(Level.INFO);
}
@Override
public boolean isWarnEnabled() {
return isEnabledForLevel(Level.WARN);
}
@Override
public boolean isWarnEnabled(Marker marker) {
return isEnabledForLevel(Level.WARN, marker);
}
@Override
public void warn(String msg) {
normalizedLog(Level.WARN, null, msg, null, null, null, null);
}
@Override
public void warn(String format, Object arg) {
normalizedLog(Level.WARN, null, format, arg, null, null, null);
}
@Override
public void warn(String format, Object arg1, Object arg2) {
normalizedLog(Level.WARN, null, format, arg1, arg2, null, null);
}
@Override
public void warn(String format, Object... arguments) {
normalizedLog(Level.WARN, null, format, null, null, arguments, null);
}
@Override
public void warn(String msg, Throwable t) {
normalizedLog(Level.WARN, null, msg, null, null, null, t);
}
@Override
public void warn(Marker marker, String msg) {
normalizedLog(Level.WARN, marker, msg, null, null, null, null);
}
@Override
public void warn(Marker marker, String format, Object arg) {
normalizedLog(Level.WARN, marker, format, arg, null, null, null);
}
@Override
public void warn(Marker marker, String format, Object arg1, Object arg2) {
normalizedLog(Level.WARN, marker, format, arg1, arg2, null, null);
}
@Override
public void warn(Marker marker, String format, Object... argArray) {
normalizedLog(Level.WARN, marker, format, null, null, argArray, null);
}
@Override
public void warn(Marker marker, String msg, Throwable t) {
normalizedLog(Level.WARN, marker, msg, null, null, null, t);
}
@Override
public LoggingEventBuilder atWarn() {
return makeLoggingEventBuilder(Level.WARN);
}
@Override
public boolean isErrorEnabled() {
return isEnabledForLevel(Level.ERROR);
}
@Override
public boolean isErrorEnabled(Marker marker) {
return isEnabledForLevel(Level.ERROR, marker);
}
@Override
public void error(String msg) {
normalizedLog(Level.ERROR, null, msg, null, null, null, null);
}
@Override
public void error(String format, Object arg) {
normalizedLog(Level.ERROR, null, format, arg, null, null, null);
}
@Override
public void error(String format, Object arg1, Object arg2) {
normalizedLog(Level.ERROR, null, format, arg1, arg2, null, null);
}
@Override
public void error(String format, Object... arguments) {
normalizedLog(Level.ERROR, null, format, null, null, arguments, null);
}
@Override
public void error(String msg, Throwable t) {
normalizedLog(Level.ERROR, null, msg, null, null, null, t);
}
@Override
public void error(Marker marker, String msg) {
normalizedLog(Level.ERROR, marker, msg, null, null, null, null);
}
@Override
public void error(Marker marker, String format, Object arg) {
normalizedLog(Level.ERROR, marker, format, arg, null, null, null);
}
@Override
public void error(Marker marker, String format, Object arg1, Object arg2) {
normalizedLog(Level.ERROR, marker, format, arg1, arg2, null, null);
}
@Override
public void error(Marker marker, String format, Object... argArray) {
normalizedLog(Level.ERROR, marker, format, null, null, argArray, null);
}
@Override
public void error(Marker marker, String msg, Throwable t) {
normalizedLog(Level.ERROR, marker, msg, null, null, null, t);
}
@Override
public LoggingEventBuilder atError() {
return makeLoggingEventBuilder(Level.ERROR);
}
}