LogAppender.java
package io.jstach.rainbowgum;
import java.lang.System.Logger.Level;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import io.jstach.rainbowgum.annotation.CaseChanging;
/**
* Appenders are guaranteed to be written synchronously much like an actor in actor
* concurrency. They safely hold onto and communicate with the encoder and output.
* Appenders largely deal with correct locking, buffer reuse and flushing.
* {@linkplain LogAppender.AppenderFlag Flags } can be set to control the behavior the
* appenders and publishers can request different appender behavior through the flags.
*
* @see LogAppender.AppenderFlag
* @apiNote because appenders require complicated implementation and to guarantee
* integrity the implementations are encapsulated (sealed).
*/
public sealed interface LogAppender extends LogLifecycle {
/**
* Default Console appender name.
*/
static final String CONSOLE_APPENDER_NAME = "console";
/**
* Removes the current thread's encoding buffers from every appender of the globally
* bound {@link RainbowGum}, so a pooled thread does not keep a buffer it may never
* use again. Only {@link AppenderType#LOCK_THREAD_LOCAL_BUFFER} and
* {@link AppenderType#SYNCHRONIZED_THREAD_LOCAL_BUFFER} appenders keep per thread
* buffers; the next event the thread logs simply creates a new one. Does nothing if
* no Rainbow Gum is bound. Logging context such as MDC is cleared separately with
* {@link LogEventFactory.KeyValuesContributor#clear()}.
* {@snippet :
* LogAppender.clearThreadLocals();
* KeyValuesContributor.global().clear();
* }
*/
public static void clearThreadLocals() {
var gum = RainbowGumHolder.peek();
if (gum == null) {
return;
}
for (var appender : gum.config().serviceRegistry().find(LogAppender.class)) {
InternalLogAppender.of(appender).clearThreadLocals();
}
}
/**
* Default output file appender name.
*/
static final String FILE_APPENDER_NAME = "file";
/**
* Output appender property.
*/
static final String APPENDER_OUTPUT_PROPERTY = LogProperties.APPENDER_OUTPUT_PROPERTY;
/**
* Encoder appender property.
*/
static final String APPENDER_ENCODER_PROPERTY = LogProperties.APPENDER_ENCODER_PROPERTY;
/**
* Appender flags. A list of flags (usually comma separated).
* @see AppenderFlag
*/
static final String APPENDER_FLAGS_PROPERTY = LogProperties.APPENDER_FLAGS_PROPERTY;
/**
* Appender type.
* @see AppenderType
*/
static final String APPENDER_TYPE_PROPERTY = LogProperties.APPENDER_TYPE_PROPERTY;
/**
* Batch of events. <strong>DO NOT MODIFY THE ARRAY</strong>. Do not use the
* <code>length</code> of the passed in array but instead use <code>count</code>
* parameter.
* @param events an array guaranteed to be smaller than count.
* @param count the number of items.
*/
public void append(LogEvent[] events, int count);
/**
* Appends a single event.
* @param event event.
*/
public void append(LogEvent event);
/**
* Boolean like flags for appender that can be set with
* {@link LogAppender#APPENDER_FLAGS_PROPERTY}. Unlike {@link AppenderType} more than
* one flag can be set at once.
*/
@CaseChanging
public enum AppenderFlag {
/**
* By default the appender will call flush on each item appended or if in async
* batch mode for each batch. This flag disables that behavior so that flushing is
* left up to the output (or an external mechanism) instead.
*/
DISABLE_IMMEDIATE_FLUSH,
/**
* The appender will drop events on reentry which happens if an appender during
* its append causes recursive appending in the same thread. This is an analog to
* what
* <a href="https://logback.qos.ch/manual/appenders.html#AppenderBase">Logback
* does by default</a>. Note that this is done using {@link ReentrantLock} and not
* ThreadLocal like logback <strong>and is not done by default hence the
* flag!</strong>
* <p>
* This flag is to allow outputs that do logging themselves. For performance
* reasons and to allow async publishers it is recommended that you fix the output
* code such that it does not do logging. This flag is ignored if
* {@link #REENTRY_LOG} is set.
* <p>
* <strong>This flag will not fix outputs causing lool like logging if an async
* publisher is used!</strong> That is why it is recommended you fix the output by
* dropping events that would cause infinite loop like logging.
* @see #REENTRY_LOG
*/
REENTRY_DROP,
/**
* The appender will log events as errors to std error on reentry which happens if
* an appender during its append causes recursive appending in the same thread.
* This is an analog to what
* <a href="https://logback.qos.ch/manual/appenders.html#AppenderBase">Logback
* does by default</a>. Note that this is done using {@link ReentrantLock} and not
* ThreadLocal like logback <strong>and is not done by default hence the
* flag!</strong> This flag is to resolve failures of outputs that then do
* logging.
* <p>
* This flag takes precedence over {@link #REENTRY_DROP}.
*/
REENTRY_LOG;
static Set<AppenderFlag> parse(Collection<String> value) {
if (value.isEmpty()) {
return EnumSet.noneOf(AppenderFlag.class);
}
var s = EnumSet.noneOf(AppenderFlag.class);
for (var v : value) {
s.add(parse(v));
}
return s;
}
static AppenderFlag parse(String value) {
return LogProperty.enumValue(AppenderFlag.class, value);
}
}
/**
* The buffer/locking strategy an appender uses. Unlike {@link AppenderFlag} exactly
* one is in effect for a given appender - set on the {@link LogAppender.Builder} (or
* via {@link LogAppender#APPENDER_TYPE_PROPERTY}) at construction time and fixed for
* the appender's lifetime; a publisher cannot change it afterward.
*/
@CaseChanging
public enum AppenderType {
/**
* The appender will create a single buffer that will be reused and will be
* protected by the appenders locking.
*/
REUSE_BUFFER,
/**
* The appender will give each thread its own reusable buffer (a
* {@link ThreadLocal}) instead of allocating a new buffer per event. Encoding is
* done <strong>outside</strong> the appender's lock (the thread's buffer is only
* visited by that thread so no protection is needed while encoding) with the lock
* only held for the final write to the output.
* <p>
* The same {@link ThreadLocal} buffer is used regardless of whether the calling
* thread is a platform or virtual thread. A virtual thread's entry becomes
* collectible once the thread itself terminates, and a typical unit of work (e.g.
* one HTTP request) logs several times on the same thread, so reusing the buffer
* across those calls still pays off even for short-lived virtual threads.
* <p>
* This is the default type - see {@link #SYNCHRONIZED_THREAD_LOCAL_BUFFER} for
* the alternative lock-kind opt-in.
*/
LOCK_THREAD_LOCAL_BUFFER,
/**
* Like {@link #LOCK_THREAD_LOCAL_BUFFER} (a reused per-thread buffer, encoding
* done outside any lock) except the final write to the output is protected by a
* plain {@code synchronized} block (the JVM's intrinsic monitor) instead of a
* {@link ReentrantLock}.
* <p>
* The Java language has no way to acquire a monitor in one method call and
* release it in another, so this appender's critical sections are written as
* literal {@code synchronized} blocks rather than going through a shared lock
* abstraction the way {@link #LOCK_THREAD_LOCAL_BUFFER} does.
* {@link AppenderFlag#REENTRY_DROP} and {@link AppenderFlag#REENTRY_LOG} are
* still honored though - {@link Thread#holdsLock(Object)} is the
* {@code synchronized} equivalent of {@code ReentrantLock}'s
* {@code isHeldByCurrentThread()}, so reentrancy is detected the same way.
* <p>
* Motivated by Log4j2's own garbage-free appenders using {@code synchronized}
* rather than a {@code java.util.concurrent} lock around their buffer-transfer
* step, and confirmed by real-workload benchmarking to outperform
* {@link ReentrantLock} under platform-thread contention - this was the default
* for a time. Real-workload benchmarking under virtual threads found the
* opposite, a large and reproducible loss versus {@link ReentrantLock} for
* reasons not fully understood (classic JEP 491 pinning was checked and ruled
* out), so {@link #LOCK_THREAD_LOCAL_BUFFER} is the default now and this type is
* an opt-in for platform-thread-heavy deployments that want the edge.
* <p>
* <strong>Explicitly setting this type honors it</strong> even on a JDK where
* {@code synchronized} still pins the carrier platform thread when called from a
* virtual thread (before <a href="https://openjdk.org/jeps/491">JEP 491</a>,
* finalized in JDK 24) - the one exception is
* {@code LogProperties#GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY}: when that global
* property is active it downgrades even an explicit request for this type to
* {@link #LOCK_THREAD_LOCAL_BUFFER}, since its whole point is a hard guarantee
* independent of anything else in the configuration.
*/
SYNCHRONIZED_THREAD_LOCAL_BUFFER,
/**
* Like {@link #LOCK_THREAD_LOCAL_BUFFER} (encoding done outside the lock, the
* lock only guards the final write to the output) except the buffer is allocated
* fresh for every single event instead of being cached in a {@link ThreadLocal}.
* <p>
* For deployments that want a hard guarantee of never using {@link ThreadLocal}
* anywhere in the logging path - {@link #REUSE_BUFFER} is the existing
* no-{@code ThreadLocal} option, but it holds its lock across the entire
* encode-then-write critical section (the single shared buffer must stay
* protected for as long as anything is writing into it), so it pays for that
* guarantee with lock contention proportional to encoding cost, not just I/O
* cost. This type keeps {@link #LOCK_THREAD_LOCAL_BUFFER}'s low-contention shape
* (encode into a buffer nothing else can see, lock only for the write) while
* dropping the {@link ThreadLocal} - the buffer is simply a local variable, not
* cached anywhere, so nothing needs to be evicted or leaked-if-forgotten either.
* The tradeoff is a fresh buffer allocation (and whatever it grows to internally)
* on every single event instead of amortizing that allocation across a thread's
* whole lifetime.
* @apiNote not the default - {@link #LOCK_THREAD_LOCAL_BUFFER}'s reused buffer is
* the better choice unless avoiding {@link ThreadLocal} entirely is a hard
* requirement, not just a preference.
*/
LOCK_NEW_BUFFER,
/*
* Real-workload benchmarking under virtual threads found
* SYNCHRONIZED_THREAD_LOCAL_BUFFER and LOCK_THREAD_LOCAL_BUFFER each winning by a
* real, repeatable, double-digit-percentage margin on their own platform (native
* image, HotSpot respectively) and losing by a comparable margin on the other - a
* genuine cross-over, not just a shrinking gap, so no single fixed choice serves
* both well. Matters most for something distributed as more than one build of the
* same artifact for the same deployment - a native executable and a plain jar,
* say - where picking correctly per build would otherwise mean either shipping
* different configuration for each or sniffing the platform in application code.
* Detection is the same org.graalvm.nativeimage.imagecode system property check
* every native-image-aware framework uses to avoid a hard dependency on GraalVM's
* own SDK.
*/
/**
* Picks whichever concrete type actually performs best for the platform this
* process is currently running on, decided once, at appender construction time,
* not re-checked afterward. <strong>Currently</strong> that means
* {@link #SYNCHRONIZED_THREAD_LOCAL_BUFFER} when running as a GraalVM native
* image, or {@link #LOCK_THREAD_LOCAL_BUFFER} otherwise - but that specific
* mapping is an implementation detail of this heuristic, not a contract. The
* whole point of this type is to track whichever choice is actually best, so
* which concrete type a given platform resolves to today may change in a future
* release without notice, as benchmarking improves or platforms evolve.
* <strong>If your deployment needs a specific type to always be used, pick that
* type explicitly instead of relying on what {@code AUTO_DETECT} happens to
* resolve to right now.</strong>
* <p>
* <strong>Not the default</strong> - requires explicitly requesting this type,
* programmatically or via {@link LogAppender#APPENDER_TYPE_PROPERTY}. Downgraded
* the same as an explicit request for whichever type it currently resolves to
* would be: by {@code LogProperties#GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY} if
* it resolves to {@link #SYNCHRONIZED_THREAD_LOCAL_BUFFER}, and by
* {@code LogProperties#GLOBAL_THREADLOCAL_DISABLED_PROPERTY} either way.
*/
AUTO_DETECT;
static AppenderType parse(String value) {
return LogProperty.enumValue(AppenderType.class, value);
}
}
/**
* Creates a builder.
* @param name appender name.
* @return builder.
*/
public static Builder builder(String name) {
/*
* Eagerly triggers LogProperties' own key-parameter-value validation now,
* matching every annotation-processor-generated Builder's own constructor, which
* does the same via its own eager interpolateKey call: needed because this
* hand-written Builder can otherwise skip every keyed property lookup entirely
* when every field is set explicitly, never validating the name at all. Wrapped
* into a ValidationException the same way a generated builder's constructor is,
* so a bad name reports the same exception type regardless of whether it came
* from logging.appenders (LogAppenderRegistry's own validateNames call) or a
* direct programmatic builder(name) call like this one.
*/
try {
LogProperties.interpolateNamedKey(LogProperties.APPENDER_PREFIX, name);
}
catch (IllegalArgumentException e) {
throw LogProperty.ValidationException.of(LogAppender.class, e);
}
return new Builder(name);
}
/**
* Builder for creating standard appenders. Whatever is not set explicitly is resolved
* from properties keyed under {@link LogProperties#APPENDER_PREFIX} for this
* builder's name (see {@link #fromProperties(LogProperties)}), and failing that from
* a small set of defaults: no encoder resolves one from the output's own type, no
* flags means none are set, and no appender type means
* {@link AppenderType#LOCK_THREAD_LOCAL_BUFFER} - or {@link AppenderType#AUTO_DETECT}
* instead, if {@link LogProperties#GLOBAL_OPTIMIZE_PROPERTY} is enabled. There is
* deliberately no such default for output, since a required source is not something a
* generic named appender can safely guess, except for the well known
* {@code "console"}/ {@code "file"} names, which register their own fallback via
* {@link #outputDefault(LogProvider)}.
*/
public static final class Builder implements LogBuilder<Builder, LogAppender> {
private @Nullable LogProvider<? extends LogOutput> output = null;
/*
* A weaker fallback than an explicit output(...) call: consulted only if neither
* that nor APPENDER_OUTPUT_PROPERTY resolves anything. Package-private, not part
* of the public Builder API: DefaultAppenderRegistry is the only caller, for
* "console"'s stdout default, where the precedence is deliberately the opposite
* of an ordinary explicit value (the property is meant to override this default,
* not the other way around).
*/
private @Nullable LogProvider<? extends LogOutput> outputDefault = null;
private @Nullable LogProvider<? extends LogEncoder> encoder = null;
private @Nullable EnumSet<AppenderFlag> flags = null;
private @Nullable AppenderType appenderType = null;
private final String name;
private Builder(String name) {
this.name = name;
}
Builder outputDefault(LogProvider<? extends LogOutput> outputDefault) {
this.outputDefault = outputDefault;
return this;
}
/**
* Name of the appender.
* @return name.
*/
public String name() {
return this.name;
}
/**
* Sets output.
* @param output output.
* @return builder.
*/
public Builder output(LogProvider<? extends LogOutput> output) {
this.output = output;
return this;
}
/**
* Sets output.
* @param output output.
* @return builder.
*/
public Builder output(LogOutput output) {
this.output = LogProvider.of(output);
return this;
}
/**
* Sets formatter as encoder.
* @param formatter formatter to be converted to encoder.
* @return builder.
* @see LogEncoder#of(LogFormatter)
*/
public Builder formatter(LogFormatter formatter) {
this.encoder = LogEncoder.of(formatter);
return this;
}
/**
* Sets formatter as encoder.
* @param formatter formatter to be converted to encoder.
* @return builder.
* @see LogEncoder#of(LogFormatter)
*/
public Builder formatter(LogFormatter.EventFormatter formatter) {
this.encoder = LogEncoder.of(formatter);
return this;
}
/**
* Sets encoder.
* @param encoder encoder not <code>null</code>.
* @return builder.
*/
public Builder encoder(LogProvider<? extends LogEncoder> encoder) {
this.encoder = encoder;
return this;
}
/**
* Sets encoder.
* @param encoder encoder not <code>null</code>.
* @return builder.
*/
public Builder encoder(LogEncoder encoder) {
this.encoder = LogProvider.of(encoder);
return this;
}
/**
* Sets appender flags.
* @param flags flags will replace all flags currently set.
* @return this.
*/
public Builder flags(Collection<AppenderFlag> flags) {
_flags().addAll(flags);
return this;
}
private EnumSet<AppenderFlag> _flags() {
EnumSet<AppenderFlag> flags = this.flags;
if (flags == null) {
this.flags = flags = EnumSet.noneOf(AppenderFlag.class);
}
return flags;
}
/**
* Adds a flag.
* @param flag flag.
* @return this.
*/
public Builder flag(AppenderFlag flag) {
_flags().add(flag);
return this;
}
/**
* Sets the appender type (buffer/locking strategy). If not set it is resolved
* from {@link LogAppender#APPENDER_TYPE_PROPERTY} or otherwise defaults to
* {@link AppenderType#LOCK_THREAD_LOCAL_BUFFER} ({@link AppenderType#AUTO_DETECT}
* instead if {@link LogProperties#GLOBAL_OPTIMIZE_PROPERTY} is enabled). Calling
* this method always wins over both.
* @param appenderType appender type.
* @return this.
*/
public Builder appenderType(AppenderType appenderType) {
this.appenderType = appenderType;
return this;
}
@Override
public String propertyPrefix() {
return LogProperties.APPENDER_PREFIX;
}
/**
* Fills in whatever of flags/appender type is not already explicitly set on this
* builder from properties keyed under {@link #propertyPrefix()} for this
* builder's name; an already-set field always wins over the property, matching
* every other generated builder's {@code fromProperties} in this project. Both
* are resolved and validated together against one {@link LogProperty.Validator},
* so a builder with both malformed reports both in a single exception rather than
* just the first one reached.
* <p>
* Output/encoder are deliberately not handled here even though they too are keyed
* under {@link #propertyPrefix()}: turning either into a concrete, registered
* {@link LogOutput}/{@link LogEncoder} needs a {@link LogConfig} (to actually
* look up the URI scheme), which this method does not have. {@link #build()}
* resolves those two once a real {@link LogConfig} is available, using the same
* property keys.
* @param properties properties to resolve unset fields from.
* @return this.
*/
@Override
public Builder fromProperties(LogProperties properties) {
/*
* Register every result with the validator first, and only call validate(),
* which throws one aggregate exception up front if anything registered is
* Missing (when added via add(...)) or an Error (either method), before
* extracting any individual value below. Extracting a value via
* value()/valueOrNull() throws immediately for a still-unhandled Error (see
* LogProperty.Result.Error's own valueOrNull()), which would otherwise let
* the first bad property escape unwrapped before the Validator ever got a
* chance to collect the rest: the same ordering the annotation processor
* generates for every other builder in this project.
*/
var validator = LogProperty.Validator.of(LogAppender.class);
var flagsResult = flags == null ? properties.forKey(APPENDER_FLAGS_PROPERTY, name)
.ofList()
.map(AppenderFlag::parse)
.validateIfError(validator) : null;
var appenderTypeResult = appenderType == null ? properties.forKey(APPENDER_TYPE_PROPERTY, name)
.ofString()
.map(AppenderType::parse)
.validateIfError(validator) : null;
validator.validate();
if (flagsResult != null) {
var resolved = flagsResult.valueOrNull();
// AppenderFlag.parse always returns an EnumSet (empty or not), so
// EnumSet.copyOf is always safe here, never the "non-EnumSet and empty"
// case it rejects.
if (resolved != null) {
flags = EnumSet.copyOf(resolved);
}
}
if (appenderTypeResult != null) {
appenderType = appenderTypeResult.valueOrNull();
}
return this;
}
/**
* Builds.
* @return an appender factory.
*/
public LogProvider<LogAppender> build() {
/*
* We need to capture parameters since appender creation needs to be lazy, and
* a copy is made below (rather than mutating this builder's own fields) so a
* shared/reused Builder instance is never mutated by a later
* fromProperties(...) call the lazy lambda makes once a real LogConfig is
* available.
*/
var _name = name;
var _output = output;
var _outputDefault = outputDefault;
var _encoder = encoder;
var _flags = flags;
var _appenderType = appenderType;
/*
* TODO should we use the parent name for resolution?
*/
return (n, config) -> {
var b = new Builder(_name);
b.flags = _flags;
b.appenderType = _appenderType;
b.fromProperties(config.properties());
LogOutput output = LogProvider.provideOrNull(_output, _name, config);
if (output == null) {
/*
* A malformed (not just missing) property must throw here,
* immediately, regardless of whether a default exists below: e.g.
* "console" with a genuinely bad logging.appender.console.output.uri
* must fail loudly, not silently fall back to stdout. Only the
* Missing case falls through to outputDefault.
*/
output = switch (outputProperty(_name, config)) {
case LogProperty.Result.Success<LogOutput> s -> s.value();
case LogProperty.Result.Missing<LogOutput> m -> null;
case LogProperty.Result.Error<LogOutput> e -> e.value();
};
}
if (output == null) {
output = LogProvider.provideOrNull(_outputDefault, _name, config);
}
if (output == null) {
/*
* No explicit value, no property, no default: the same missing
* -property failure a required property with no fallback produces
* anywhere else. Re-deriving the same (definitely still Missing)
* result and calling value() throws it unwrapped, no Validator
* involved, same as before this refactor.
*/
output = DefaultAppenderRegistry.rawValue(outputProperty(_name, config)).value();
}
final LogOutput finalOutput = output;
LogEncoder explicitEncoder = _encoder != null ? LogProvider.provideOrNull(_encoder, _name, config)
: null;
LogEncoder encoder;
if (finalOutput instanceof LogOutput.ProvidesEncoder pe
&& pe.policy() == LogOutput.ProvidesEncoder.Policy.MANDATORY) {
boolean propertyWired = !(encoderProperty(_name, config) instanceof LogProperty.Result.Missing);
if (explicitEncoder != null || propertyWired) {
throw LogProperty.ValidationException
.of(LogAppender.class, new IllegalArgumentException("Appender '" + _name + "' output "
+ finalOutput.getClass().getName()
+ " provides a mandatory encoder and does not allow another encoder to be configured."));
}
encoder = pe.encoder(_name, config);
}
else {
encoder = explicitEncoder;
if (encoder == null) {
var propertyResult = encoderProperty(_name, config);
if (finalOutput instanceof LogOutput.ProvidesEncoder pe
&& propertyResult instanceof LogProperty.Result.Missing) {
encoder = pe.encoder(_name, config);
}
else {
encoder = DefaultAppenderRegistry.rawValue(propertyResult.or(() -> config.encoderRegistry()
.encoderForOutputType(finalOutput.type())
.provide(_name, config))).value();
}
}
}
Set<AppenderFlag> flags = b.flags != null ? b.flags : EnumSet.noneOf(AppenderFlag.class);
AppenderType appenderType = b.appenderType != null ? b.appenderType
: AbstractLogAppender.globalOptimizeEnabled ? AppenderType.AUTO_DETECT
: AppenderType.LOCK_THREAD_LOCAL_BUFFER;
return DirectLogAppender.of(_name, output, encoder, appenderType, flags, config.alerts(),
config.metrics());
};
}
/*
* Resolves APPENDER_OUTPUT_PROPERTY all the way to a concrete LogOutput (not just
* a LogProvider), via LogProperty.provideValue so a failure inside the provided
* LogOutput's own construction (its own separate property tree, e.g.
* logging.appender.file.output.file.bufferSize) is not relabeled
* "Error converting property" as if APPENDER_OUTPUT_PROPERTY's own value had
* failed to convert - see LogProperty#providingError. Moved here since output
* resolution is now entirely this Builder's concern.
*/
private static LogProperty.Result<LogOutput> outputProperty(String name, LogConfig config) {
return LogProperty.provideValue(
config.properties().forKey(APPENDER_OUTPUT_PROPERTY, name).ofProvider(LogOutput::of), name, config);
}
private static LogProperty.Result<LogEncoder> encoderProperty(String name, LogConfig config) {
return LogProperty.provideValue(
config.properties().forKey(APPENDER_ENCODER_PROPERTY, name).ofProvider(LogEncoder::of), name,
config);
}
}
/**
* Provides appenders safely to the publisher. The providing calls of
* <code>asXXX</code> can only be called once as they register the appenders.
*/
final class Appenders {
private final AtomicBoolean created = new AtomicBoolean();
private final String name;
private final LogConfig config;
private final List<LogProvider<LogAppender>> appenders;
/*
* Set once, inside asList()/asSingle(), so LogReporter can find out what a
* publisher was actually given without asking the publisher itself (which would
* mean every current and future publisher implementation cooperating): see
* resolvedOrNull(). Not re-derivable by calling asList()/asSingle() again: both
* are one-shot (see the created guard above).
*/
private @Nullable List<? extends LogAppender> resolved;
Appenders(String name, LogConfig config, List<LogProvider<LogAppender>> appenders) {
super();
this.name = name;
this.config = config;
this.appenders = appenders;
}
/**
* Return the appenders as a list.
* @return list of appenders.
* @throws IllegalStateException if appenders are already registered.
*/
public List<? extends LogAppender> asList() throws IllegalStateException {
if (created.compareAndSet(false, true)) {
var apps = appenders();
List<LogAppender> appenders = new ArrayList<>();
for (var a : apps) {
appenders.add(register(a));
}
resolved = appenders;
return appenders;
}
else {
throw new IllegalStateException("Appenders already provided.");
}
}
/**
* Consolidate the appenders as a single appender, appended synchronously. If more
* than one appender is combined, each keeps its own independent lock and is
* appended to directly - see {@link CompositeLogAppender}.
* @return single appender.
* @throws IllegalStateException if appenders are already registered.
*/
public LogAppender asSingle() throws IllegalStateException {
if (created.compareAndSet(false, true)) {
var apps = appenders();
var appender = composite(apps);
resolved = List.of(appender);
return register(appender);
}
else {
throw new IllegalStateException("Appenders already provided.");
}
}
/**
* The appenders actually resolved by whichever of {@link #asList()}/
* {@link #asSingle()} a publisher factory called, or <code>null</code> if neither
* has been called yet (a publisher factory that never calls either one has no
* real appenders to report on regardless).
* @return resolved appenders, or <code>null</code>.
*/
@Nullable List<? extends LogAppender> resolvedOrNull() {
return resolved;
}
private LogAppender register(LogAppender appender) {
return switch (appender) {
case DirectLogAppender ia -> {
config.serviceRegistry().put(LogAppender.class, name + "." + ia.name(), ia);
yield ia;
}
case CompositeLogAppender ca -> {
config.serviceRegistry().put(LogAppender.class, name, ca);
yield ca;
}
default -> {
throw new IllegalStateException();
}
};
}
private List<LogAppender> appenders() {
return LogProvider.flatten(appenders)
.describe(n -> "Appenders for route: '" + n + "'")
.provide(name, config);
}
/**
* Creates a composite log appender from many, each keeping its own independent
* lock.
* @param appenders appenders.
* @return appender.
*/
private static LogAppender composite(List<? extends LogAppender> appenders) {
if (appenders.isEmpty()) {
throw new IllegalArgumentException("A single appender is required");
}
if (appenders.size() == 1) {
return Objects.requireNonNull(appenders.get(0));
}
return CompositeLogAppender.of(appenders);
}
}
@Override
public void close();
}
interface AppenderVisitor {
boolean consume(DirectLogAppender appender);
}
/**
* This is a JAVADOC BUG
*/
sealed interface InternalLogAppender extends LogAppender, Actor {
static InternalLogAppender of(LogAppender appender) {
return Objects.requireNonNull((InternalLogAppender) appender); // TODO eclipse
// bug.
}
/**
* An appender can act on actions. One of the key actions is reopening files.
* @param action action to run.
* @return alert events for anything that failed, empty if the action fully succeeded.
*/
@Override
public List<LogEvent> act(LogAction action);
/**
* Removes the current thread's thread local state, if any. Unlike {@link #act} this
* takes no lock since only the calling thread's state is touched.
*/
default void clearThreadLocals() {
}
}
sealed interface DirectLogAppender extends InternalLogAppender {
String name();
LogOutput output();
LogEncoder encoder();
default List<LogEvent> _request(LogAction action) {
return switch (action) {
case LogAction.StandardAction a -> switch (a) {
case LogAction.StandardAction.REOPEN -> reopen();
case LogAction.StandardAction.FLUSH -> flush();
};
};
}
/**
* Reopens {@link #output()}. Any failure is reported to the alert system rather than
* thrown, at {@link Level#ERROR}.
* @return the alert event if reopening failed, empty if it succeeded.
*/
List<LogEvent> reopen();
/**
* Flushes {@link #output()}. Any failure is reported to the alert system rather than
* thrown, at {@link Level#ERROR}.
* @return the alert event if flushing failed, empty if it succeeded.
*/
List<LogEvent> flush();
static DirectLogAppender of(String name, LogOutput output, LogEncoder encoder, AppenderType type,
Set<LogAppender.AppenderFlag> flags, LogAlerts alerts, LogMetrics metrics) {
type = AbstractLogAppender.resolveAutoDetectAppenderType(type);
type = AbstractLogAppender.guardSynchronizedAppenderType(type);
type = AbstractLogAppender.guardThreadLocalAppenderType(type);
return switch (type) {
case REUSE_BUFFER ->
new ReuseBufferLogAppender(name, output, encoder, flags, new ReentrantLock(), alerts, metrics);
case SYNCHRONIZED_THREAD_LOCAL_BUFFER ->
new SynchronizedThreadLocalBufferLogAppender(name, output, encoder, flags, alerts, metrics);
case LOCK_THREAD_LOCAL_BUFFER -> new LockThreadLocalBufferLogAppender(name, output, encoder, flags,
new ReentrantLock(), alerts, metrics);
case LOCK_NEW_BUFFER ->
new LockNewBufferLogAppender(name, output, encoder, flags, new ReentrantLock(), alerts, metrics);
case AUTO_DETECT ->
throw new IllegalStateException("AUTO_DETECT should have already been resolved to a concrete type");
};
}
}
/**
* An abstract appender to help create custom appenders.
*/
sealed abstract class AbstractLogAppender implements DirectLogAppender {
/*
* Set once from LogProperties#GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY during
* LogConfig construction (see DefaultLogConfig) - a global, process-wide guarantee
* that no appender will ever use `synchronized`, for deployments that want that
* guaranteed even when something explicitly requests
* SYNCHRONIZED_THREAD_LOCAL_BUFFER. Global (not per-route/per-appender) by design,
* matching the property's own scope.
*/
static volatile boolean forceReentrantLockAppenders = false;
/**
* Downgrades an explicit
* {@link LogAppender.AppenderType#SYNCHRONIZED_THREAD_LOCAL_BUFFER} to
* {@link LogAppender.AppenderType#LOCK_THREAD_LOCAL_BUFFER} if
* {@link #forceReentrantLockAppenders} is active - the enforcement point that makes
* the global no-synchronized guarantee a real guarantee rather than just a changed
* default.
* @param type type as given to an appender factory method.
* @return {@code type} unchanged, unless the guarantee is active and
* {@code SYNCHRONIZED_THREAD_LOCAL_BUFFER} was requested, in which case
* {@code LOCK_THREAD_LOCAL_BUFFER} instead.
*/
static LogAppender.AppenderType guardSynchronizedAppenderType(LogAppender.AppenderType type) {
if (!forceReentrantLockAppenders || type != LogAppender.AppenderType.SYNCHRONIZED_THREAD_LOCAL_BUFFER) {
return type;
}
return LogAppender.AppenderType.LOCK_THREAD_LOCAL_BUFFER;
}
/*
* Set once from LogProperties#GLOBAL_THREADLOCAL_DISABLED_PROPERTY during LogConfig
* construction (see DefaultLogConfig) - a global, process-wide guarantee that no
* appender will ever use ThreadLocal, for deployments that want that guaranteed even
* when something explicitly requests LOCK_THREAD_LOCAL_BUFFER (the default) or
* SYNCHRONIZED_THREAD_LOCAL_BUFFER. Global (not per-route/per-appender) by design,
* matching the property's own scope - rainbowgum-slf4j's MDC support independently
* reads the same property key to decide whether to disable itself too, see
* LogProperties#GLOBAL_THREADLOCAL_DISABLED_PROPERTY's javadoc.
*/
static volatile boolean forceNoThreadLocalAppenders = false;
/*
* Set once from LogProperties#GLOBAL_OPTIMIZE_PROPERTY during LogConfig construction
* (see DefaultLogConfig) - unlike
* forceReentrantLockAppenders/forceNoThreadLocalAppenders above, this is not a
* downgrade applied to an already-resolved type; it only changes what
* LogAppender.Builder#build() picks when appenderType was never set at all
* (explicitly, either way, always wins). See LogProperties#GLOBAL_OPTIMIZE_PROPERTY's
* javadoc for what it currently resolves to and why that is not a contract.
*/
static volatile boolean globalOptimizeEnabled = false;
/**
* Downgrades either {@link ThreadLocal}-backed type (
* {@link LogAppender.AppenderType#LOCK_THREAD_LOCAL_BUFFER} or
* {@link LogAppender.AppenderType#SYNCHRONIZED_THREAD_LOCAL_BUFFER}) to
* {@link LogAppender.AppenderType#LOCK_NEW_BUFFER} if
* {@link #forceNoThreadLocalAppenders} is active - the enforcement point that makes
* the global no-{@link ThreadLocal} guarantee a real guarantee rather than just a
* changed default. An explicit {@link LogAppender.AppenderType#REUSE_BUFFER} request
* is left as-is - it is already {@link ThreadLocal}-free, so there is nothing to
* downgrade.
* @param type type as given to an appender factory method.
* @return {@code type} unchanged, unless the guarantee is active and a
* {@link ThreadLocal}-backed type was requested, in which case
* {@code LOCK_NEW_BUFFER} instead.
*/
static LogAppender.AppenderType guardThreadLocalAppenderType(LogAppender.AppenderType type) {
if (!forceNoThreadLocalAppenders) {
return type;
}
return switch (type) {
case LOCK_THREAD_LOCAL_BUFFER, SYNCHRONIZED_THREAD_LOCAL_BUFFER -> LogAppender.AppenderType.LOCK_NEW_BUFFER;
case REUSE_BUFFER, LOCK_NEW_BUFFER -> type;
case AUTO_DETECT ->
throw new IllegalStateException("AUTO_DETECT should have already been resolved to a concrete type");
};
}
/**
* Resolves {@link LogAppender.AppenderType#AUTO_DETECT} to
* {@link LogAppender.AppenderType#SYNCHRONIZED_THREAD_LOCAL_BUFFER} or
* {@link LogAppender.AppenderType#LOCK_THREAD_LOCAL_BUFFER} depending on
* {@link #isNativeImageRuntime()}; any other type is returned unchanged. Called
* before {@link #guardSynchronizedAppenderType(LogAppender.AppenderType)} and
* {@link #guardThreadLocalAppenderType(LogAppender.AppenderType)} so a resolved
* {@code SYNCHRONIZED_THREAD_LOCAL_BUFFER} is still subject to both of those global
* guarantees the same as if it had been requested directly.
* @param type type as given to an appender factory method.
* @return {@code type} unchanged unless it was {@code AUTO_DETECT}.
*/
static LogAppender.AppenderType resolveAutoDetectAppenderType(LogAppender.AppenderType type) {
if (type != LogAppender.AppenderType.AUTO_DETECT) {
return type;
}
return isNativeImageRuntime() ? LogAppender.AppenderType.SYNCHRONIZED_THREAD_LOCAL_BUFFER
: LogAppender.AppenderType.LOCK_THREAD_LOCAL_BUFFER;
}
/**
* The system property GraalVM native-image itself sets to {@code buildtime} during
* the image build and {@code runtime} when the built image actually executes - absent
* entirely on a plain JVM. The standard, dependency-free way every native-image-aware
* framework checks "am I native" without a hard compile-time dependency on GraalVM's
* own SDK just to ask this one question.
*/
static final String NATIVE_IMAGE_CODE_PROPERTY = "org.graalvm.nativeimage.imagecode";
/**
* Whether this process is currently running as an executing (not building) GraalVM
* native image - see {@link #NATIVE_IMAGE_CODE_PROPERTY}.
* @return true if running as a native image at runtime.
*/
static boolean isNativeImageRuntime() {
return "runtime".equals(System.getProperty(NATIVE_IMAGE_CODE_PROPERTY));
}
/**
* Whether an appender should drop (or drop-and-log) an append call because it is
* reentrant - i.e. the current thread is already inside a previous call to the same
* appender's write path, which happens if an output does logging itself during its
* own write. Shared by every appender that can detect reentrancy, regardless of
* whether it does so via a {@link ReentrantLock} (
* {@code lock.isHeldByCurrentThread()}) or a {@code synchronized} block (
* {@link Thread#holdsLock(Object)}) - callers pass in whichever check applies to
* them. Whenever this returns {@code true}, {@link LogMetrics#EVENTS_DROPPED_METRIC}
* is incremented by {@code count} regardless of whether
* {@link AppenderFlag#REENTRY_LOG} is set - counting and logging/alerting are
* separate concerns, so the count still happens even when the drop itself is silent.
* @param reentrant whether the current thread already holds this appender's
* lock/monitor.
* @param flags the appender's flags.
* @param metrics where to record {@link LogMetrics#EVENTS_DROPPED_METRIC} if events
* are dropped.
* @param count number of events that would be dropped - {@code 1} for a single event
* append, or the batch size for a batch append.
* @return {@code true} if the caller should drop the event(s) without appending.
*/
static boolean shouldDropForReentry(boolean reentrant, Set<LogAppender.AppenderFlag> flags, LogMetrics metrics,
int count) {
if (!reentrant) {
return false;
}
if (flags.contains(LogAppender.AppenderFlag.REENTRY_LOG)) {
Exception exception = new Exception("reentrant appender");
MetaLog.error(LogAppender.class, exception);
metrics.errorCounter(LogMetrics.EVENTS_DROPPED_METRIC, count);
return true;
}
if (flags.contains(LogAppender.AppenderFlag.REENTRY_DROP)) {
metrics.errorCounter(LogMetrics.EVENTS_DROPPED_METRIC, count);
return true;
}
return false;
}
/**
* name.
*/
protected final String name;
/**
* output
*/
protected final LogOutput output;
/**
* encoder
*/
protected final LogEncoder encoder;
protected final Set<LogAppender.AppenderFlag> flags;
protected final boolean immediateFlush;
/**
* alerts for reporting encode/write failures that this appender catches so they never
* propagate back to whatever application thread called logger.info(...).
*/
protected final LogAlerts alerts;
/**
* metrics for recording counters like {@link LogMetrics#EVENTS_DROPPED_METRIC}.
*/
protected final LogMetrics metrics;
/**
* Creates an appender from an output and encoder.
* @param output set the output field and will be started and closed with the
* appender.
* @param encoder set the encoder field.
* @param alerts alerts for reporting encode/write failures.
* @param metrics metrics for recording counters.
*/
protected AbstractLogAppender(String name, LogOutput output, LogEncoder encoder,
Set<LogAppender.AppenderFlag> flags, LogAlerts alerts, LogMetrics metrics) {
super();
this.name = name;
this.output = output;
this.encoder = encoder;
this.flags = flags;
this.immediateFlush = !flags.contains(LogAppender.AppenderFlag.DISABLE_IMMEDIATE_FLUSH);
this.alerts = alerts;
this.metrics = metrics;
}
@Override
public void start(LogConfig config) {
output.start(config);
}
@Override
public void close() {
output.close();
}
@Override
public String toString() {
return getClass().getName() + "[name=" + name + " encoder=" + encoder + ", " + "output=" + output + ", flags="
+ flags + "]";
}
@Override
public String name() {
return this.name;
}
@Override
public LogOutput output() {
return this.output;
}
@Override
public LogEncoder encoder() {
return this.encoder;
}
@Override
public List<LogEvent> reopen() {
metrics.infoCounter(LogMetrics.REOPEN_METRIC, 1);
try {
output.reopen();
return List.of();
}
catch (Exception e) {
metrics.errorCounter(LogMetrics.REOPEN_FAIL_METRIC, 1);
var event = errorEvent(getClass(), "appender '" + name + "' failed to reopen output", e);
alerts.alert(event);
return List.of(event);
}
}
@Override
public List<LogEvent> flush() {
try {
output.flush();
return List.of();
}
catch (Exception e) {
var event = errorEvent(getClass(), "appender '" + name + "' failed to flush output", e);
alerts.alert(event);
return List.of(event);
}
}
private static LogEvent errorEvent(Class<?> loggerName, String message, Throwable throwable) {
var currentThread = Thread.currentThread();
return LogEvent.of(Instant.now(), currentThread.getName(), currentThread.threadId(), Level.ERROR,
loggerName.getName(), message, KeyValues.of(), throwable);
}
}
/**
* Combines more than one appender on a route into one {@link LogAppender}. Each appender
* keeps the independent lock it was already constructed with, and
* {@link #append(LogEvent)}/{@link #append(LogEvent[], int)} skip locking at the
* composite level entirely and append to every component directly - so e.g. a console
* appender and a file appender under the same route never contend on the same lock for
* unrelated I/O. {@link #start(LogConfig)}/{@link #close()}/{@link #act(LogAction)} do
* the same - there is no composite-owned mutable state to protect, only a loop over
* components that already handle their own synchronization where it matters.
*/
@SuppressWarnings("ArrayRecordComponent")
record CompositeLogAppender(DirectLogAppender[] appenders) implements InternalLogAppender {
public static CompositeLogAppender of(List<? extends LogAppender> appenders) {
@SuppressWarnings("null") // TODO Eclipse issue here
DirectLogAppender @NonNull [] array = appenders.stream()
.map(CompositeLogAppender::cast)
.toArray(i -> new DirectLogAppender[i]);
return new CompositeLogAppender(array);
}
private static DirectLogAppender cast(LogAppender appender) {
return (DirectLogAppender) appender;
}
@Override
public void append(LogEvent event) {
for (var appender : appenders) {
appender.append(event);
}
}
@Override
public void append(LogEvent[] event, int count) {
for (var appender : appenders) {
appender.append(event, count);
}
}
@Override
public void close() {
for (var appender : appenders) {
appender.close();
}
}
@Override
public void start(LogConfig config) {
for (var appender : appenders) {
appender.start(config);
}
}
@Override
public List<LogEvent> act(LogAction action) {
return Actor.act(appenders, action);
}
@Override
public void clearThreadLocals() {
for (var appender : appenders) {
appender.clearThreadLocals();
}
}
@Override
public String toString() {
return getClass().getName() + "[appenders=" + Arrays.toString(appenders) + "]";
}
}
sealed abstract class LockLogAppender extends AbstractLogAppender implements InternalLogAppender {
protected final ReentrantLock lock;
public LockLogAppender(String name, LogOutput output, LogEncoder encoder, Set<LogAppender.AppenderFlag> flags,
ReentrantLock lock, LogAlerts alerts, LogMetrics metrics) {
super(name, output, encoder, flags, alerts, metrics);
this.lock = lock;
}
@Override
public List<LogEvent> act(LogAction action) {
lock.lock();
try {
return _request(action);
}
finally {
lock.unlock();
}
}
@Override
public void close() {
lock.lock();
try {
super.close();
}
finally {
lock.unlock();
}
}
}
/*
* The idea here is to reuse the buffer trading lock contention for less GC.
*/
final class ReuseBufferLogAppender extends LockLogAppender implements InternalLogAppender {
private final LogEncoder.Buffer buffer;
ReuseBufferLogAppender(String name, LogOutput output, LogEncoder encoder, Set<LogAppender.AppenderFlag> flags,
ReentrantLock lock, LogAlerts alerts, LogMetrics metrics) {
super(name, output, encoder, flags, lock, alerts, metrics);
this.buffer = encoder.buffer(output.bufferHints());
}
@Override
public final void append(LogEvent event) {
if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, 1)) {
return;
}
try {
lock.lock();
try {
buffer.clear();
encoder.encode(event, buffer);
output.write(event, buffer);
if (immediateFlush) {
output.flush();
}
}
finally {
lock.unlock();
}
}
catch (Exception e) {
alerts.error(getClass(), "appender '" + name + "' failed to append event", e);
metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, 1);
}
}
@Override
public void append(LogEvent[] events, int count) {
if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, count)) {
return;
}
try {
lock.lock();
try {
output.write(events, count, encoder, buffer);
if (immediateFlush) {
output.flush();
}
}
finally {
lock.unlock();
}
}
catch (Exception e) {
alerts.error(getClass(), "appender '" + name + "' failed to append batch of " + count + " event(s)", e);
metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, count);
}
}
@Override
public void close() {
lock.lock();
try {
super.close();
buffer.close();
}
finally {
lock.unlock();
}
}
}
/*
* The idea here is to encode outside the lock. Instead of allocating a fresh buffer per
* event or sharing (and thus serializing access to) a single buffer
* (ReuseBufferLogAppender), each thread gets its own buffer that only it will ever touch,
* so encoding never needs to be guarded by the lock at all - only the final write to the
* output does.
*/
final class LockThreadLocalBufferLogAppender extends LockLogAppender implements InternalLogAppender {
/*
* There is no way to enumerate every thread's buffer to close it on appender close so
* we rely on Buffer implementations not holding onto real resources (today they are
* all just wrapped in-memory builders) and let the ThreadLocal itself (and
* consequently the per-thread entries) become collectible once this appender is
* discarded.
*/
// CheckerFramework's ThreadLocal stub declares T as inherently @Nullable since get()
// can return null before initialValue() runs, but withInitial(...) below guarantees
// it never does here.
@SuppressWarnings("nullness:type.argument")
private final ThreadLocal<LogEncoder.Buffer> bufferThreadLocal;
LockThreadLocalBufferLogAppender(String name, LogOutput output, LogEncoder encoder,
Set<LogAppender.AppenderFlag> flags, ReentrantLock lock, LogAlerts alerts, LogMetrics metrics) {
super(name, output, encoder, flags, lock, alerts, metrics);
this.bufferThreadLocal = ThreadLocal.withInitial(() -> encoder.buffer(output.bufferHints()));
}
@Override
public void clearThreadLocals() {
bufferThreadLocal.remove();
}
@Override
public final void append(LogEvent event) {
try {
var buffer = bufferThreadLocal.get();
buffer.clear();
encoder.encode(event, buffer);
writeLocked(event, buffer);
}
catch (Exception e) {
alerts.error(getClass(), "appender '" + name + "' failed to append event", e);
metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, 1);
}
}
private void writeLocked(LogEvent event, LogEncoder.Buffer buffer) {
if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, 1)) {
return;
}
lock.lock();
try {
output.write(event, buffer);
if (immediateFlush) {
output.flush();
}
}
finally {
lock.unlock();
}
}
@Override
public void append(LogEvent[] events, int count) {
if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, count)) {
return;
}
try {
lock.lock();
try {
output.write(events, count, encoder, bufferThreadLocal.get());
if (immediateFlush) {
output.flush();
}
}
finally {
lock.unlock();
}
}
catch (Exception e) {
alerts.error(getClass(), "appender '" + name + "' failed to append batch of " + count + " event(s)", e);
metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, count);
}
}
}
/*
* Like LockThreadLocalBufferLogAppender (encode outside the lock, lock only the final
* write) except the buffer is a fresh allocation per event instead of a ThreadLocal - for
* deployments that want a hard guarantee of no ThreadLocal anywhere in the logging path
* without paying ReuseBufferLogAppender's lock-across-the-whole-encode cost.
*/
final class LockNewBufferLogAppender extends LockLogAppender implements InternalLogAppender {
LockNewBufferLogAppender(String name, LogOutput output, LogEncoder encoder, Set<LogAppender.AppenderFlag> flags,
ReentrantLock lock, LogAlerts alerts, LogMetrics metrics) {
super(name, output, encoder, flags, lock, alerts, metrics);
}
@Override
public final void append(LogEvent event) {
try {
var buffer = encoder.buffer(output.bufferHints());
encoder.encode(event, buffer);
writeLocked(event, buffer);
}
catch (Exception e) {
alerts.error(getClass(), "appender '" + name + "' failed to append event", e);
metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, 1);
}
}
private void writeLocked(LogEvent event, LogEncoder.Buffer buffer) {
if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, 1)) {
return;
}
lock.lock();
try {
output.write(event, buffer);
if (immediateFlush) {
output.flush();
}
}
finally {
lock.unlock();
}
}
@Override
public void append(LogEvent[] events, int count) {
if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, count)) {
return;
}
try {
var buffer = encoder.buffer(output.bufferHints());
lock.lock();
try {
output.write(events, count, encoder, buffer);
if (immediateFlush) {
output.flush();
}
}
finally {
lock.unlock();
}
}
catch (Exception e) {
alerts.error(getClass(), "appender '" + name + "' failed to append batch of " + count + " event(s)", e);
metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, count);
}
}
}
/*
* Like LockThreadLocalBufferLogAppender (per-thread reused buffer, encode outside any
* lock) but the final write is protected by a plain `synchronized` block on this
* appender's own monitor instead of a ReentrantLock. Does not extend LockLogAppender -
* there is no way to acquire a monitor in one method call and release it in another, so
* this appender's critical sections are written as literal synchronized blocks instead.
*/
final class SynchronizedThreadLocalBufferLogAppender extends AbstractLogAppender implements InternalLogAppender {
private final Object monitor = new Object();
// See LockThreadLocalBufferLogAppender's identical field for why this suppression is
// needed.
@SuppressWarnings("nullness:type.argument")
private final ThreadLocal<LogEncoder.Buffer> bufferThreadLocal;
SynchronizedThreadLocalBufferLogAppender(String name, LogOutput output, LogEncoder encoder,
Set<LogAppender.AppenderFlag> flags, LogAlerts alerts, LogMetrics metrics) {
super(name, output, encoder, flags, alerts, metrics);
this.bufferThreadLocal = ThreadLocal.withInitial(() -> encoder.buffer(output.bufferHints()));
}
@Override
public void clearThreadLocals() {
bufferThreadLocal.remove();
}
@Override
public void append(LogEvent event) {
try {
var buffer = bufferThreadLocal.get();
buffer.clear();
encoder.encode(event, buffer);
writeSynchronized(event, buffer);
}
catch (Exception e) {
alerts.error(getClass(), "appender '" + name + "' failed to append event", e);
metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, 1);
}
}
private void writeSynchronized(LogEvent event, LogEncoder.Buffer buffer) {
if (shouldDropForReentry(Thread.holdsLock(monitor), flags, metrics, 1)) {
return;
}
synchronized (monitor) {
output.write(event, buffer);
if (immediateFlush) {
output.flush();
}
}
}
@Override
public void append(LogEvent[] events, int count) {
if (shouldDropForReentry(Thread.holdsLock(monitor), flags, metrics, count)) {
return;
}
try {
synchronized (monitor) {
output.write(events, count, encoder, bufferThreadLocal.get());
if (immediateFlush) {
output.flush();
}
}
}
catch (Exception e) {
alerts.error(getClass(), "appender '" + name + "' failed to append batch of " + count + " event(s)", e);
metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, count);
}
}
@Override
public void close() {
synchronized (monitor) {
super.close();
}
}
@Override
public List<LogEvent> act(LogAction action) {
synchronized (monitor) {
return _request(action);
}
}
}