001package io.jstach.rainbowgum;
002
003import java.io.UncheckedIOException;
004import java.util.ArrayList;
005import java.util.Arrays;
006import java.util.Collection;
007import java.util.EnumSet;
008import java.util.List;
009import java.util.Locale;
010import java.util.Objects;
011import java.util.Set;
012import java.util.concurrent.atomic.AtomicBoolean;
013import java.util.concurrent.locks.ReentrantLock;
014
015import org.eclipse.jdt.annotation.NonNull;
016import org.eclipse.jdt.annotation.Nullable;
017
018import io.jstach.rainbowgum.LogResponse.Status;
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, LogEventConsumer {
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         * Batch of events. <strong>DO NOT MODIFY THE ARRAY</strong>. Do not use the
062         * <code>length</code> of the passed in array but instead use <code>count</code>
063         * parameter.
064         * @param events an array guaranteed to be smaller than count.
065         * @param count the number of items.
066         */
067        public void append(LogEvent[] events, int count);
068
069        @Override
070        public void append(LogEvent event);
071
072        /**
073         * Boolean like flags for appender that can be set with
074         * {@link LogAppender#APPENDER_FLAGS_PROPERTY}. Publisher may choose to add flags to
075         * the appenders and will be added if no flags are set on the appenders. Consequently
076         * great care should be taken when setting flags as performance maybe greatly impacted
077         * if a publisher is not designed for the flag.
078         */
079        @CaseChanging
080        public enum AppenderFlag {
081
082                /**
083                 * The appender will create a single buffer that will be reused and will be
084                 * protected by the appenders locking.
085                 */
086                REUSE_BUFFER,
087                /**
088                 * The appender will give each thread its own reusable buffer (a
089                 * {@link ThreadLocal}) instead of allocating a new buffer per event. Unlike
090                 * {@link #REUSE_BUFFER} the encoding is done <strong>outside</strong> the
091                 * appender's lock (the thread's buffer is only visited by that thread so no
092                 * protection is needed while encoding) with the lock only held for the final
093                 * write to the output, which is the same trade-off {@link #REUSE_BUFFER} makes
094                 * except without serializing the encoding step itself.
095                 * <p>
096                 * This flag is ignored if {@link #REUSE_BUFFER} is also set.
097                 * <p>
098                 * The same {@link ThreadLocal} buffer is used regardless of whether the calling
099                 * thread is a platform or virtual thread. A virtual thread's entry becomes
100                 * collectible once the thread itself terminates, and a typical unit of work (e.g.
101                 * one HTTP request) logs several times on the same thread, so reusing the buffer
102                 * across those calls still pays off even for short-lived virtual threads.
103                 * <p>
104                 * This is the strategy an appender uses <strong>even when no flag is explicitly
105                 * set</strong> - see {@code DirectLogAppender#defaultAppender} for the default
106                 * selection, and {@link #SYNCHRONIZED_THREAD_LOCAL_BUFFER} for the alternative
107                 * lock-kind opt-in.
108                 */
109                LOCK_THREAD_LOCAL_BUFFER,
110                /**
111                 * Like {@link #LOCK_THREAD_LOCAL_BUFFER} (a reused per-thread buffer, encoding
112                 * done outside any lock) except the final write to the output is protected by a
113                 * plain {@code synchronized} block (the JVM's intrinsic monitor) instead of a
114                 * {@link ReentrantLock}.
115                 * <p>
116                 * The Java language has no way to acquire a monitor in one method call and
117                 * release it in another, so this appender's critical sections are written as
118                 * literal {@code synchronized} blocks rather than going through a shared lock
119                 * abstraction the way every other flag combination does. {@link #REENTRY_DROP}
120                 * and {@link #REENTRY_LOG} are still honored though -
121                 * {@link Thread#holdsLock(Object)} is the {@code synchronized} equivalent of
122                 * {@code ReentrantLock}'s {@code isHeldByCurrentThread()}, so reentrancy is
123                 * detected the same way.
124                 * <p>
125                 * Motivated by Log4j2's own garbage-free appenders using {@code synchronized}
126                 * rather than a {@code java.util.concurrent} lock around their buffer-transfer
127                 * step, and confirmed by real-workload benchmarking to outperform
128                 * {@link ReentrantLock} under platform-thread contention - this was the default
129                 * for a time. Real-workload benchmarking under virtual threads found the
130                 * opposite, a large and reproducible loss versus {@link ReentrantLock} for
131                 * reasons not fully understood (classic JEP 491 pinning was checked and ruled
132                 * out), so {@link #LOCK_THREAD_LOCAL_BUFFER} is the default now and this flag is
133                 * an opt-in for platform-thread-heavy deployments that want the edge. Takes
134                 * precedence over {@link #LOCK_THREAD_LOCAL_BUFFER} (redundant if both are set)
135                 * but not {@link #REUSE_BUFFER}.
136                 * <p>
137                 * <strong>Explicitly setting this flag honors it</strong> even on a JDK where
138                 * {@code synchronized} still pins the carrier platform thread when called from a
139                 * virtual thread (before <a href="https://openjdk.org/jeps/491">JEP 491</a>,
140                 * finalized in JDK 24) - the one exception is
141                 * {@code LogProperties#GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY}: when that global
142                 * property is active it downgrades even an explicit request for this flag to
143                 * {@link #LOCK_THREAD_LOCAL_BUFFER}, since its whole point is a hard guarantee
144                 * independent of anything else in the configuration.
145                 */
146                SYNCHRONIZED_THREAD_LOCAL_BUFFER,
147                /**
148                 * By default the appender will call flush on each item appended or if in async
149                 * batch mode for each batch. This flag disables that behavior so that flushing is
150                 * left up to the output (or an external mechanism) instead.
151                 */
152                DISABLE_IMMEDIATE_FLUSH,
153                /**
154                 * The appender will drop events on reentry which happens if an appender during
155                 * its append causes recursive appending in the same thread. This is an analog to
156                 * what
157                 * <a href="https://logback.qos.ch/manual/appenders.html#AppenderBase">Logback
158                 * does by default</a>. Note that this is done using {@link ReentrantLock} and not
159                 * ThreadLocal like logback <strong>and is not done by default hence the
160                 * flag!</strong>
161                 * <p>
162                 * This flag is to allow outputs that do logging themselves. For performance
163                 * reasons and to allow async publishers it is recommended that you fix the output
164                 * code such that it does not do logging. This flag is ignored if
165                 * {@link #REENTRY_LOG} is set.
166                 * <p>
167                 * <strong>This flag will not fix outputs causing lool like logging if an async
168                 * publisher is used!</strong> That is why it is recommended you fix the output by
169                 * dropping events that would cause infinite loop like logging.
170                 * @see #REENTRY_LOG
171                 */
172                REENTRY_DROP,
173                /**
174                 * The appender will log events as errors to std error on reentry which happens if
175                 * an appender during its append causes recursive appending in the same thread.
176                 * This is an analog to what
177                 * <a href="https://logback.qos.ch/manual/appenders.html#AppenderBase">Logback
178                 * does by default</a>. Note that this is done using {@link ReentrantLock} and not
179                 * ThreadLocal like logback <strong>and is not done by default hence the
180                 * flag!</strong> This flag is to resolve failures of outputs that then do
181                 * logging.
182                 * <p>
183                 * This flag takes precedence over {@link #REENTRY_DROP}.
184                 */
185                REENTRY_LOG;
186
187                static Set<AppenderFlag> parse(Collection<String> value) {
188                        if (value.isEmpty()) {
189                                return EnumSet.noneOf(AppenderFlag.class);
190                        }
191                        var s = EnumSet.noneOf(AppenderFlag.class);
192                        for (var v : value) {
193                                s.add(parse(v));
194                        }
195                        return s;
196                }
197
198                static AppenderFlag parse(String value) {
199                        String v = value.toUpperCase(Locale.ROOT);
200                        return AppenderFlag.valueOf(v);
201                }
202
203        }
204
205        /**
206         * Creates a builder.
207         * @param name appender name.
208         * @return builder.
209         */
210        public static Builder builder(String name) {
211                return new Builder(name);
212        }
213
214        /**
215         * Builder for creating standard appenders.
216         * <p>
217         * If the output is not set standard out will be used. If the encoder is not set a
218         * default encoder will be resolved from the output.
219         */
220        public static final class Builder {
221
222                private @Nullable LogProvider<? extends LogOutput> output = null;
223
224                private @Nullable LogProvider<? extends LogEncoder> encoder = null;
225
226                private @Nullable EnumSet<AppenderFlag> flags = null;
227
228                private final String name;
229
230                private Builder(String name) {
231                        this.name = name;
232                }
233
234                /**
235                 * Name of the appender.
236                 * @return name.
237                 */
238                public String name() {
239                        return this.name;
240                }
241
242                /**
243                 * Sets output.
244                 * @param output output.
245                 * @return builder.
246                 */
247                public Builder output(LogProvider<? extends LogOutput> output) {
248                        this.output = output;
249                        return this;
250                }
251
252                /**
253                 * Sets output.
254                 * @param output output.
255                 * @return builder.
256                 */
257                public Builder output(LogOutput output) {
258                        this.output = LogProvider.of(output);
259                        return this;
260                }
261
262                /**
263                 * Sets formatter as encoder.
264                 * @param formatter formatter to be converted to encoder.
265                 * @return builder.
266                 * @see LogEncoder#of(LogFormatter)
267                 */
268                public Builder formatter(LogFormatter formatter) {
269                        this.encoder = LogProvider.of(LogEncoder.of(formatter));
270                        return this;
271                }
272
273                /**
274                 * Sets formatter as encoder.
275                 * @param formatter formatter to be converted to encoder.
276                 * @return builder.
277                 * @see LogEncoder#of(LogFormatter)
278                 */
279                public Builder formatter(LogFormatter.EventFormatter formatter) {
280                        this.encoder = LogProvider.of(LogEncoder.of(formatter));
281                        return this;
282                }
283
284                /**
285                 * Sets encoder.
286                 * @param encoder encoder not <code>null</code>.
287                 * @return builder.
288                 */
289                public Builder encoder(LogProvider<? extends LogEncoder> encoder) {
290                        this.encoder = encoder;
291                        return this;
292                }
293
294                /**
295                 * Sets encoder.
296                 * @param encoder encoder not <code>null</code>.
297                 * @return builder.
298                 */
299                public Builder encoder(LogEncoder encoder) {
300                        this.encoder = LogProvider.of(encoder);
301                        return this;
302                }
303
304                /**
305                 * Sets appender flags.
306                 * @param flags flags will replace all flags currently set.
307                 * @return this.
308                 */
309                public Builder flags(Collection<AppenderFlag> flags) {
310                        _flags().addAll(flags);
311                        return this;
312                }
313
314                private EnumSet<AppenderFlag> _flags() {
315                        EnumSet<AppenderFlag> flags = this.flags;
316                        if (flags == null) {
317                                this.flags = flags = EnumSet.noneOf(AppenderFlag.class);
318                        }
319                        return flags;
320                }
321
322                /**
323                 * Adds a flag.
324                 * @param flag flag.
325                 * @return this.
326                 */
327                public Builder flag(AppenderFlag flag) {
328                        _flags().add(flag);
329                        return this;
330                }
331
332                /**
333                 * Builds.
334                 * @return an appender factory.
335                 */
336                public LogProvider<LogAppender> build() {
337                        /*
338                         * We need to capture parameters since appender creation needs to be lazy.
339                         */
340                        var _name = name;
341                        var _output = output;
342                        var _encoder = encoder;
343                        var _flags = flags;
344                        /*
345                         * TODO should we use the parent name for resolution?
346                         */
347                        return (n, config) -> {
348                                AppenderConfig a = new AppenderConfig(_name, LogProvider.provideOrNull(_output, _name, config),
349                                                LogProvider.provideOrNull(_encoder, _name, config), _flags);
350                                return DefaultAppenderRegistry.appender(a, config);
351                        };
352                }
353
354        }
355
356        /**
357         * Provides appenders safely to the publisher. The providing calls of
358         * <code>asXXX</code> can only be called once as they register the appenders.
359         */
360        class Appenders {
361
362                private final AtomicBoolean created = new AtomicBoolean();
363
364                private final String name;
365
366                private final LogConfig config;
367
368                private final List<LogProvider<LogAppender>> appenders;
369
370                private Set<LogAppender.AppenderFlag> flags = EnumSet.noneOf(LogAppender.AppenderFlag.class);
371
372                Appenders(String name, LogConfig config, List<LogProvider<LogAppender>> appenders) {
373                        super();
374                        this.name = name;
375                        this.config = config;
376                        this.appenders = appenders;
377                }
378
379                /**
380                 * Sets flags for the appenders which should be done prior to <code>asXXX</code>.
381                 * @param flags appender flags.
382                 * @return this;
383                 */
384                public Appenders flags(Set<LogAppender.AppenderFlag> flags) {
385                        this.flags = flags;
386                        return this;
387                }
388
389                /**
390                 * Return the appenders as a list.
391                 * @return list of appenders.
392                 * @throws IllegalStateException if appenders are already registered.
393                 */
394                public List<? extends LogAppender> asList() throws IllegalStateException {
395                        if (created.compareAndSet(false, true)) {
396                                var apps = appenders();
397                                List<LogAppender> appenders = new ArrayList<>();
398                                for (var a : apps) {
399                                        appenders.add(register(a));
400                                }
401                                return appenders;
402                        }
403                        else {
404                                throw new IllegalStateException("Appenders already provided.");
405                        }
406
407                }
408
409                /**
410                 * Consolidate the appenders as a single appender, appended synchronously. If more
411                 * than one appender is combined, each keeps its own independent lock and is
412                 * appended to directly - see {@link CompositeLogAppender}.
413                 * @return single appender.
414                 * @throws IllegalStateException if appenders are already registered.
415                 */
416                public LogAppender asSingle() throws IllegalStateException {
417                        if (created.compareAndSet(false, true)) {
418                                var apps = appenders();
419                                var appender = composite(apps);
420                                return register(appender);
421                        }
422                        else {
423                                throw new IllegalStateException("Appenders already provided.");
424                        }
425                }
426
427                private LogAppender register(LogAppender appender) {
428                        return switch (appender) {
429                                case DirectLogAppender ia -> {
430                                        var _a = ia.withFlags(flags);
431                                        config.serviceRegistry().put(LogAppender.class, name + "." + _a.name(), _a);
432                                        yield _a;
433                                }
434                                case CompositeLogAppender ca -> {
435                                        var _a = ca.withFlags(flags);
436                                        config.serviceRegistry().put(LogAppender.class, name, _a);
437                                        yield _a;
438                                }
439                                default -> {
440                                        throw new IllegalStateException();
441                                }
442                        };
443                }
444
445                private List<LogAppender> appenders() {
446                        return LogProvider.flatten(appenders)
447                                .describe(n -> "Appenders for route: '" + n + "'")
448                                .provide(name, config);
449                }
450
451                /**
452                 * Creates a composite log appender from many, each keeping its own independent
453                 * lock.
454                 * @param appenders appenders.
455                 * @return appender.
456                 */
457                private static LogAppender composite(List<? extends LogAppender> appenders) {
458                        if (appenders.isEmpty()) {
459                                throw new IllegalArgumentException("A single appender is required");
460                        }
461                        if (appenders.size() == 1) {
462                                return Objects.requireNonNull(appenders.get(0));
463                        }
464                        return CompositeLogAppender.of(appenders, Set.of());
465                }
466
467        }
468
469        @Override
470        public void close();
471
472}
473
474interface AppenderVisitor {
475
476        boolean consume(DirectLogAppender appender);
477
478}
479
480/**
481 * This is a JAVADOC BUG
482 */
483sealed interface InternalLogAppender extends LogAppender, Actor {
484
485        static InternalLogAppender of(LogAppender appender) {
486                return Objects.requireNonNull((InternalLogAppender) appender); // TODO eclipse
487                                                                                                                                                // bug.
488        }
489
490        /**
491         * An appender can act on actions. One of the key actions is reopening files.
492         * @param action action to run.
493         * @return responses.
494         */
495        @Override
496        public List<LogResponse> act(LogAction action);
497
498}
499
500sealed interface DirectLogAppender extends InternalLogAppender {
501
502        String name();
503
504        LogOutput output();
505
506        LogEncoder encoder();
507
508        default List<LogResponse> _request(LogAction action) {
509                List<LogResponse> r = switch (action) {
510                        case LogAction.StandardAction a -> switch (a) {
511                                case LogAction.StandardAction.REOPEN -> List.of(reopen());
512                                case LogAction.StandardAction.FLUSH -> List.of(flush());
513                        };
514                };
515                return r;
516        }
517
518        default LogResponse reopen() {
519                var status = output().reopen();
520                return new Response(LogOutput.class, name(), status);
521        }
522
523        default LogResponse flush() {
524                output().flush();
525                return new Response(LogOutput.class, name(), LogResponse.Status.StandardStatus.OK);
526        }
527
528        static DirectLogAppender of(String name, LogOutput output, LogEncoder encoder,
529                        Set<LogAppender.AppenderFlag> flags) {
530                flags = AbstractLogAppender.guardSynchronizedFlag(flags);
531                if (flags.contains(AppenderFlag.REUSE_BUFFER)) {
532                        return new ReuseBufferLogAppender(name, output, encoder, flags, new ReentrantLock());
533                }
534                if (flags.contains(AppenderFlag.SYNCHRONIZED_THREAD_LOCAL_BUFFER)) {
535                        return new SynchronizedThreadLocalBufferLogAppender(name, output, encoder, flags);
536                }
537                if (flags.contains(AppenderFlag.LOCK_THREAD_LOCAL_BUFFER)) {
538                        return new LockThreadLocalBufferLogAppender(name, output, encoder, flags, new ReentrantLock());
539                }
540                return defaultAppender(name, output, encoder, flags);
541        }
542
543        /**
544         * Picks the appender used when no {@link AppenderFlag} explicitly requests a
545         * buffer/lock strategy: {@link AppenderFlag#LOCK_THREAD_LOCAL_BUFFER}, the same
546         * appender a caller gets from setting that flag explicitly. Supports
547         * {@link AppenderFlag#REENTRY_DROP}/{@link AppenderFlag#REENTRY_LOG} directly (see
548         * {@link AbstractLogAppender#shouldDropForReentry}), so no fallback to a third
549         * appender is needed here for those flags.
550         * <p>
551         * {@link AppenderFlag#SYNCHRONIZED_THREAD_LOCAL_BUFFER} measured faster under
552         * platform-thread contention in real-workload benchmarking and was the default for a
553         * time, but real-workload benchmarking under virtual threads found the opposite - a
554         * large, reproducible win for {@code LOCK_THREAD_LOCAL_BUFFER} there, for reasons not
555         * fully understood (checked and ruled out classic JEP 491 pinning as the cause).
556         * Given RainbowGum's own audience skews toward newer JDKs and virtual-thread
557         * workloads, {@code LOCK_THREAD_LOCAL_BUFFER} is the safer default;
558         * {@code synchronized} remains available as an explicit opt-in for
559         * platform-thread-heavy deployments that want that edge.
560         */
561        static DirectLogAppender defaultAppender(String name, LogOutput output, LogEncoder encoder,
562                        Set<LogAppender.AppenderFlag> flags) {
563                return new LockThreadLocalBufferLogAppender(name, output, encoder, flags, new ReentrantLock());
564        }
565
566        // @Override
567        DirectLogAppender withFlags(Set<LogAppender.AppenderFlag> flags);
568
569}
570
571/**
572 * An abstract appender to help create custom appenders.
573 */
574sealed abstract class AbstractLogAppender implements DirectLogAppender {
575
576        /*
577         * Set once from LogProperties#GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY during
578         * LogConfig construction (see DefaultLogConfig) - a global, process-wide guarantee
579         * that no appender will ever use `synchronized`, for deployments that want that
580         * guaranteed even when something explicitly requests
581         * SYNCHRONIZED_THREAD_LOCAL_BUFFER. Global (not per-route/per-appender) by design,
582         * matching the property's own scope.
583         */
584        static volatile boolean forceReentrantLockAppenders = false;
585
586        /**
587         * Downgrades an explicit
588         * {@link LogAppender.AppenderFlag#SYNCHRONIZED_THREAD_LOCAL_BUFFER} to
589         * {@link LogAppender.AppenderFlag#LOCK_THREAD_LOCAL_BUFFER} if
590         * {@link #forceReentrantLockAppenders} is active - the enforcement point that makes
591         * the global no-synchronized guarantee a real guarantee rather than just a changed
592         * default, since an explicit flag would otherwise bypass
593         * {@link DirectLogAppender#defaultAppender} entirely.
594         * @param flags flags as given to an appender factory method.
595         * @return {@code flags} unchanged, unless the guarantee is active and
596         * {@code SYNCHRONIZED_THREAD_LOCAL_BUFFER} was requested, in which case a copy with
597         * that flag replaced by {@code LOCK_THREAD_LOCAL_BUFFER}.
598         */
599        static Set<LogAppender.AppenderFlag> guardSynchronizedFlag(Set<LogAppender.AppenderFlag> flags) {
600                if (!forceReentrantLockAppenders
601                                || !flags.contains(LogAppender.AppenderFlag.SYNCHRONIZED_THREAD_LOCAL_BUFFER)) {
602                        return flags;
603                }
604                var copy = EnumSet.copyOf(flags);
605                copy.remove(LogAppender.AppenderFlag.SYNCHRONIZED_THREAD_LOCAL_BUFFER);
606                copy.add(LogAppender.AppenderFlag.LOCK_THREAD_LOCAL_BUFFER);
607                return copy;
608        }
609
610        /**
611         * Whether an appender should drop (or drop-and-log) an append call because it is
612         * reentrant - i.e. the current thread is already inside a previous call to the same
613         * appender's write path, which happens if an output does logging itself during its
614         * own write. Shared by every appender that can detect reentrancy, regardless of
615         * whether it does so via a {@link ReentrantLock} (
616         * {@code lock.isHeldByCurrentThread()}) or a {@code synchronized} block (
617         * {@link Thread#holdsLock(Object)}) - callers pass in whichever check applies to
618         * them.
619         * @param reentrant whether the current thread already holds this appender's
620         * lock/monitor.
621         * @param flags the appender's flags.
622         * @return {@code true} if the caller should drop the event without appending.
623         */
624        static boolean shouldDropForReentry(boolean reentrant, Set<LogAppender.AppenderFlag> flags) {
625                if (!reentrant) {
626                        return false;
627                }
628                if (flags.contains(LogAppender.AppenderFlag.REENTRY_LOG)) {
629                        Exception exception = new Exception("reentrant appender");
630                        MetaLog.error(LogAppender.class, exception);
631                        return true;
632                }
633                return flags.contains(LogAppender.AppenderFlag.REENTRY_DROP);
634        }
635
636        /**
637         * name.
638         */
639        protected final String name;
640
641        /**
642         * output
643         */
644        protected final LogOutput output;
645
646        /**
647         * encoder
648         */
649        protected final LogEncoder encoder;
650
651        protected final Set<LogAppender.AppenderFlag> flags;
652
653        protected final boolean immediateFlush;
654
655        /**
656         * Creates an appender from an output and encoder.
657         * @param output set the output field and will be started and closed with the
658         * appender.
659         * @param encoder set the encoder field.
660         */
661        protected AbstractLogAppender(String name, LogOutput output, LogEncoder encoder,
662                        Set<LogAppender.AppenderFlag> flags) {
663                super();
664                this.name = name;
665                this.output = output;
666                this.encoder = encoder;
667                this.flags = flags;
668                this.immediateFlush = !flags.contains(LogAppender.AppenderFlag.DISABLE_IMMEDIATE_FLUSH);
669        }
670
671        @Override
672        public void start(LogConfig config) {
673                output.start(config);
674        }
675
676        @Override
677        public void close() {
678                output.close();
679        }
680
681        @Override
682        public String toString() {
683                return getClass().getName() + "[name=" + name + " encoder=" + encoder + ", " + "output=" + output + ", flags="
684                                + flags + "]";
685        }
686
687        @Override
688        public String name() {
689                return this.name;
690        }
691
692        @Override
693        public LogOutput output() {
694                return this.output;
695        }
696
697        @Override
698        public LogEncoder encoder() {
699                return this.encoder;
700        }
701
702}
703
704/**
705 * Combines more than one appender on a route into one {@link LogAppender}. Each appender
706 * keeps the independent lock it was already constructed with, and
707 * {@link #append(LogEvent)}/{@link #append(LogEvent[], int)} skip locking at the
708 * composite level entirely and append to every component directly - so e.g. a console
709 * appender and a file appender under the same route never contend on the same lock for
710 * unrelated I/O. {@link #start(LogConfig)}/{@link #close()}/{@link #act(LogAction)} do
711 * the same - there is no composite-owned mutable state to protect, only a loop over
712 * components that already handle their own synchronization where it matters.
713 */
714@SuppressWarnings("ArrayRecordComponent")
715record CompositeLogAppender(DirectLogAppender[] appenders) implements InternalLogAppender {
716
717        public static CompositeLogAppender of(List<? extends LogAppender> appenders, Set<LogAppender.AppenderFlag> flags) {
718                @SuppressWarnings("null") // TODO Eclipse issue here
719                DirectLogAppender @NonNull [] array = appenders.stream()
720                        .map(CompositeLogAppender::cast)
721                        .map(a -> a.withFlags(flags))
722                        .toArray(i -> new DirectLogAppender[i]);
723                return new CompositeLogAppender(array);
724        }
725
726        private static DirectLogAppender cast(LogAppender appender) {
727                return (DirectLogAppender) appender;
728        }
729
730        @Override
731        public void append(LogEvent event) {
732                for (var appender : appenders) {
733                        appender.append(event);
734                }
735        }
736
737        @Override
738        public void append(LogEvent[] event, int count) {
739                for (var appender : appenders) {
740                        appender.append(event, count);
741                }
742        }
743
744        @Override
745        public void close() {
746                for (var appender : appenders) {
747                        appender.close();
748                }
749        }
750
751        @Override
752        public void start(LogConfig config) {
753                for (var appender : appenders) {
754                        appender.start(config);
755                }
756        }
757
758        @Override
759        public List<LogResponse> act(LogAction action) {
760                return Actor.act(appenders, action);
761        }
762
763        public CompositeLogAppender withFlags(Set<LogAppender.AppenderFlag> flags) {
764                if (flags.isEmpty()) {
765                        return this;
766                }
767                return of(List.of(appenders), flags);
768        }
769
770        @Override
771        public String toString() {
772                return getClass().getName() + "[appenders=" + Arrays.toString(appenders) + "]";
773        }
774
775}
776
777sealed abstract class LockLogAppender extends AbstractLogAppender implements InternalLogAppender {
778
779        protected final ReentrantLock lock;
780
781        public LockLogAppender(String name, LogOutput output, LogEncoder encoder, Set<LogAppender.AppenderFlag> flags,
782                        ReentrantLock lock) {
783                super(name, output, encoder, flags);
784                this.lock = lock;
785        }
786
787        @Override
788        public List<LogResponse> act(LogAction action) {
789                lock.lock();
790                try {
791                        return _request(action);
792                }
793                catch (UncheckedIOException ioe) {
794                        return List.of(new Response(LogOutput.class, name, Status.ErrorStatus.of(ioe)));
795                }
796                finally {
797                        lock.unlock();
798                }
799        }
800
801        @Override
802        public void close() {
803                lock.lock();
804                try {
805                        super.close();
806                }
807                finally {
808                        lock.unlock();
809                }
810        }
811
812        @Override
813        public DirectLogAppender withFlags(Set<LogAppender.AppenderFlag> flags) {
814                if (flags.isEmpty()) {
815                        return this;
816                }
817                if (this.flags.containsAll(flags)) {
818                        return this;
819                }
820                flags = EnumSet.copyOf(flags);
821                flags.addAll(this.flags);
822                flags = guardSynchronizedFlag(flags);
823                if (flags.contains(LogAppender.AppenderFlag.REUSE_BUFFER)) {
824                        return new ReuseBufferLogAppender(name, output, encoder, flags, lock);
825                }
826                if (flags.contains(LogAppender.AppenderFlag.SYNCHRONIZED_THREAD_LOCAL_BUFFER)) {
827                        return new SynchronizedThreadLocalBufferLogAppender(name, output, encoder, flags);
828                }
829                if (flags.contains(LogAppender.AppenderFlag.LOCK_THREAD_LOCAL_BUFFER)) {
830                        return new LockThreadLocalBufferLogAppender(name, output, encoder, flags, lock);
831                }
832                return DirectLogAppender.defaultAppender(name, output, encoder, flags);
833        }
834
835}
836
837/*
838 * The idea here is to reuse the buffer trading lock contention for less GC.
839 */
840final class ReuseBufferLogAppender extends LockLogAppender implements InternalLogAppender {
841
842        private final LogEncoder.Buffer buffer;
843
844        ReuseBufferLogAppender(String name, LogOutput output, LogEncoder encoder, Set<LogAppender.AppenderFlag> flags,
845                        ReentrantLock lock) {
846                super(name, output, encoder, flags, lock);
847                this.buffer = encoder.buffer(output.bufferHints());
848        }
849
850        @Override
851        public final void append(LogEvent event) {
852                if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags)) {
853                        return;
854                }
855                lock.lock();
856                try {
857                        buffer.clear();
858                        encoder.encode(event, buffer);
859                        output.write(event, buffer);
860                        if (immediateFlush) {
861                                output.flush();
862                        }
863                }
864                finally {
865                        lock.unlock();
866                }
867        }
868
869        @Override
870        public void append(LogEvent[] events, int count) {
871                if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags)) {
872                        return;
873                }
874                lock.lock();
875                try {
876                        output.write(events, count, encoder, buffer);
877                        if (immediateFlush) {
878                                output.flush();
879                        }
880                }
881                finally {
882                        lock.unlock();
883                }
884        }
885
886        @Override
887        public void close() {
888                lock.lock();
889                try {
890                        super.close();
891                        buffer.close();
892                }
893                finally {
894                        lock.unlock();
895                }
896        }
897
898}
899
900/*
901 * The idea here is to encode outside the lock. Instead of allocating a fresh buffer per
902 * event or sharing (and thus serializing access to) a single buffer
903 * (ReuseBufferLogAppender), each thread gets its own buffer that only it will ever touch,
904 * so encoding never needs to be guarded by the lock at all - only the final write to the
905 * output does.
906 */
907final class LockThreadLocalBufferLogAppender extends LockLogAppender implements InternalLogAppender {
908
909        /*
910         * There is no way to enumerate every thread's buffer to close it on appender close so
911         * we rely on Buffer implementations not holding onto real resources (today they are
912         * all just wrapped in-memory builders) and let the ThreadLocal itself (and
913         * consequently the per-thread entries) become collectible once this appender is
914         * discarded.
915         */
916        // CheckerFramework's ThreadLocal stub declares T as inherently @Nullable since get()
917        // can return null before initialValue() runs, but withInitial(...) below guarantees
918        // it never does here.
919        @SuppressWarnings("nullness:type.argument")
920        private final ThreadLocal<LogEncoder.Buffer> bufferThreadLocal;
921
922        LockThreadLocalBufferLogAppender(String name, LogOutput output, LogEncoder encoder,
923                        Set<LogAppender.AppenderFlag> flags, ReentrantLock lock) {
924                super(name, output, encoder, flags, lock);
925                this.bufferThreadLocal = ThreadLocal.withInitial(() -> encoder.buffer(output.bufferHints()));
926        }
927
928        @Override
929        public final void append(LogEvent event) {
930                var buffer = bufferThreadLocal.get();
931                buffer.clear();
932                encoder.encode(event, buffer);
933                writeLocked(event, buffer);
934        }
935
936        private void writeLocked(LogEvent event, LogEncoder.Buffer buffer) {
937                if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags)) {
938                        return;
939                }
940                lock.lock();
941                try {
942                        output.write(event, buffer);
943                        if (immediateFlush) {
944                                output.flush();
945                        }
946                }
947                finally {
948                        lock.unlock();
949                }
950        }
951
952        @Override
953        public void append(LogEvent[] events, int count) {
954                if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags)) {
955                        return;
956                }
957                lock.lock();
958                try {
959                        output.write(events, count, encoder, bufferThreadLocal.get());
960                        if (immediateFlush) {
961                                output.flush();
962                        }
963                }
964                finally {
965                        lock.unlock();
966                }
967        }
968
969}
970
971/*
972 * Like LockThreadLocalBufferLogAppender (per-thread reused buffer, encode outside any
973 * lock) but the final write is protected by a plain `synchronized` block on this
974 * appender's own monitor instead of a ReentrantLock. Does not extend LockLogAppender -
975 * there is no way to acquire a monitor in one method call and release it in another, so
976 * this appender's critical sections are written as literal synchronized blocks instead.
977 */
978final class SynchronizedThreadLocalBufferLogAppender extends AbstractLogAppender implements InternalLogAppender {
979
980        private final Object monitor = new Object();
981
982        // See LockThreadLocalBufferLogAppender's identical field for why this suppression is
983        // needed.
984        @SuppressWarnings("nullness:type.argument")
985        private final ThreadLocal<LogEncoder.Buffer> bufferThreadLocal;
986
987        SynchronizedThreadLocalBufferLogAppender(String name, LogOutput output, LogEncoder encoder,
988                        Set<LogAppender.AppenderFlag> flags) {
989                super(name, output, encoder, flags);
990                this.bufferThreadLocal = ThreadLocal.withInitial(() -> encoder.buffer(output.bufferHints()));
991        }
992
993        @Override
994        public void append(LogEvent event) {
995                var buffer = bufferThreadLocal.get();
996                buffer.clear();
997                encoder.encode(event, buffer);
998                writeSynchronized(event, buffer);
999        }
1000
1001        private void writeSynchronized(LogEvent event, LogEncoder.Buffer buffer) {
1002                if (shouldDropForReentry(Thread.holdsLock(monitor), flags)) {
1003                        return;
1004                }
1005                synchronized (monitor) {
1006                        output.write(event, buffer);
1007                        if (immediateFlush) {
1008                                output.flush();
1009                        }
1010                }
1011        }
1012
1013        @Override
1014        public void append(LogEvent[] events, int count) {
1015                if (shouldDropForReentry(Thread.holdsLock(monitor), flags)) {
1016                        return;
1017                }
1018                synchronized (monitor) {
1019                        output.write(events, count, encoder, bufferThreadLocal.get());
1020                        if (immediateFlush) {
1021                                output.flush();
1022                        }
1023                }
1024        }
1025
1026        @Override
1027        public void close() {
1028                synchronized (monitor) {
1029                        super.close();
1030                }
1031        }
1032
1033        @Override
1034        public List<LogResponse> act(LogAction action) {
1035                synchronized (monitor) {
1036                        try {
1037                                return _request(action);
1038                        }
1039                        catch (UncheckedIOException ioe) {
1040                                return List.of(new Response(LogOutput.class, name, Status.ErrorStatus.of(ioe)));
1041                        }
1042                }
1043        }
1044
1045        @Override
1046        public DirectLogAppender withFlags(Set<LogAppender.AppenderFlag> flags) {
1047                if (flags.isEmpty()) {
1048                        return this;
1049                }
1050                if (this.flags.containsAll(flags)) {
1051                        return this;
1052                }
1053                flags = EnumSet.copyOf(flags);
1054                flags.addAll(this.flags);
1055                flags = guardSynchronizedFlag(flags);
1056                if (flags.contains(LogAppender.AppenderFlag.REUSE_BUFFER)) {
1057                        return new ReuseBufferLogAppender(name, output, encoder, flags, new ReentrantLock());
1058                }
1059                if (flags.contains(LogAppender.AppenderFlag.SYNCHRONIZED_THREAD_LOCAL_BUFFER)) {
1060                        return new SynchronizedThreadLocalBufferLogAppender(name, output, encoder, flags);
1061                }
1062                if (flags.contains(LogAppender.AppenderFlag.LOCK_THREAD_LOCAL_BUFFER)) {
1063                        return new LockThreadLocalBufferLogAppender(name, output, encoder, flags, new ReentrantLock());
1064                }
1065                return DirectLogAppender.defaultAppender(name, output, encoder, flags);
1066        }
1067
1068}