001package io.jstach.rainbowgum;
002
003import java.lang.System.Logger.Level;
004import java.time.Instant;
005import java.util.ArrayList;
006import java.util.Arrays;
007import java.util.Collection;
008import java.util.EnumSet;
009import java.util.List;
010import java.util.Locale;
011import java.util.Objects;
012import java.util.Set;
013import java.util.concurrent.atomic.AtomicBoolean;
014import java.util.concurrent.locks.ReentrantLock;
015
016import org.jspecify.annotations.NonNull;
017import org.jspecify.annotations.Nullable;
018
019import io.jstach.rainbowgum.annotation.CaseChanging;
020
021/**
022 * Appenders are guaranteed to be written synchronously much like an actor in actor
023 * concurrency. They safely hold onto and communicate with the encoder and output.
024 * Appenders largely deal with correct locking, buffer reuse and flushing.
025 * {@linkplain LogAppender.AppenderFlag Flags } can be set to control the behavior the
026 * appenders and publishers can request different appender behavior through the flags.
027 *
028 * @see LogAppender.AppenderFlag
029 * @apiNote because appenders require complicated implementation and to guarantee
030 * integrity the implementations are encapsulated (sealed).
031 */
032public sealed interface LogAppender extends LogLifecycle {
033
034        /**
035         * Default Console appender name.
036         */
037        static final String CONSOLE_APPENDER_NAME = "console";
038
039        /**
040         * Default output file appender name.
041         */
042        static final String FILE_APPENDER_NAME = "file";
043
044        /**
045         * Output appender property.
046         */
047        static final String APPENDER_OUTPUT_PROPERTY = LogProperties.APPENDER_OUTPUT_PROPERTY;
048
049        /**
050         * Encoder appender property.
051         */
052        static final String APPENDER_ENCODER_PROPERTY = LogProperties.APPENDER_ENCODER_PROPERTY;
053
054        /**
055         * Appender flags. A list of flags (usually comma separated).
056         * @see AppenderFlag
057         */
058        static final String APPENDER_FLAGS_PROPERTY = LogProperties.APPENDER_FLAGS_PROPERTY;
059
060        /**
061         * Appender type.
062         * @see AppenderType
063         */
064        static final String APPENDER_TYPE_PROPERTY = LogProperties.APPENDER_TYPE_PROPERTY;
065
066        /**
067         * Batch of events. <strong>DO NOT MODIFY THE ARRAY</strong>. Do not use the
068         * <code>length</code> of the passed in array but instead use <code>count</code>
069         * parameter.
070         * @param events an array guaranteed to be smaller than count.
071         * @param count the number of items.
072         */
073        public void append(LogEvent[] events, int count);
074
075        /**
076         * Appends a single event.
077         * @param event event.
078         */
079        public void append(LogEvent event);
080
081        /**
082         * Boolean like flags for appender that can be set with
083         * {@link LogAppender#APPENDER_FLAGS_PROPERTY}. Unlike {@link AppenderType} more than
084         * one flag can be set at once.
085         */
086        @CaseChanging
087        public enum AppenderFlag {
088
089                /**
090                 * By default the appender will call flush on each item appended or if in async
091                 * batch mode for each batch. This flag disables that behavior so that flushing is
092                 * left up to the output (or an external mechanism) instead.
093                 */
094                DISABLE_IMMEDIATE_FLUSH,
095                /**
096                 * The appender will drop events on reentry which happens if an appender during
097                 * its append causes recursive appending in the same thread. This is an analog to
098                 * what
099                 * <a href="https://logback.qos.ch/manual/appenders.html#AppenderBase">Logback
100                 * does by default</a>. Note that this is done using {@link ReentrantLock} and not
101                 * ThreadLocal like logback <strong>and is not done by default hence the
102                 * flag!</strong>
103                 * <p>
104                 * This flag is to allow outputs that do logging themselves. For performance
105                 * reasons and to allow async publishers it is recommended that you fix the output
106                 * code such that it does not do logging. This flag is ignored if
107                 * {@link #REENTRY_LOG} is set.
108                 * <p>
109                 * <strong>This flag will not fix outputs causing lool like logging if an async
110                 * publisher is used!</strong> That is why it is recommended you fix the output by
111                 * dropping events that would cause infinite loop like logging.
112                 * @see #REENTRY_LOG
113                 */
114                REENTRY_DROP,
115                /**
116                 * The appender will log events as errors to std error on reentry which happens if
117                 * an appender during its append causes recursive appending in the same thread.
118                 * This is an analog to what
119                 * <a href="https://logback.qos.ch/manual/appenders.html#AppenderBase">Logback
120                 * does by default</a>. Note that this is done using {@link ReentrantLock} and not
121                 * ThreadLocal like logback <strong>and is not done by default hence the
122                 * flag!</strong> This flag is to resolve failures of outputs that then do
123                 * logging.
124                 * <p>
125                 * This flag takes precedence over {@link #REENTRY_DROP}.
126                 */
127                REENTRY_LOG;
128
129                static Set<AppenderFlag> parse(Collection<String> value) {
130                        if (value.isEmpty()) {
131                                return EnumSet.noneOf(AppenderFlag.class);
132                        }
133                        var s = EnumSet.noneOf(AppenderFlag.class);
134                        for (var v : value) {
135                                s.add(parse(v));
136                        }
137                        return s;
138                }
139
140                static AppenderFlag parse(String value) {
141                        String v = value.toUpperCase(Locale.ROOT);
142                        return AppenderFlag.valueOf(v);
143                }
144
145        }
146
147        /**
148         * The buffer/locking strategy an appender uses. Unlike {@link AppenderFlag} exactly
149         * one is in effect for a given appender - set on the {@link LogAppender.Builder} (or
150         * via {@link LogAppender#APPENDER_TYPE_PROPERTY}) at construction time and fixed for
151         * the appender's lifetime; a publisher cannot change it afterward.
152         */
153        @CaseChanging
154        public enum AppenderType {
155
156                /**
157                 * The appender will create a single buffer that will be reused and will be
158                 * protected by the appenders locking.
159                 */
160                REUSE_BUFFER,
161                /**
162                 * The appender will give each thread its own reusable buffer (a
163                 * {@link ThreadLocal}) instead of allocating a new buffer per event. Encoding is
164                 * done <strong>outside</strong> the appender's lock (the thread's buffer is only
165                 * visited by that thread so no protection is needed while encoding) with the lock
166                 * only held for the final write to the output.
167                 * <p>
168                 * The same {@link ThreadLocal} buffer is used regardless of whether the calling
169                 * thread is a platform or virtual thread. A virtual thread's entry becomes
170                 * collectible once the thread itself terminates, and a typical unit of work (e.g.
171                 * one HTTP request) logs several times on the same thread, so reusing the buffer
172                 * across those calls still pays off even for short-lived virtual threads.
173                 * <p>
174                 * This is the default type - see {@link #SYNCHRONIZED_THREAD_LOCAL_BUFFER} for
175                 * the alternative lock-kind opt-in.
176                 */
177                LOCK_THREAD_LOCAL_BUFFER,
178                /**
179                 * Like {@link #LOCK_THREAD_LOCAL_BUFFER} (a reused per-thread buffer, encoding
180                 * done outside any lock) except the final write to the output is protected by a
181                 * plain {@code synchronized} block (the JVM's intrinsic monitor) instead of a
182                 * {@link ReentrantLock}.
183                 * <p>
184                 * The Java language has no way to acquire a monitor in one method call and
185                 * release it in another, so this appender's critical sections are written as
186                 * literal {@code synchronized} blocks rather than going through a shared lock
187                 * abstraction the way {@link #LOCK_THREAD_LOCAL_BUFFER} does.
188                 * {@link AppenderFlag#REENTRY_DROP} and {@link AppenderFlag#REENTRY_LOG} are
189                 * still honored though - {@link Thread#holdsLock(Object)} is the
190                 * {@code synchronized} equivalent of {@code ReentrantLock}'s
191                 * {@code isHeldByCurrentThread()}, so reentrancy is detected the same way.
192                 * <p>
193                 * Motivated by Log4j2's own garbage-free appenders using {@code synchronized}
194                 * rather than a {@code java.util.concurrent} lock around their buffer-transfer
195                 * step, and confirmed by real-workload benchmarking to outperform
196                 * {@link ReentrantLock} under platform-thread contention - this was the default
197                 * for a time. Real-workload benchmarking under virtual threads found the
198                 * opposite, a large and reproducible loss versus {@link ReentrantLock} for
199                 * reasons not fully understood (classic JEP 491 pinning was checked and ruled
200                 * out), so {@link #LOCK_THREAD_LOCAL_BUFFER} is the default now and this type is
201                 * an opt-in for platform-thread-heavy deployments that want the edge.
202                 * <p>
203                 * <strong>Explicitly setting this type honors it</strong> even on a JDK where
204                 * {@code synchronized} still pins the carrier platform thread when called from a
205                 * virtual thread (before <a href="https://openjdk.org/jeps/491">JEP 491</a>,
206                 * finalized in JDK 24) - the one exception is
207                 * {@code LogProperties#GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY}: when that global
208                 * property is active it downgrades even an explicit request for this type to
209                 * {@link #LOCK_THREAD_LOCAL_BUFFER}, since its whole point is a hard guarantee
210                 * independent of anything else in the configuration.
211                 */
212                SYNCHRONIZED_THREAD_LOCAL_BUFFER,
213                /**
214                 * Like {@link #LOCK_THREAD_LOCAL_BUFFER} (encoding done outside the lock, the
215                 * lock only guards the final write to the output) except the buffer is allocated
216                 * fresh for every single event instead of being cached in a {@link ThreadLocal}.
217                 * <p>
218                 * For deployments that want a hard guarantee of never using {@link ThreadLocal}
219                 * anywhere in the logging path - {@link #REUSE_BUFFER} is the existing
220                 * no-{@code ThreadLocal} option, but it holds its lock across the entire
221                 * encode-then-write critical section (the single shared buffer must stay
222                 * protected for as long as anything is writing into it), so it pays for that
223                 * guarantee with lock contention proportional to encoding cost, not just I/O
224                 * cost. This type keeps {@link #LOCK_THREAD_LOCAL_BUFFER}'s low-contention shape
225                 * (encode into a buffer nothing else can see, lock only for the write) while
226                 * dropping the {@link ThreadLocal} - the buffer is simply a local variable, not
227                 * cached anywhere, so nothing needs to be evicted or leaked-if-forgotten either.
228                 * The tradeoff is a fresh buffer allocation (and whatever it grows to internally)
229                 * on every single event instead of amortizing that allocation across a thread's
230                 * whole lifetime.
231                 * @apiNote not the default - {@link #LOCK_THREAD_LOCAL_BUFFER}'s reused buffer is
232                 * the better choice unless avoiding {@link ThreadLocal} entirely is a hard
233                 * requirement, not just a preference.
234                 */
235                LOCK_NEW_BUFFER,
236                /*
237                 * Real-workload benchmarking under virtual threads found
238                 * SYNCHRONIZED_THREAD_LOCAL_BUFFER and LOCK_THREAD_LOCAL_BUFFER each winning by a
239                 * real, repeatable, double-digit-percentage margin on their own platform (native
240                 * image, HotSpot respectively) and losing by a comparable margin on the other - a
241                 * genuine cross-over, not just a shrinking gap, so no single fixed choice serves
242                 * both well. Matters most for something distributed as more than one build of the
243                 * same artifact for the same deployment - a native executable and a plain jar,
244                 * say - where picking correctly per build would otherwise mean either shipping
245                 * different configuration for each or sniffing the platform in application code.
246                 * Detection is the same org.graalvm.nativeimage.imagecode system property check
247                 * every native-image-aware framework uses to avoid a hard dependency on GraalVM's
248                 * own SDK.
249                 */
250                /**
251                 * Picks whichever concrete type actually performs best for the platform this
252                 * process is currently running on, decided once, at appender construction time,
253                 * not re-checked afterward. <strong>Currently</strong> that means
254                 * {@link #SYNCHRONIZED_THREAD_LOCAL_BUFFER} when running as a GraalVM native
255                 * image, or {@link #LOCK_THREAD_LOCAL_BUFFER} otherwise - but that specific
256                 * mapping is an implementation detail of this heuristic, not a contract. The
257                 * whole point of this type is to track whichever choice is actually best, so
258                 * which concrete type a given platform resolves to today may change in a future
259                 * release without notice, as benchmarking improves or platforms evolve.
260                 * <strong>If your deployment needs a specific type to always be used, pick that
261                 * type explicitly instead of relying on what {@code AUTO_DETECT} happens to
262                 * resolve to right now.</strong>
263                 * <p>
264                 * <strong>Not the default</strong> - requires explicitly requesting this type,
265                 * programmatically or via {@link LogAppender#APPENDER_TYPE_PROPERTY}. Downgraded
266                 * the same as an explicit request for whichever type it currently resolves to
267                 * would be: by {@code LogProperties#GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY} if
268                 * it resolves to {@link #SYNCHRONIZED_THREAD_LOCAL_BUFFER}, and by
269                 * {@code LogProperties#GLOBAL_THREADLOCAL_DISABLED_PROPERTY} either way.
270                 */
271                AUTO_DETECT;
272
273                static AppenderType parse(String value) {
274                        String v = value.toUpperCase(Locale.ROOT);
275                        return AppenderType.valueOf(v);
276                }
277
278        }
279
280        /**
281         * Creates a builder.
282         * @param name appender name.
283         * @return builder.
284         */
285        public static Builder builder(String name) {
286                /*
287                 * Eagerly triggers LogProperties' own key-parameter-value validation now,
288                 * matching every annotation-processor-generated Builder's own constructor, which
289                 * does the same via its own eager interpolateKey call: needed because this
290                 * hand-written Builder can otherwise skip every keyed property lookup entirely
291                 * when every field is set explicitly, never validating the name at all. Wrapped
292                 * into a ValidationException the same way a generated builder's constructor is,
293                 * so a bad name reports the same exception type regardless of whether it came
294                 * from logging.appenders (LogAppenderRegistry's own validateNames call) or a
295                 * direct programmatic builder(name) call like this one.
296                 */
297                try {
298                        LogProperties.interpolateNamedKey(LogProperties.APPENDER_PREFIX, name);
299                }
300                catch (IllegalArgumentException e) {
301                        throw LogProperty.ValidationException.of(LogAppender.class, e);
302                }
303                return new Builder(name);
304        }
305
306        /**
307         * Builder for creating standard appenders. Whatever is not set explicitly is resolved
308         * from properties keyed under {@link LogProperties#APPENDER_PREFIX} for this
309         * builder's name (see {@link #fromProperties(LogProperties)}), and failing that from
310         * a small set of defaults: no encoder resolves one from the output's own type, no
311         * flags means none are set, and no appender type means
312         * {@link AppenderType#LOCK_THREAD_LOCAL_BUFFER} - or {@link AppenderType#AUTO_DETECT}
313         * instead, if {@link LogProperties#GLOBAL_OPTIMIZE_PROPERTY} is enabled. There is
314         * deliberately no such default for output, since a required source is not something a
315         * generic named appender can safely guess, except for the well known
316         * {@code "console"}/ {@code "file"} names, which register their own fallback via
317         * {@link #outputDefault(LogProvider)}.
318         */
319        public static final class Builder implements LogBuilder<Builder, LogAppender> {
320
321                private @Nullable LogProvider<? extends LogOutput> output = null;
322
323                /*
324                 * A weaker fallback than an explicit output(...) call: consulted only if neither
325                 * that nor APPENDER_OUTPUT_PROPERTY resolves anything. Package-private, not part
326                 * of the public Builder API: DefaultAppenderRegistry is the only caller, for
327                 * "console"'s stdout default, where the precedence is deliberately the opposite
328                 * of an ordinary explicit value (the property is meant to override this default,
329                 * not the other way around).
330                 */
331                private @Nullable LogProvider<? extends LogOutput> outputDefault = null;
332
333                private @Nullable LogProvider<? extends LogEncoder> encoder = null;
334
335                private @Nullable EnumSet<AppenderFlag> flags = null;
336
337                private @Nullable AppenderType appenderType = null;
338
339                private final String name;
340
341                private Builder(String name) {
342                        this.name = name;
343                }
344
345                Builder outputDefault(LogProvider<? extends LogOutput> outputDefault) {
346                        this.outputDefault = outputDefault;
347                        return this;
348                }
349
350                /**
351                 * Name of the appender.
352                 * @return name.
353                 */
354                public String name() {
355                        return this.name;
356                }
357
358                /**
359                 * Sets output.
360                 * @param output output.
361                 * @return builder.
362                 */
363                public Builder output(LogProvider<? extends LogOutput> output) {
364                        this.output = output;
365                        return this;
366                }
367
368                /**
369                 * Sets output.
370                 * @param output output.
371                 * @return builder.
372                 */
373                public Builder output(LogOutput output) {
374                        this.output = LogProvider.of(output);
375                        return this;
376                }
377
378                /**
379                 * Sets formatter as encoder.
380                 * @param formatter formatter to be converted to encoder.
381                 * @return builder.
382                 * @see LogEncoder#of(LogFormatter)
383                 */
384                public Builder formatter(LogFormatter formatter) {
385                        this.encoder = LogEncoder.of(formatter);
386                        return this;
387                }
388
389                /**
390                 * Sets formatter as encoder.
391                 * @param formatter formatter to be converted to encoder.
392                 * @return builder.
393                 * @see LogEncoder#of(LogFormatter)
394                 */
395                public Builder formatter(LogFormatter.EventFormatter formatter) {
396                        this.encoder = LogEncoder.of(formatter);
397                        return this;
398                }
399
400                /**
401                 * Sets encoder.
402                 * @param encoder encoder not <code>null</code>.
403                 * @return builder.
404                 */
405                public Builder encoder(LogProvider<? extends LogEncoder> encoder) {
406                        this.encoder = encoder;
407                        return this;
408                }
409
410                /**
411                 * Sets encoder.
412                 * @param encoder encoder not <code>null</code>.
413                 * @return builder.
414                 */
415                public Builder encoder(LogEncoder encoder) {
416                        this.encoder = LogProvider.of(encoder);
417                        return this;
418                }
419
420                /**
421                 * Sets appender flags.
422                 * @param flags flags will replace all flags currently set.
423                 * @return this.
424                 */
425                public Builder flags(Collection<AppenderFlag> flags) {
426                        _flags().addAll(flags);
427                        return this;
428                }
429
430                private EnumSet<AppenderFlag> _flags() {
431                        EnumSet<AppenderFlag> flags = this.flags;
432                        if (flags == null) {
433                                this.flags = flags = EnumSet.noneOf(AppenderFlag.class);
434                        }
435                        return flags;
436                }
437
438                /**
439                 * Adds a flag.
440                 * @param flag flag.
441                 * @return this.
442                 */
443                public Builder flag(AppenderFlag flag) {
444                        _flags().add(flag);
445                        return this;
446                }
447
448                /**
449                 * Sets the appender type (buffer/locking strategy). If not set it is resolved
450                 * from {@link LogAppender#APPENDER_TYPE_PROPERTY} or otherwise defaults to
451                 * {@link AppenderType#LOCK_THREAD_LOCAL_BUFFER} ({@link AppenderType#AUTO_DETECT}
452                 * instead if {@link LogProperties#GLOBAL_OPTIMIZE_PROPERTY} is enabled). Calling
453                 * this method always wins over both.
454                 * @param appenderType appender type.
455                 * @return this.
456                 */
457                public Builder appenderType(AppenderType appenderType) {
458                        this.appenderType = appenderType;
459                        return this;
460                }
461
462                @Override
463                public String propertyPrefix() {
464                        return LogProperties.APPENDER_PREFIX;
465                }
466
467                /**
468                 * Fills in whatever of flags/appender type is not already explicitly set on this
469                 * builder from properties keyed under {@link #propertyPrefix()} for this
470                 * builder's name; an already-set field always wins over the property, matching
471                 * every other generated builder's {@code fromProperties} in this project. Both
472                 * are resolved and validated together against one {@link LogProperty.Validator},
473                 * so a builder with both malformed reports both in a single exception rather than
474                 * just the first one reached.
475                 * <p>
476                 * Output/encoder are deliberately not handled here even though they too are keyed
477                 * under {@link #propertyPrefix()}: turning either into a concrete, registered
478                 * {@link LogOutput}/{@link LogEncoder} needs a {@link LogConfig} (to actually
479                 * look up the URI scheme), which this method does not have. {@link #build()}
480                 * resolves those two once a real {@link LogConfig} is available, using the same
481                 * property keys.
482                 * @param properties properties to resolve unset fields from.
483                 * @return this.
484                 */
485                @Override
486                public Builder fromProperties(LogProperties properties) {
487                        /*
488                         * Register every result with the validator first, and only call validate(),
489                         * which throws one aggregate exception up front if anything registered is
490                         * Missing (when added via add(...)) or an Error (either method), before
491                         * extracting any individual value below. Extracting a value via
492                         * value()/valueOrNull() throws immediately for a still-unhandled Error (see
493                         * LogProperty.Result.Error's own valueOrNull()), which would otherwise let
494                         * the first bad property escape unwrapped before the Validator ever got a
495                         * chance to collect the rest: the same ordering the annotation processor
496                         * generates for every other builder in this project.
497                         */
498                        var validator = LogProperty.Validator.of(LogAppender.class);
499                        var flagsResult = flags == null ? properties.forKey(APPENDER_FLAGS_PROPERTY, name)
500                                .ofList()
501                                .map(AppenderFlag::parse)
502                                .validateIfError(validator) : null;
503                        var appenderTypeResult = appenderType == null ? properties.forKey(APPENDER_TYPE_PROPERTY, name)
504                                .ofString()
505                                .map(AppenderType::parse)
506                                .validateIfError(validator) : null;
507                        validator.validate();
508                        if (flagsResult != null) {
509                                var resolved = flagsResult.valueOrNull();
510                                // AppenderFlag.parse always returns an EnumSet (empty or not), so
511                                // EnumSet.copyOf is always safe here, never the "non-EnumSet and empty"
512                                // case it rejects.
513                                if (resolved != null) {
514                                        flags = EnumSet.copyOf(resolved);
515                                }
516                        }
517                        if (appenderTypeResult != null) {
518                                appenderType = appenderTypeResult.valueOrNull();
519                        }
520                        return this;
521                }
522
523                /**
524                 * Builds.
525                 * @return an appender factory.
526                 */
527                public LogProvider<LogAppender> build() {
528                        /*
529                         * We need to capture parameters since appender creation needs to be lazy, and
530                         * a copy is made below (rather than mutating this builder's own fields) so a
531                         * shared/reused Builder instance is never mutated by a later
532                         * fromProperties(...) call the lazy lambda makes once a real LogConfig is
533                         * available.
534                         */
535                        var _name = name;
536                        var _output = output;
537                        var _outputDefault = outputDefault;
538                        var _encoder = encoder;
539                        var _flags = flags;
540                        var _appenderType = appenderType;
541                        /*
542                         * TODO should we use the parent name for resolution?
543                         */
544                        return (n, config) -> {
545                                var b = new Builder(_name);
546                                b.flags = _flags;
547                                b.appenderType = _appenderType;
548                                b.fromProperties(config.properties());
549
550                                LogOutput output = LogProvider.provideOrNull(_output, _name, config);
551                                if (output == null) {
552                                        /*
553                                         * A malformed (not just missing) property must throw here,
554                                         * immediately, regardless of whether a default exists below: e.g.
555                                         * "console" with a genuinely bad logging.appender.console.output must
556                                         * fail loudly, not silently fall back to stdout. Only the Missing
557                                         * case falls through to outputDefault.
558                                         */
559                                        output = switch (outputProperty(_name, config)) {
560                                                case LogProperty.Result.Success<LogOutput> s -> s.value();
561                                                case LogProperty.Result.Missing<LogOutput> m -> null;
562                                                case LogProperty.Result.Error<LogOutput> e -> e.value();
563                                        };
564                                }
565                                if (output == null) {
566                                        output = LogProvider.provideOrNull(_outputDefault, _name, config);
567                                }
568                                if (output == null) {
569                                        /*
570                                         * No explicit value, no property, no default: the same missing
571                                         * -property failure a required property with no fallback produces
572                                         * anywhere else. Re-deriving the same (definitely still Missing)
573                                         * result and calling value() throws it unwrapped, no Validator
574                                         * involved, same as before this refactor.
575                                         */
576                                        output = DefaultAppenderRegistry.rawValue(outputProperty(_name, config)).value();
577                                }
578
579                                final LogOutput finalOutput = output;
580                                LogEncoder explicitEncoder = _encoder != null ? LogProvider.provideOrNull(_encoder, _name, config)
581                                                : null;
582                                LogEncoder encoder;
583                                if (finalOutput instanceof LogOutput.ProvidesEncoder pe
584                                                && pe.policy() == LogOutput.ProvidesEncoder.Policy.MANDATORY) {
585                                        boolean propertyWired = !(encoderProperty(_name, config) instanceof LogProperty.Result.Missing);
586                                        if (explicitEncoder != null || propertyWired) {
587                                                throw LogProperty.ValidationException
588                                                        .of(LogAppender.class, new IllegalArgumentException("Appender '" + _name + "' output "
589                                                                        + finalOutput.getClass().getName()
590                                                                        + " provides a mandatory encoder and does not allow another encoder to be configured."));
591                                        }
592                                        encoder = pe.encoder(_name, config);
593                                }
594                                else {
595                                        encoder = explicitEncoder;
596                                        if (encoder == null) {
597                                                var propertyResult = encoderProperty(_name, config);
598                                                if (finalOutput instanceof LogOutput.ProvidesEncoder pe
599                                                                && propertyResult instanceof LogProperty.Result.Missing) {
600                                                        encoder = pe.encoder(_name, config);
601                                                }
602                                                else {
603                                                        encoder = DefaultAppenderRegistry.rawValue(propertyResult.or(() -> config.encoderRegistry()
604                                                                .encoderForOutputType(finalOutput.type())
605                                                                .provide(_name, config))).value();
606                                                }
607                                        }
608                                }
609
610                                Set<AppenderFlag> flags = b.flags != null ? b.flags : EnumSet.noneOf(AppenderFlag.class);
611                                AppenderType appenderType = b.appenderType != null ? b.appenderType
612                                                : AbstractLogAppender.globalOptimizeEnabled ? AppenderType.AUTO_DETECT
613                                                                : AppenderType.LOCK_THREAD_LOCAL_BUFFER;
614
615                                return DirectLogAppender.of(_name, output, encoder, appenderType, flags, config.alerts(),
616                                                config.metrics());
617                        };
618                }
619
620                /*
621                 * Resolves APPENDER_OUTPUT_PROPERTY all the way to a concrete LogOutput (not just
622                 * a LogProvider), via LogProperty.provideValue so a failure inside the provided
623                 * LogOutput's own construction (its own separate property tree, e.g.
624                 * logging.output.file.bufferSize) is not relabeled "Error converting property" as
625                 * if APPENDER_OUTPUT_PROPERTY's own value had failed to convert - see
626                 * LogProperty#providingError. Moved here since output resolution is now entirely
627                 * this Builder's concern.
628                 */
629                private static LogProperty.Result<LogOutput> outputProperty(String name, LogConfig config) {
630                        return LogProperty.provideValue(
631                                        config.properties().forKey(APPENDER_OUTPUT_PROPERTY, name).ofProvider(LogOutput::of), name, config);
632                }
633
634                private static LogProperty.Result<LogEncoder> encoderProperty(String name, LogConfig config) {
635                        return LogProperty.provideValue(
636                                        config.properties().forKey(APPENDER_ENCODER_PROPERTY, name).ofProvider(LogEncoder::of), name,
637                                        config);
638                }
639
640        }
641
642        /**
643         * Provides appenders safely to the publisher. The providing calls of
644         * <code>asXXX</code> can only be called once as they register the appenders.
645         */
646        final class Appenders {
647
648                private final AtomicBoolean created = new AtomicBoolean();
649
650                private final String name;
651
652                private final LogConfig config;
653
654                private final List<LogProvider<LogAppender>> appenders;
655
656                /*
657                 * Set once, inside asList()/asSingle(), so LogReporter can find out what a
658                 * publisher was actually given without asking the publisher itself (which would
659                 * mean every current and future publisher implementation cooperating): see
660                 * resolvedOrNull(). Not re-derivable by calling asList()/asSingle() again: both
661                 * are one-shot (see the created guard above).
662                 */
663                private @Nullable List<? extends LogAppender> resolved;
664
665                Appenders(String name, LogConfig config, List<LogProvider<LogAppender>> appenders) {
666                        super();
667                        this.name = name;
668                        this.config = config;
669                        this.appenders = appenders;
670                }
671
672                /**
673                 * Return the appenders as a list.
674                 * @return list of appenders.
675                 * @throws IllegalStateException if appenders are already registered.
676                 */
677                public List<? extends LogAppender> asList() throws IllegalStateException {
678                        if (created.compareAndSet(false, true)) {
679                                var apps = appenders();
680                                List<LogAppender> appenders = new ArrayList<>();
681                                for (var a : apps) {
682                                        appenders.add(register(a));
683                                }
684                                resolved = appenders;
685                                return appenders;
686                        }
687                        else {
688                                throw new IllegalStateException("Appenders already provided.");
689                        }
690
691                }
692
693                /**
694                 * Consolidate the appenders as a single appender, appended synchronously. If more
695                 * than one appender is combined, each keeps its own independent lock and is
696                 * appended to directly - see {@link CompositeLogAppender}.
697                 * @return single appender.
698                 * @throws IllegalStateException if appenders are already registered.
699                 */
700                public LogAppender asSingle() throws IllegalStateException {
701                        if (created.compareAndSet(false, true)) {
702                                var apps = appenders();
703                                var appender = composite(apps);
704                                resolved = List.of(appender);
705                                return register(appender);
706                        }
707                        else {
708                                throw new IllegalStateException("Appenders already provided.");
709                        }
710                }
711
712                /**
713                 * The appenders actually resolved by whichever of {@link #asList()}/
714                 * {@link #asSingle()} a publisher factory called, or <code>null</code> if neither
715                 * has been called yet (a publisher factory that never calls either one has no
716                 * real appenders to report on regardless).
717                 * @return resolved appenders, or <code>null</code>.
718                 */
719                @Nullable List<? extends LogAppender> resolvedOrNull() {
720                        return resolved;
721                }
722
723                private LogAppender register(LogAppender appender) {
724                        return switch (appender) {
725                                case DirectLogAppender ia -> {
726                                        config.serviceRegistry().put(LogAppender.class, name + "." + ia.name(), ia);
727                                        yield ia;
728                                }
729                                case CompositeLogAppender ca -> {
730                                        config.serviceRegistry().put(LogAppender.class, name, ca);
731                                        yield ca;
732                                }
733                                default -> {
734                                        throw new IllegalStateException();
735                                }
736                        };
737                }
738
739                private List<LogAppender> appenders() {
740                        return LogProvider.flatten(appenders)
741                                .describe(n -> "Appenders for route: '" + n + "'")
742                                .provide(name, config);
743                }
744
745                /**
746                 * Creates a composite log appender from many, each keeping its own independent
747                 * lock.
748                 * @param appenders appenders.
749                 * @return appender.
750                 */
751                private static LogAppender composite(List<? extends LogAppender> appenders) {
752                        if (appenders.isEmpty()) {
753                                throw new IllegalArgumentException("A single appender is required");
754                        }
755                        if (appenders.size() == 1) {
756                                return Objects.requireNonNull(appenders.get(0));
757                        }
758                        return CompositeLogAppender.of(appenders);
759                }
760
761        }
762
763        @Override
764        public void close();
765
766}
767
768interface AppenderVisitor {
769
770        boolean consume(DirectLogAppender appender);
771
772}
773
774/**
775 * This is a JAVADOC BUG
776 */
777sealed interface InternalLogAppender extends LogAppender, Actor {
778
779        static InternalLogAppender of(LogAppender appender) {
780                return Objects.requireNonNull((InternalLogAppender) appender); // TODO eclipse
781                                                                                                                                                // bug.
782        }
783
784        /**
785         * An appender can act on actions. One of the key actions is reopening files.
786         * @param action action to run.
787         * @return alert events for anything that failed, empty if the action fully succeeded.
788         */
789        @Override
790        public List<LogEvent> act(LogAction action);
791
792}
793
794sealed interface DirectLogAppender extends InternalLogAppender {
795
796        String name();
797
798        LogOutput output();
799
800        LogEncoder encoder();
801
802        default List<LogEvent> _request(LogAction action) {
803                return switch (action) {
804                        case LogAction.StandardAction a -> switch (a) {
805                                case LogAction.StandardAction.REOPEN -> reopen();
806                                case LogAction.StandardAction.FLUSH -> flush();
807                        };
808                };
809        }
810
811        /**
812         * Reopens {@link #output()}. Any failure is reported to the alert system rather than
813         * thrown, at {@link Level#ERROR}.
814         * @return the alert event if reopening failed, empty if it succeeded.
815         */
816        List<LogEvent> reopen();
817
818        /**
819         * Flushes {@link #output()}. Any failure is reported to the alert system rather than
820         * thrown, at {@link Level#ERROR}.
821         * @return the alert event if flushing failed, empty if it succeeded.
822         */
823        List<LogEvent> flush();
824
825        static DirectLogAppender of(String name, LogOutput output, LogEncoder encoder, AppenderType type,
826                        Set<LogAppender.AppenderFlag> flags, LogAlerts alerts, LogMetrics metrics) {
827                type = AbstractLogAppender.resolveAutoDetectAppenderType(type);
828                type = AbstractLogAppender.guardSynchronizedAppenderType(type);
829                type = AbstractLogAppender.guardThreadLocalAppenderType(type);
830                return switch (type) {
831                        case REUSE_BUFFER ->
832                                new ReuseBufferLogAppender(name, output, encoder, flags, new ReentrantLock(), alerts, metrics);
833                        case SYNCHRONIZED_THREAD_LOCAL_BUFFER ->
834                                new SynchronizedThreadLocalBufferLogAppender(name, output, encoder, flags, alerts, metrics);
835                        case LOCK_THREAD_LOCAL_BUFFER -> new LockThreadLocalBufferLogAppender(name, output, encoder, flags,
836                                        new ReentrantLock(), alerts, metrics);
837                        case LOCK_NEW_BUFFER ->
838                                new LockNewBufferLogAppender(name, output, encoder, flags, new ReentrantLock(), alerts, metrics);
839                        case AUTO_DETECT ->
840                                throw new IllegalStateException("AUTO_DETECT should have already been resolved to a concrete type");
841                };
842        }
843
844}
845
846/**
847 * An abstract appender to help create custom appenders.
848 */
849sealed abstract class AbstractLogAppender implements DirectLogAppender {
850
851        /*
852         * Set once from LogProperties#GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY during
853         * LogConfig construction (see DefaultLogConfig) - a global, process-wide guarantee
854         * that no appender will ever use `synchronized`, for deployments that want that
855         * guaranteed even when something explicitly requests
856         * SYNCHRONIZED_THREAD_LOCAL_BUFFER. Global (not per-route/per-appender) by design,
857         * matching the property's own scope.
858         */
859        static volatile boolean forceReentrantLockAppenders = false;
860
861        /**
862         * Downgrades an explicit
863         * {@link LogAppender.AppenderType#SYNCHRONIZED_THREAD_LOCAL_BUFFER} to
864         * {@link LogAppender.AppenderType#LOCK_THREAD_LOCAL_BUFFER} if
865         * {@link #forceReentrantLockAppenders} is active - the enforcement point that makes
866         * the global no-synchronized guarantee a real guarantee rather than just a changed
867         * default.
868         * @param type type as given to an appender factory method.
869         * @return {@code type} unchanged, unless the guarantee is active and
870         * {@code SYNCHRONIZED_THREAD_LOCAL_BUFFER} was requested, in which case
871         * {@code LOCK_THREAD_LOCAL_BUFFER} instead.
872         */
873        static LogAppender.AppenderType guardSynchronizedAppenderType(LogAppender.AppenderType type) {
874                if (!forceReentrantLockAppenders || type != LogAppender.AppenderType.SYNCHRONIZED_THREAD_LOCAL_BUFFER) {
875                        return type;
876                }
877                return LogAppender.AppenderType.LOCK_THREAD_LOCAL_BUFFER;
878        }
879
880        /*
881         * Set once from LogProperties#GLOBAL_THREADLOCAL_DISABLED_PROPERTY during LogConfig
882         * construction (see DefaultLogConfig) - a global, process-wide guarantee that no
883         * appender will ever use ThreadLocal, for deployments that want that guaranteed even
884         * when something explicitly requests LOCK_THREAD_LOCAL_BUFFER (the default) or
885         * SYNCHRONIZED_THREAD_LOCAL_BUFFER. Global (not per-route/per-appender) by design,
886         * matching the property's own scope - rainbowgum-slf4j's MDC support independently
887         * reads the same property key to decide whether to disable itself too, see
888         * LogProperties#GLOBAL_THREADLOCAL_DISABLED_PROPERTY's javadoc.
889         */
890        static volatile boolean forceNoThreadLocalAppenders = false;
891
892        /*
893         * Set once from LogProperties#GLOBAL_OPTIMIZE_PROPERTY during LogConfig construction
894         * (see DefaultLogConfig) - unlike
895         * forceReentrantLockAppenders/forceNoThreadLocalAppenders above, this is not a
896         * downgrade applied to an already-resolved type; it only changes what
897         * LogAppender.Builder#build() picks when appenderType was never set at all
898         * (explicitly, either way, always wins). See LogProperties#GLOBAL_OPTIMIZE_PROPERTY's
899         * javadoc for what it currently resolves to and why that is not a contract.
900         */
901        static volatile boolean globalOptimizeEnabled = false;
902
903        /**
904         * Downgrades either {@link ThreadLocal}-backed type (
905         * {@link LogAppender.AppenderType#LOCK_THREAD_LOCAL_BUFFER} or
906         * {@link LogAppender.AppenderType#SYNCHRONIZED_THREAD_LOCAL_BUFFER}) to
907         * {@link LogAppender.AppenderType#LOCK_NEW_BUFFER} if
908         * {@link #forceNoThreadLocalAppenders} is active - the enforcement point that makes
909         * the global no-{@link ThreadLocal} guarantee a real guarantee rather than just a
910         * changed default. An explicit {@link LogAppender.AppenderType#REUSE_BUFFER} request
911         * is left as-is - it is already {@link ThreadLocal}-free, so there is nothing to
912         * downgrade.
913         * @param type type as given to an appender factory method.
914         * @return {@code type} unchanged, unless the guarantee is active and a
915         * {@link ThreadLocal}-backed type was requested, in which case
916         * {@code LOCK_NEW_BUFFER} instead.
917         */
918        static LogAppender.AppenderType guardThreadLocalAppenderType(LogAppender.AppenderType type) {
919                if (!forceNoThreadLocalAppenders) {
920                        return type;
921                }
922                return switch (type) {
923                        case LOCK_THREAD_LOCAL_BUFFER, SYNCHRONIZED_THREAD_LOCAL_BUFFER -> LogAppender.AppenderType.LOCK_NEW_BUFFER;
924                        case REUSE_BUFFER, LOCK_NEW_BUFFER -> type;
925                        case AUTO_DETECT ->
926                                throw new IllegalStateException("AUTO_DETECT should have already been resolved to a concrete type");
927                };
928        }
929
930        /**
931         * Resolves {@link LogAppender.AppenderType#AUTO_DETECT} to
932         * {@link LogAppender.AppenderType#SYNCHRONIZED_THREAD_LOCAL_BUFFER} or
933         * {@link LogAppender.AppenderType#LOCK_THREAD_LOCAL_BUFFER} depending on
934         * {@link #isNativeImageRuntime()}; any other type is returned unchanged. Called
935         * before {@link #guardSynchronizedAppenderType(LogAppender.AppenderType)} and
936         * {@link #guardThreadLocalAppenderType(LogAppender.AppenderType)} so a resolved
937         * {@code SYNCHRONIZED_THREAD_LOCAL_BUFFER} is still subject to both of those global
938         * guarantees the same as if it had been requested directly.
939         * @param type type as given to an appender factory method.
940         * @return {@code type} unchanged unless it was {@code AUTO_DETECT}.
941         */
942        static LogAppender.AppenderType resolveAutoDetectAppenderType(LogAppender.AppenderType type) {
943                if (type != LogAppender.AppenderType.AUTO_DETECT) {
944                        return type;
945                }
946                return isNativeImageRuntime() ? LogAppender.AppenderType.SYNCHRONIZED_THREAD_LOCAL_BUFFER
947                                : LogAppender.AppenderType.LOCK_THREAD_LOCAL_BUFFER;
948        }
949
950        /**
951         * The system property GraalVM native-image itself sets to {@code buildtime} during
952         * the image build and {@code runtime} when the built image actually executes - absent
953         * entirely on a plain JVM. The standard, dependency-free way every native-image-aware
954         * framework checks "am I native" without a hard compile-time dependency on GraalVM's
955         * own SDK just to ask this one question.
956         */
957        static final String NATIVE_IMAGE_CODE_PROPERTY = "org.graalvm.nativeimage.imagecode";
958
959        /**
960         * Whether this process is currently running as an executing (not building) GraalVM
961         * native image - see {@link #NATIVE_IMAGE_CODE_PROPERTY}.
962         * @return true if running as a native image at runtime.
963         */
964        static boolean isNativeImageRuntime() {
965                return "runtime".equals(System.getProperty(NATIVE_IMAGE_CODE_PROPERTY));
966        }
967
968        /**
969         * Whether an appender should drop (or drop-and-log) an append call because it is
970         * reentrant - i.e. the current thread is already inside a previous call to the same
971         * appender's write path, which happens if an output does logging itself during its
972         * own write. Shared by every appender that can detect reentrancy, regardless of
973         * whether it does so via a {@link ReentrantLock} (
974         * {@code lock.isHeldByCurrentThread()}) or a {@code synchronized} block (
975         * {@link Thread#holdsLock(Object)}) - callers pass in whichever check applies to
976         * them. Whenever this returns {@code true}, {@link LogMetrics#EVENTS_DROPPED_METRIC}
977         * is incremented by {@code count} regardless of whether
978         * {@link AppenderFlag#REENTRY_LOG} is set - counting and logging/alerting are
979         * separate concerns, so the count still happens even when the drop itself is silent.
980         * @param reentrant whether the current thread already holds this appender's
981         * lock/monitor.
982         * @param flags the appender's flags.
983         * @param metrics where to record {@link LogMetrics#EVENTS_DROPPED_METRIC} if events
984         * are dropped.
985         * @param count number of events that would be dropped - {@code 1} for a single event
986         * append, or the batch size for a batch append.
987         * @return {@code true} if the caller should drop the event(s) without appending.
988         */
989        static boolean shouldDropForReentry(boolean reentrant, Set<LogAppender.AppenderFlag> flags, LogMetrics metrics,
990                        int count) {
991                if (!reentrant) {
992                        return false;
993                }
994                if (flags.contains(LogAppender.AppenderFlag.REENTRY_LOG)) {
995                        Exception exception = new Exception("reentrant appender");
996                        MetaLog.error(LogAppender.class, exception);
997                        metrics.errorCounter(LogMetrics.EVENTS_DROPPED_METRIC, count);
998                        return true;
999                }
1000                if (flags.contains(LogAppender.AppenderFlag.REENTRY_DROP)) {
1001                        metrics.errorCounter(LogMetrics.EVENTS_DROPPED_METRIC, count);
1002                        return true;
1003                }
1004                return false;
1005        }
1006
1007        /**
1008         * name.
1009         */
1010        protected final String name;
1011
1012        /**
1013         * output
1014         */
1015        protected final LogOutput output;
1016
1017        /**
1018         * encoder
1019         */
1020        protected final LogEncoder encoder;
1021
1022        protected final Set<LogAppender.AppenderFlag> flags;
1023
1024        protected final boolean immediateFlush;
1025
1026        /**
1027         * alerts for reporting encode/write failures that this appender catches so they never
1028         * propagate back to whatever application thread called logger.info(...).
1029         */
1030        protected final LogAlerts alerts;
1031
1032        /**
1033         * metrics for recording counters like {@link LogMetrics#EVENTS_DROPPED_METRIC}.
1034         */
1035        protected final LogMetrics metrics;
1036
1037        /**
1038         * Creates an appender from an output and encoder.
1039         * @param output set the output field and will be started and closed with the
1040         * appender.
1041         * @param encoder set the encoder field.
1042         * @param alerts alerts for reporting encode/write failures.
1043         * @param metrics metrics for recording counters.
1044         */
1045        protected AbstractLogAppender(String name, LogOutput output, LogEncoder encoder,
1046                        Set<LogAppender.AppenderFlag> flags, LogAlerts alerts, LogMetrics metrics) {
1047                super();
1048                this.name = name;
1049                this.output = output;
1050                this.encoder = encoder;
1051                this.flags = flags;
1052                this.immediateFlush = !flags.contains(LogAppender.AppenderFlag.DISABLE_IMMEDIATE_FLUSH);
1053                this.alerts = alerts;
1054                this.metrics = metrics;
1055        }
1056
1057        @Override
1058        public void start(LogConfig config) {
1059                output.start(config);
1060        }
1061
1062        @Override
1063        public void close() {
1064                output.close();
1065        }
1066
1067        @Override
1068        public String toString() {
1069                return getClass().getName() + "[name=" + name + " encoder=" + encoder + ", " + "output=" + output + ", flags="
1070                                + flags + "]";
1071        }
1072
1073        @Override
1074        public String name() {
1075                return this.name;
1076        }
1077
1078        @Override
1079        public LogOutput output() {
1080                return this.output;
1081        }
1082
1083        @Override
1084        public LogEncoder encoder() {
1085                return this.encoder;
1086        }
1087
1088        @Override
1089        public List<LogEvent> reopen() {
1090                try {
1091                        output.reopen();
1092                        return List.of();
1093                }
1094                catch (Exception e) {
1095                        var event = errorEvent(getClass(), "appender '" + name + "' failed to reopen output", e);
1096                        alerts.error(event);
1097                        return List.of(event);
1098                }
1099        }
1100
1101        @Override
1102        public List<LogEvent> flush() {
1103                try {
1104                        output.flush();
1105                        return List.of();
1106                }
1107                catch (Exception e) {
1108                        var event = errorEvent(getClass(), "appender '" + name + "' failed to flush output", e);
1109                        alerts.error(event);
1110                        return List.of(event);
1111                }
1112        }
1113
1114        private static LogEvent errorEvent(Class<?> loggerName, String message, Throwable throwable) {
1115                var currentThread = Thread.currentThread();
1116                return LogEvent.of(Instant.now(), currentThread.getName(), currentThread.threadId(), Level.ERROR,
1117                                loggerName.getName(), message, KeyValues.of(), throwable);
1118        }
1119
1120}
1121
1122/**
1123 * Combines more than one appender on a route into one {@link LogAppender}. Each appender
1124 * keeps the independent lock it was already constructed with, and
1125 * {@link #append(LogEvent)}/{@link #append(LogEvent[], int)} skip locking at the
1126 * composite level entirely and append to every component directly - so e.g. a console
1127 * appender and a file appender under the same route never contend on the same lock for
1128 * unrelated I/O. {@link #start(LogConfig)}/{@link #close()}/{@link #act(LogAction)} do
1129 * the same - there is no composite-owned mutable state to protect, only a loop over
1130 * components that already handle their own synchronization where it matters.
1131 */
1132@SuppressWarnings("ArrayRecordComponent")
1133record CompositeLogAppender(DirectLogAppender[] appenders) implements InternalLogAppender {
1134
1135        public static CompositeLogAppender of(List<? extends LogAppender> appenders) {
1136                @SuppressWarnings("null") // TODO Eclipse issue here
1137                DirectLogAppender @NonNull [] array = appenders.stream()
1138                        .map(CompositeLogAppender::cast)
1139                        .toArray(i -> new DirectLogAppender[i]);
1140                return new CompositeLogAppender(array);
1141        }
1142
1143        private static DirectLogAppender cast(LogAppender appender) {
1144                return (DirectLogAppender) appender;
1145        }
1146
1147        @Override
1148        public void append(LogEvent event) {
1149                for (var appender : appenders) {
1150                        appender.append(event);
1151                }
1152        }
1153
1154        @Override
1155        public void append(LogEvent[] event, int count) {
1156                for (var appender : appenders) {
1157                        appender.append(event, count);
1158                }
1159        }
1160
1161        @Override
1162        public void close() {
1163                for (var appender : appenders) {
1164                        appender.close();
1165                }
1166        }
1167
1168        @Override
1169        public void start(LogConfig config) {
1170                for (var appender : appenders) {
1171                        appender.start(config);
1172                }
1173        }
1174
1175        @Override
1176        public List<LogEvent> act(LogAction action) {
1177                return Actor.act(appenders, action);
1178        }
1179
1180        @Override
1181        public String toString() {
1182                return getClass().getName() + "[appenders=" + Arrays.toString(appenders) + "]";
1183        }
1184
1185}
1186
1187sealed abstract class LockLogAppender extends AbstractLogAppender implements InternalLogAppender {
1188
1189        protected final ReentrantLock lock;
1190
1191        public LockLogAppender(String name, LogOutput output, LogEncoder encoder, Set<LogAppender.AppenderFlag> flags,
1192                        ReentrantLock lock, LogAlerts alerts, LogMetrics metrics) {
1193                super(name, output, encoder, flags, alerts, metrics);
1194                this.lock = lock;
1195        }
1196
1197        @Override
1198        public List<LogEvent> act(LogAction action) {
1199                lock.lock();
1200                try {
1201                        return _request(action);
1202                }
1203                finally {
1204                        lock.unlock();
1205                }
1206        }
1207
1208        @Override
1209        public void close() {
1210                lock.lock();
1211                try {
1212                        super.close();
1213                }
1214                finally {
1215                        lock.unlock();
1216                }
1217        }
1218
1219}
1220
1221/*
1222 * The idea here is to reuse the buffer trading lock contention for less GC.
1223 */
1224final class ReuseBufferLogAppender extends LockLogAppender implements InternalLogAppender {
1225
1226        private final LogEncoder.Buffer buffer;
1227
1228        ReuseBufferLogAppender(String name, LogOutput output, LogEncoder encoder, Set<LogAppender.AppenderFlag> flags,
1229                        ReentrantLock lock, LogAlerts alerts, LogMetrics metrics) {
1230                super(name, output, encoder, flags, lock, alerts, metrics);
1231                this.buffer = encoder.buffer(output.bufferHints());
1232        }
1233
1234        @Override
1235        public final void append(LogEvent event) {
1236                if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, 1)) {
1237                        return;
1238                }
1239                try {
1240                        lock.lock();
1241                        try {
1242                                buffer.clear();
1243                                encoder.encode(event, buffer);
1244                                output.write(event, buffer);
1245                                if (immediateFlush) {
1246                                        output.flush();
1247                                }
1248                        }
1249                        finally {
1250                                lock.unlock();
1251                        }
1252                }
1253                catch (Exception e) {
1254                        alerts.error(getClass(), "appender '" + name + "' failed to append event", e);
1255                        metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, 1);
1256                }
1257        }
1258
1259        @Override
1260        public void append(LogEvent[] events, int count) {
1261                if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, count)) {
1262                        return;
1263                }
1264                try {
1265                        lock.lock();
1266                        try {
1267                                output.write(events, count, encoder, buffer);
1268                                if (immediateFlush) {
1269                                        output.flush();
1270                                }
1271                        }
1272                        finally {
1273                                lock.unlock();
1274                        }
1275                }
1276                catch (Exception e) {
1277                        alerts.error(getClass(), "appender '" + name + "' failed to append batch of " + count + " event(s)", e);
1278                        metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, count);
1279                }
1280        }
1281
1282        @Override
1283        public void close() {
1284                lock.lock();
1285                try {
1286                        super.close();
1287                        buffer.close();
1288                }
1289                finally {
1290                        lock.unlock();
1291                }
1292        }
1293
1294}
1295
1296/*
1297 * The idea here is to encode outside the lock. Instead of allocating a fresh buffer per
1298 * event or sharing (and thus serializing access to) a single buffer
1299 * (ReuseBufferLogAppender), each thread gets its own buffer that only it will ever touch,
1300 * so encoding never needs to be guarded by the lock at all - only the final write to the
1301 * output does.
1302 */
1303final class LockThreadLocalBufferLogAppender extends LockLogAppender implements InternalLogAppender {
1304
1305        /*
1306         * There is no way to enumerate every thread's buffer to close it on appender close so
1307         * we rely on Buffer implementations not holding onto real resources (today they are
1308         * all just wrapped in-memory builders) and let the ThreadLocal itself (and
1309         * consequently the per-thread entries) become collectible once this appender is
1310         * discarded.
1311         */
1312        // CheckerFramework's ThreadLocal stub declares T as inherently @Nullable since get()
1313        // can return null before initialValue() runs, but withInitial(...) below guarantees
1314        // it never does here.
1315        @SuppressWarnings("nullness:type.argument")
1316        private final ThreadLocal<LogEncoder.Buffer> bufferThreadLocal;
1317
1318        LockThreadLocalBufferLogAppender(String name, LogOutput output, LogEncoder encoder,
1319                        Set<LogAppender.AppenderFlag> flags, ReentrantLock lock, LogAlerts alerts, LogMetrics metrics) {
1320                super(name, output, encoder, flags, lock, alerts, metrics);
1321                this.bufferThreadLocal = ThreadLocal.withInitial(() -> encoder.buffer(output.bufferHints()));
1322        }
1323
1324        @Override
1325        public final void append(LogEvent event) {
1326                try {
1327                        var buffer = bufferThreadLocal.get();
1328                        buffer.clear();
1329                        encoder.encode(event, buffer);
1330                        writeLocked(event, buffer);
1331                }
1332                catch (Exception e) {
1333                        alerts.error(getClass(), "appender '" + name + "' failed to append event", e);
1334                        metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, 1);
1335                }
1336        }
1337
1338        private void writeLocked(LogEvent event, LogEncoder.Buffer buffer) {
1339                if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, 1)) {
1340                        return;
1341                }
1342                lock.lock();
1343                try {
1344                        output.write(event, buffer);
1345                        if (immediateFlush) {
1346                                output.flush();
1347                        }
1348                }
1349                finally {
1350                        lock.unlock();
1351                }
1352        }
1353
1354        @Override
1355        public void append(LogEvent[] events, int count) {
1356                if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, count)) {
1357                        return;
1358                }
1359                try {
1360                        lock.lock();
1361                        try {
1362                                output.write(events, count, encoder, bufferThreadLocal.get());
1363                                if (immediateFlush) {
1364                                        output.flush();
1365                                }
1366                        }
1367                        finally {
1368                                lock.unlock();
1369                        }
1370                }
1371                catch (Exception e) {
1372                        alerts.error(getClass(), "appender '" + name + "' failed to append batch of " + count + " event(s)", e);
1373                        metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, count);
1374                }
1375        }
1376
1377}
1378
1379/*
1380 * Like LockThreadLocalBufferLogAppender (encode outside the lock, lock only the final
1381 * write) except the buffer is a fresh allocation per event instead of a ThreadLocal - for
1382 * deployments that want a hard guarantee of no ThreadLocal anywhere in the logging path
1383 * without paying ReuseBufferLogAppender's lock-across-the-whole-encode cost.
1384 */
1385final class LockNewBufferLogAppender extends LockLogAppender implements InternalLogAppender {
1386
1387        LockNewBufferLogAppender(String name, LogOutput output, LogEncoder encoder, Set<LogAppender.AppenderFlag> flags,
1388                        ReentrantLock lock, LogAlerts alerts, LogMetrics metrics) {
1389                super(name, output, encoder, flags, lock, alerts, metrics);
1390        }
1391
1392        @Override
1393        public final void append(LogEvent event) {
1394                try {
1395                        var buffer = encoder.buffer(output.bufferHints());
1396                        encoder.encode(event, buffer);
1397                        writeLocked(event, buffer);
1398                }
1399                catch (Exception e) {
1400                        alerts.error(getClass(), "appender '" + name + "' failed to append event", e);
1401                        metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, 1);
1402                }
1403        }
1404
1405        private void writeLocked(LogEvent event, LogEncoder.Buffer buffer) {
1406                if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, 1)) {
1407                        return;
1408                }
1409                lock.lock();
1410                try {
1411                        output.write(event, buffer);
1412                        if (immediateFlush) {
1413                                output.flush();
1414                        }
1415                }
1416                finally {
1417                        lock.unlock();
1418                }
1419        }
1420
1421        @Override
1422        public void append(LogEvent[] events, int count) {
1423                if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, count)) {
1424                        return;
1425                }
1426                try {
1427                        var buffer = encoder.buffer(output.bufferHints());
1428                        lock.lock();
1429                        try {
1430                                output.write(events, count, encoder, buffer);
1431                                if (immediateFlush) {
1432                                        output.flush();
1433                                }
1434                        }
1435                        finally {
1436                                lock.unlock();
1437                        }
1438                }
1439                catch (Exception e) {
1440                        alerts.error(getClass(), "appender '" + name + "' failed to append batch of " + count + " event(s)", e);
1441                        metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, count);
1442                }
1443        }
1444
1445}
1446
1447/*
1448 * Like LockThreadLocalBufferLogAppender (per-thread reused buffer, encode outside any
1449 * lock) but the final write is protected by a plain `synchronized` block on this
1450 * appender's own monitor instead of a ReentrantLock. Does not extend LockLogAppender -
1451 * there is no way to acquire a monitor in one method call and release it in another, so
1452 * this appender's critical sections are written as literal synchronized blocks instead.
1453 */
1454final class SynchronizedThreadLocalBufferLogAppender extends AbstractLogAppender implements InternalLogAppender {
1455
1456        private final Object monitor = new Object();
1457
1458        // See LockThreadLocalBufferLogAppender's identical field for why this suppression is
1459        // needed.
1460        @SuppressWarnings("nullness:type.argument")
1461        private final ThreadLocal<LogEncoder.Buffer> bufferThreadLocal;
1462
1463        SynchronizedThreadLocalBufferLogAppender(String name, LogOutput output, LogEncoder encoder,
1464                        Set<LogAppender.AppenderFlag> flags, LogAlerts alerts, LogMetrics metrics) {
1465                super(name, output, encoder, flags, alerts, metrics);
1466                this.bufferThreadLocal = ThreadLocal.withInitial(() -> encoder.buffer(output.bufferHints()));
1467        }
1468
1469        @Override
1470        public void append(LogEvent event) {
1471                try {
1472                        var buffer = bufferThreadLocal.get();
1473                        buffer.clear();
1474                        encoder.encode(event, buffer);
1475                        writeSynchronized(event, buffer);
1476                }
1477                catch (Exception e) {
1478                        alerts.error(getClass(), "appender '" + name + "' failed to append event", e);
1479                        metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, 1);
1480                }
1481        }
1482
1483        private void writeSynchronized(LogEvent event, LogEncoder.Buffer buffer) {
1484                if (shouldDropForReentry(Thread.holdsLock(monitor), flags, metrics, 1)) {
1485                        return;
1486                }
1487                synchronized (monitor) {
1488                        output.write(event, buffer);
1489                        if (immediateFlush) {
1490                                output.flush();
1491                        }
1492                }
1493        }
1494
1495        @Override
1496        public void append(LogEvent[] events, int count) {
1497                if (shouldDropForReentry(Thread.holdsLock(monitor), flags, metrics, count)) {
1498                        return;
1499                }
1500                try {
1501                        synchronized (monitor) {
1502                                output.write(events, count, encoder, bufferThreadLocal.get());
1503                                if (immediateFlush) {
1504                                        output.flush();
1505                                }
1506                        }
1507                }
1508                catch (Exception e) {
1509                        alerts.error(getClass(), "appender '" + name + "' failed to append batch of " + count + " event(s)", e);
1510                        metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, count);
1511                }
1512        }
1513
1514        @Override
1515        public void close() {
1516                synchronized (monitor) {
1517                        super.close();
1518                }
1519        }
1520
1521        @Override
1522        public List<LogEvent> act(LogAction action) {
1523                synchronized (monitor) {
1524                        return _request(action);
1525                }
1526        }
1527
1528}