001package io.jstach.rainbowgum;
002
003import java.io.PrintStream;
004import java.lang.System.Logger.Level;
005import java.time.Instant;
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.List;
009import java.util.Locale;
010import java.util.Objects;
011import java.util.concurrent.CopyOnWriteArrayList;
012import java.util.concurrent.atomic.AtomicLong;
013import java.util.concurrent.locks.ReentrantLock;
014import java.util.function.Supplier;
015
016import org.jspecify.annotations.Nullable;
017
018import io.jstach.rainbowgum.annotation.EnumAlias;
019
020/**
021 * Alerts (sometimes called errors or status events elsewhere) are for reporting problems
022 * with the logging system itself rather than application logging - for example an
023 * appender failing to write, or an async publisher's queue overflowing. Because logging
024 * itself may be broken these are not routed through the normal {@link LogRouter} but are
025 * instead kept in a small in memory ring buffer (see {@link #dump()} and
026 * {@link #stats()}). Warning and error alerts are also reported immediately (currently to
027 * stderr); informational alerts remain available for diagnostics without producing output
028 * by default.
029 * <p>
030 * An instance is available from every {@link LogConfig#alerts()}. Components that are
031 * {@linkplain LogProvider provided} config, or that are
032 * {@linkplain LogLifecycle#start( LogConfig) started} with config, should prefer
033 * capturing {@code config.alerts()} over reaching for global state.
034 * <p>
035 * {@link LogAlerts} is itself a {@link LogLifecycle}: {@link #start(LogConfig)} is called
036 * exactly once, after every
037 * {@link io.jstach.rainbowgum.spi.RainbowGumServiceProvider.Configurator} has run, and is
038 * where {@link UnobservedErrorsAction} (already resolved at construction, same as
039 * capacity) gets applied against whatever has been recorded by then - see that enum for
040 * what "nobody is listening yet" means and why it matters specifically at that moment
041 * rather than for the life of the process.
042 *
043 * @see LogConfig#alerts()
044 */
045public sealed interface LogAlerts extends LogLifecycle permits DefaultLogAlerts, SwappableLogAlerts {
046
047        /**
048         * Default capacity of the alert ring buffer.
049         */
050        static final int DEFAULT_CAPACITY = 512;
051
052        /**
053         * Records an alert.
054         * @param event event describing the alert.
055         */
056        public void alert(LogEvent event);
057
058        /**
059         * Records an alert.
060         * @param loggerName usually the class where the alert originated.
061         * @param throwable cause of the alert.
062         */
063        default void error(Class<?> loggerName, Throwable throwable) {
064                String m = Objects.requireNonNullElse(throwable.getMessage(), "exception");
065                error(loggerName, m, throwable);
066        }
067
068        /**
069         * Records an alert.
070         * @param loggerName usually the class where the alert originated.
071         * @param message alert message.
072         * @param throwable cause of the alert.
073         */
074        default void error(Class<?> loggerName, String message, Throwable throwable) {
075                alert(event(loggerName, Level.ERROR, message, throwable));
076        }
077
078        /**
079         * Records a warning alert.
080         * @param loggerName usually the class where the alert originated.
081         * @param message alert message.
082         */
083        default void warn(Class<?> loggerName, String message) {
084                alert(event(loggerName, Level.WARNING, message, null));
085        }
086
087        /**
088         * Records an informational alert.
089         * @param loggerName usually the class where the alert originated.
090         * @param message alert message.
091         */
092        default void info(Class<?> loggerName, String message) {
093                alert(event(loggerName, Level.INFO, message, null));
094        }
095
096        private static LogEvent event(Class<?> loggerName, Level level, String message, @Nullable Throwable throwable) {
097                var currentThread = Thread.currentThread();
098                return LogEvent.of(Instant.now(), currentThread.getName(), currentThread.threadId(), level,
099                                loggerName.getName(), message, KeyValues.of(), throwable);
100        }
101
102        /**
103         * A snapshot of the alerts currently held in the ring buffer, oldest first. Alerts
104         * are evicted oldest first once the buffer is at {@link Stats#capacity()}.
105         * @return immutable snapshot.
106         */
107        public List<LogEvent> dump();
108
109        /**
110         * Clears the ring buffer. Does not reset {@link Stats#total()} - like a
111         * Prometheus/Micrometer counter, that is meant to be monotonically increasing for the
112         * life of the process; a downstream metrics system computes rate of change rather
113         * than relying on the counter itself being reset. Clearing diagnostics does not undo
114         * an alert that fails startup under {@link FailLevel}.
115         */
116        public void clear();
117
118        /**
119         * Stats about the alerts recorded so far.
120         * @return stats.
121         */
122        public Stats stats();
123
124        /**
125         * Registers a listener that is notified synchronously, in addition to the alert being
126         * recorded in the ring buffer, every time {@link #alert(LogEvent)} is called.
127         * <p>
128         * <strong>Listeners are held with a normal (strong) reference and are not
129         * automatically removed.</strong> This is deliberate: alert listeners are expected to
130         * be few and long lived - typically registered once (e.g. a metrics bridge or an
131         * ops/paging integration) for as long as the owning {@link LogConfig} is - rather
132         * than one per short lived object. A weak reference would risk silently dropping a
133         * listener (a lambda with no other strong reference could be collected almost
134         * immediately) which is the wrong failure mode for something whose entire job is not
135         * losing alerts. Call {@link AutoCloseable#close()} on the returned registration to
136         * unregister deterministically once the caller's own lifecycle ends.
137         * @param listener listener to register.
138         * @return registration; {@link AutoCloseable#close()} unregisters the listener.
139         */
140        public AutoCloseable addListener(Listener listener);
141
142        /**
143         * Notified of alerts as they happen. See {@link #addListener(Listener)}.
144         */
145        @FunctionalInterface
146        interface Listener {
147
148                /**
149                 * Called synchronously every time an alert is recorded. Should not throw -
150                 * exceptions are caught and reported separately so a broken listener cannot
151                 * disrupt alert recording or other listeners.
152                 * @param event the alert.
153                 */
154                void onAlert(LogEvent event);
155
156        }
157
158        /**
159         * Stats about alerts recorded.
160         *
161         * @param total total number of alerts ever recorded, including ones since evicted
162         * from the ring buffer.
163         * @param size number of alerts currently held in the ring buffer.
164         * @param capacity maximum number of alerts the ring buffer holds.
165         */
166        record Stats(long total, int size, int capacity) {
167        }
168
169        /**
170         * What building LogConfig and starting Rainbow Gum do with alerts recorded while
171         * starting, set by {@value LogProperties#ALERTS_FAIL_PROPERTY}. Checked once when
172         * {@link LogConfig} is built and once right after Rainbow Gum starts; alerts after
173         * that never fail anything. Evicting or clearing an alert from the diagnostic queue
174         * does not prevent it from failing startup.
175         */
176        enum FailLevel {
177
178                /**
179                 * Default. Alerts never fail startup.
180                 */
181                @EnumAlias("false")
182                OFF,
183                /**
184                 * Fail if an error alert is recorded while starting.
185                 */
186                ERROR,
187                /**
188                 * Fail if a warning or error alert is recorded while starting.
189                 */
190                @EnumAlias({ "true", "warn" })
191                WARNING;
192
193                static FailLevel parse(String value) {
194                        return switch (value.toLowerCase(java.util.Locale.ROOT)) {
195                                case "true", "warn" -> WARNING;
196                                case "false" -> OFF;
197                                default -> LogProperty.enumValue(FailLevel.class, value, "true", "false", "warn");
198                        };
199                }
200
201                boolean fails(Level level) {
202                        return switch (this) {
203                                case OFF -> false;
204                                case ERROR -> level == Level.ERROR;
205                                case WARNING -> level == Level.ERROR || level == Level.WARNING;
206                        };
207                }
208
209        }
210
211        /**
212         * The alert level for a property key that was set but not read while starting, set by
213         * {@value LogProperties#ALERTS_UNUSED_KEY_PROPERTY}. Keys are only checked in the
214         * debug modes that report them and when {@value LogProperties#ALERTS_FAIL_PROPERTY}
215         * would fail on them.
216         */
217        enum UnusedKeyLevel {
218
219                /**
220                 * Default. A key close to a key that was read is a warning with a suggestion, as
221                 * is a URI query parameter the component its URI chose does not read. Any other
222                 * unread key is information, since it may be for a component type that was not
223                 * chosen.
224                 */
225                INFO,
226                /**
227                 * Every unread key is a warning, so {@code logging.alerts.fail=warning} fails on
228                 * any of them.
229                 */
230                @EnumAlias("warn")
231                WARNING,
232                /**
233                 * Every unread key is an error, so {@code logging.alerts.fail=error} fails on any
234                 * of them.
235                 */
236                ERROR;
237
238                static UnusedKeyLevel parse(String value) {
239                        return switch (value.toLowerCase(java.util.Locale.ROOT)) {
240                                case "warn" -> WARNING;
241                                default -> LogProperty.enumValue(UnusedKeyLevel.class, value, "warn");
242                        };
243                }
244
245                Level level(boolean likelyMistake) {
246                        return switch (this) {
247                                case INFO -> likelyMistake ? Level.WARNING : Level.INFO;
248                                case WARNING -> Level.WARNING;
249                                case ERROR -> Level.ERROR;
250                        };
251                }
252
253        }
254
255        /**
256         * What {@link #start(LogConfig)} does if, at that moment, at least one error alert
257         * has been recorded and still zero {@link Listener}s are registered - the situation
258         * Logback's {@code StatusManager} calls an unobserved status: nothing is watching, so
259         * whatever went wrong during property loading or a
260         * {@link io.jstach.rainbowgum.spi.RainbowGumServiceProvider.Configurator} would
261         * otherwise only ever have reached the individual, easy to miss stderr lines each
262         * {@link #alert(LogEvent)} call already produces.
263         * <p>
264         * Deliberately keyed as
265         * {@value LogProperties#ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY} (an enum, not a
266         * boolean) since "fail" without "dump" first would throw away the only diagnostic
267         * evidence of why it failed.
268         */
269        enum UnobservedErrorsAction {
270
271                /**
272                 * Do nothing beyond the immediate reporting of warning and error events.
273                 */
274                NONE,
275                /**
276                 * Default. Report the whole backlog to {@code MetaLog} as one clearly labeled
277                 * block, the same way Logback auto-installs a console listener and prints its
278                 * accumulated status if nothing else is watching by the end of configuration -
279                 * RainbowGum still starts.
280                 */
281                DUMP,
282                /**
283                 * Same reporting as {@link #DUMP}, but then throws, so
284                 * {@link LogConfig.Builder#build()} (and therefore whatever is building a
285                 * {@link RainbowGum} from it) never completes. Goes further than Logback ever
286                 * does - an explicit, opt-in choice of integrity over resilience for deployments
287                 * where a silently-broken bootstrap is worse than refusing to start.
288                 */
289                FAIL;
290
291                static UnobservedErrorsAction parse(String value) {
292                        return LogProperty.enumValue(UnobservedErrorsAction.class, value);
293                }
294
295        }
296
297}
298
299/*
300 * Handed to PropertiesProviders. Buffers alerts until the configured LogAlerts exists,
301 * then drainTo replays the buffer into it and forwards every later call, so a provider
302 * may keep this instance and alert after provideProperties returns. Replay and swap
303 * happen under one lock so a concurrent alert is neither lost nor reordered. Listener
304 * registration is never supported: providers record alerts but cannot observe them.
305 */
306final class SwappableLogAlerts implements LogAlerts {
307
308        private final List<LogEvent> events = new ArrayList<>();
309
310        private long total;
311
312        private volatile @Nullable LogAlerts delegate;
313
314        @Override
315        public void alert(LogEvent event) {
316                var d = delegate;
317                if (d == null) {
318                        synchronized (this) {
319                                d = delegate;
320                                if (d == null) {
321                                        events.add(event.freeze());
322                                        total++;
323                                        return;
324                                }
325                        }
326                }
327                d.alert(event);
328        }
329
330        synchronized void drainTo(LogAlerts target) {
331                if (delegate != null) {
332                        throw new IllegalStateException("Alerts have already been drained");
333                }
334                for (var event : events) {
335                        target.alert(event);
336                }
337                events.clear();
338                delegate = target;
339        }
340
341        @Override
342        public synchronized List<LogEvent> dump() {
343                var d = delegate;
344                return d != null ? d.dump() : List.copyOf(events);
345        }
346
347        @Override
348        public synchronized void clear() {
349                var d = delegate;
350                if (d != null) {
351                        d.clear();
352                }
353                else {
354                        events.clear();
355                }
356        }
357
358        @Override
359        public synchronized Stats stats() {
360                var d = delegate;
361                return d != null ? d.stats() : new Stats(total, events.size(), Integer.MAX_VALUE);
362        }
363
364        @Override
365        public AutoCloseable addListener(Listener listener) {
366                throw new UnsupportedOperationException("Properties providers cannot register alert listeners");
367        }
368
369        @Override
370        public void start(LogConfig config) {
371                throw new UnsupportedOperationException("Properties provider alerts cannot be started");
372        }
373
374        @Override
375        public synchronized void close() {
376                // The drained delegate belongs to LogConfig, so only an undrained buffer is
377                // dropped.
378                if (delegate == null) {
379                        events.clear();
380                }
381        }
382
383}
384
385final class DefaultLogAlerts implements LogAlerts {
386
387        private final LogEvent[] ring;
388
389        private int start = 0;
390
391        private int size = 0;
392
393        private final AtomicLong total = new AtomicLong();
394
395        private final AtomicLong errors = new AtomicLong();
396
397        private final ReentrantLock lock = new ReentrantLock();
398
399        private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();
400
401        /*
402         * Kept apart from listeners.isEmpty() deliberately: DefaultLogConfig's own
403         * metrics-bridge listener (see its constructor) is always registered before
404         * start(...) ever runs, and a passive in-process counter nobody has wired an exporter
405         * to is not "someone is watching" in the sense UnobservedErrorsAction cares about -
406         * only a real addListener(Listener) call (this class's public API) flips this.
407         */
408        private volatile boolean hasExternalListener = false;
409
410        private final LogEventFactory eventFactory = LogEventFactory.of(DefaultLogAlerts.class.getName());
411
412        /*
413         * Resolved once, at construction (same as capacity) rather than inside start(...):
414         * unlike capacity this does not shape any constructor-time state, but LogProperties
415         * is available just as early, and start(LogConfig) is really about announcing "I have
416         * started" (config is there because a few edge cases needed it, not because this
417         * needed to be a start-time lookup).
418         */
419        private final UnobservedErrorsAction unobservedErrorsAction;
420
421        /*
422         * Private and deliberately not defensive - of(LogProperties) below is the only
423         * caller, and it has already validated both fields (capacity > 0, a resolvable
424         * UnobservedErrorsAction) before ever reaching this constructor. Re-checking here
425         * would be defensive programming duplicating what was already a real, reported
426         * validation failure one call up - see of(...)'s own comment.
427         */
428        private final FailLevel failLevel;
429
430        /*
431         * Names what set the fail level, for the failure message: the property, or
432         * logging.debug=help raising it.
433         */
434        private final String failLevelSource;
435
436        private final UnusedKeyLevel unusedKeyLevel;
437
438        /*
439         * Startup failure is independent of the bounded diagnostic queue. Keep the first
440         * failure and the count until startup ends, even if clear() or eviction removes it
441         * from the queue. All three fields are protected by lock.
442         */
443        private long startupFailures;
444
445        private @Nullable LogEvent firstStartupFailure;
446
447        private boolean startupComplete;
448
449        private DefaultLogAlerts(int capacity, UnobservedErrorsAction unobservedErrorsAction, FailLevel failLevel,
450                        String failLevelSource, UnusedKeyLevel unusedKeyLevel) {
451                this.ring = new LogEvent[capacity];
452                this.unobservedErrorsAction = unobservedErrorsAction;
453                this.failLevel = failLevel;
454                this.failLevelSource = failLevelSource;
455                this.unusedKeyLevel = unusedKeyLevel;
456        }
457
458        FailLevel failLevel() {
459                return failLevel;
460        }
461
462        UnusedKeyLevel unusedKeyLevel() {
463                return unusedKeyLevel;
464        }
465
466        /*
467         * Checks the startup failure count and lists retained failing alerts, including the
468         * first failure even if it left the queue. Called when LogConfig is built and right
469         * after Rainbow Gum starts.
470         */
471        void failIfAlerted(String phase) {
472                checkStartup(phase, false);
473        }
474
475        void finishStartup() {
476                checkStartup("starting", true);
477        }
478
479        private void checkStartup(String phase, boolean complete) {
480                lock.lock();
481                try {
482                        if (startupComplete) {
483                                return;
484                        }
485                        startupComplete = complete;
486                        var first = firstStartupFailure;
487                        if (first == null) {
488                                return;
489                        }
490                        var failing = new ArrayList<>(dump().stream().filter(e -> failLevel.fails(e.level())).toList());
491                        if (!failing.contains(first)) {
492                                failing.addFirst(first);
493                        }
494                        var sb = new StringBuilder();
495                        sb.append(startupFailures)
496                                .append(" alert(s) at ")
497                                .append(failLevel.name().toLowerCase(Locale.ROOT))
498                                .append(" or above were recorded while ")
499                                .append(phase)
500                                .append(" and ")
501                                .append(failLevelSource)
502                                .append(":");
503                        for (var e : failing) {
504                                sb.append("\n[").append(e.level()).append("] ");
505                                e.formattedMessage(sb);
506                        }
507                        if (startupFailures > failing.size()) {
508                                sb.append("\n")
509                                        .append(startupFailures - failing.size())
510                                        .append(" additional failing alert(s) are no longer retained.");
511                        }
512                        throw new IllegalStateException(sb.toString());
513                }
514                finally {
515                        if (complete) {
516                                firstStartupFailure = null;
517                        }
518                        lock.unlock();
519                }
520        }
521
522        /*
523         * The fail level as resolved from properties, before LogAlerts exists, so LogConfig
524         * can decide whether to record property reads. Resolved again, and validated, by
525         * of(...).
526         */
527        static FailLevel failLevelOr(LogProperties properties, FailLevel fallback) {
528                var value = properties.valueOrNull(LogProperties.ALERTS_FAIL_PROPERTY);
529                try {
530                        return value == null ? fallback : FailLevel.parse(value);
531                }
532                catch (RuntimeException e) {
533                        return fallback;
534                }
535        }
536
537        /*
538         * The unused key level as resolved from properties, before LogAlerts exists, like
539         * failLevelOr. An invalid value is reported by of(...).
540         */
541        static UnusedKeyLevel unusedKeyLevelOr(LogProperties properties, UnusedKeyLevel fallback) {
542                var value = properties.valueOrNull(LogProperties.ALERTS_UNUSED_KEY_PROPERTY);
543                try {
544                        return value == null ? fallback : UnusedKeyLevel.parse(value);
545                }
546                catch (RuntimeException e) {
547                        return fallback;
548                }
549        }
550
551        /*
552         * Package-friend static factory: resolves and validates both
553         * LogProperties#ALERTS_CAPACITY_PROPERTY and
554         * LogProperties#ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY against one shared Validator
555         * - a mistake in both at once is reported together instead of only the first one
556         * found - then constructs. requirePositiveCapacity is a plain int -> int validating
557         * transform (not an object construction) specifically so it can sit inside map(...)
558         * without tripping CheckerFramework's Initialization Checker, which does not refine a
559         * `new Foo(...)` expression back to @Initialized when it appears directly inside a
560         * lambda body passed through a generic method like map(...) - the one and only `new
561         * DefaultLogAlerts(...)` call stays a plain, unconditional statement here instead.
562         */
563        static DefaultLogAlerts of(LogProperties properties, FailLevel failLevelFallback,
564                        UnusedKeyLevel unusedKeyLevelFallback, boolean help) {
565                var validator = LogProperty.Validator.of(LogAlerts.class);
566                var failLevelResult = properties.forKey(LogProperties.ALERTS_FAIL_PROPERTY)
567                        .ofString()
568                        .map(FailLevel::parse)
569                        .or(failLevelFallback)
570                        .validateIfError(validator);
571                var unobservedErrorsActionResult = properties.forKey(LogProperties.ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY)
572                        .ofString()
573                        .map(UnobservedErrorsAction::parse)
574                        .or(UnobservedErrorsAction.DUMP)
575                        .validateIfError(validator);
576                var unusedKeyLevelResult = properties.forKey(LogProperties.ALERTS_UNUSED_KEY_PROPERTY)
577                        .ofString()
578                        .map(UnusedKeyLevel::parse)
579                        .or(unusedKeyLevelFallback)
580                        .validateIfError(validator);
581                var capacityResult = properties.forKey(LogProperties.ALERTS_CAPACITY_PROPERTY)
582                        .ofInt()
583                        .or(LogAlerts.DEFAULT_CAPACITY)
584                        .map(DefaultLogAlerts::requirePositiveCapacity)
585                        .validateIfError(validator);
586                validator.validate();
587                var failLevel = failLevelResult.value();
588                String failLevelSource = LogProperties.ALERTS_FAIL_PROPERTY + "=" + failLevel.name().toLowerCase(Locale.ROOT);
589                if (help && failLevel != FailLevel.WARNING) {
590                        failLevel = FailLevel.WARNING;
591                        failLevelSource = "logging.debug=help";
592                }
593                return new DefaultLogAlerts(capacityResult.value(), unobservedErrorsActionResult.value(), failLevel,
594                                failLevelSource, unusedKeyLevelResult.value());
595        }
596
597        private static int requirePositiveCapacity(int capacity) {
598                if (capacity <= 0) {
599                        throw new IllegalArgumentException("capacity should be greater than 0");
600                }
601                return capacity;
602        }
603
604        @Override
605        public void alert(LogEvent event) {
606                var frozen = event.freeze();
607                total.incrementAndGet();
608                if (frozen.level() == Level.ERROR) {
609                        errors.incrementAndGet();
610                }
611                lock.lock();
612                try {
613                        if (!startupComplete && failLevel.fails(frozen.level())) {
614                                startupFailures++;
615                                if (firstStartupFailure == null) {
616                                        firstStartupFailure = frozen;
617                                }
618                        }
619                        if (size < ring.length) {
620                                ring[(start + size) % ring.length] = frozen;
621                                size++;
622                        }
623                        else {
624                                ring[start] = frozen;
625                                start = (start + 1) % ring.length;
626                        }
627                }
628                finally {
629                        lock.unlock();
630                }
631                for (var listener : listeners) {
632                        try {
633                                listener.onAlert(frozen);
634                        }
635                        catch (Exception e) {
636                                MetaLog.INSTANCE.log(eventFactory.eventNoArg(Level.ERROR, "LogAlerts.Listener threw", e));
637                        }
638                }
639                if (frozen.level() == Level.WARNING || frozen.level() == Level.ERROR) {
640                        MetaLog.INSTANCE.log(frozen);
641                }
642        }
643
644        @Override
645        public AutoCloseable addListener(Listener listener) {
646                listeners.add(listener);
647                hasExternalListener = true;
648                return () -> listeners.remove(listener);
649        }
650
651        /*
652         * DefaultLogConfig's own metrics-bridge listener goes through this instead of
653         * addListener(Listener) - see hasExternalListener's own comment for why.
654         */
655        void addInternalListener(Listener listener) {
656                listeners.add(listener);
657        }
658
659        @Override
660        public void start(LogConfig config) {
661                if (unobservedErrorsAction == UnobservedErrorsAction.NONE || hasExternalListener || errors.get() == 0) {
662                        return;
663                }
664                var backlog = dump();
665                MetaLog.error(eventFactory.eventNoArg(Level.ERROR,
666                                backlog.size() + " alert(s) were recorded before any LogAlerts.Listener was registered - "
667                                                + "dumping the backlog now since nothing else will see it:",
668                                null));
669                for (var event : backlog) {
670                        MetaLog.error(event);
671                }
672                if (unobservedErrorsAction == UnobservedErrorsAction.FAIL) {
673                        throw new IllegalStateException(
674                                        backlog.size() + " alert(s) were recorded before any LogAlerts.Listener was registered and "
675                                                        + LogProperties.ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY + "=FAIL - refusing to start.");
676                }
677        }
678
679        @Override
680        public void close() {
681                listeners.clear();
682                lock.lock();
683                try {
684                        startupComplete = true;
685                        firstStartupFailure = null;
686                }
687                finally {
688                        lock.unlock();
689                }
690        }
691
692        @Override
693        public List<LogEvent> dump() {
694                lock.lock();
695                try {
696                        List<LogEvent> list = new ArrayList<>(size);
697                        for (int i = 0; i < size; i++) {
698                                var e = ring[(start + i) % ring.length];
699                                if (e != null) {
700                                        list.add(e);
701                                }
702                        }
703                        return List.copyOf(list);
704                }
705                finally {
706                        lock.unlock();
707                }
708        }
709
710        @Override
711        public void clear() {
712                lock.lock();
713                try {
714                        Arrays.fill(ring, null);
715                        start = 0;
716                        size = 0;
717                }
718                finally {
719                        lock.unlock();
720                }
721        }
722
723        @Override
724        public Stats stats() {
725                lock.lock();
726                try {
727                        return new Stats(total.get(), size, ring.length);
728                }
729                finally {
730                        lock.unlock();
731                }
732        }
733
734}
735
736/**
737 * Logging about logging. This is the static, always-available entry point used by code
738 * that cannot easily reach a {@link LogConfig} (e.g. before a {@link RainbowGum} is
739 * bound), and always reports directly (currently to stderr) rather than forwarding to a
740 * bound {@link RainbowGum}'s {@link LogConfig#alerts()} - alerts is a small bounded ring
741 * buffer meant to hold a curated, recent view of noteworthy logging-system problems, and
742 * {@link MetaLog} is used from enough different low level failure paths that routing it
743 * there risked flooding/evicting genuine alerts with whatever volume of things end up
744 * calling this class.
745 * <p>
746 * Components that already have (or can easily capture) a {@link LogConfig} - for example
747 * via {@link LogProvider} or {@link LogLifecycle#start(LogConfig)} - should prefer
748 * {@link LogConfig#alerts()} directly instead of this class.
749 *
750 * @author agentgt
751 */
752@SuppressWarnings("ImmutableEnumChecker")
753enum MetaLog implements LogEventLogger {
754
755        INSTANCE;
756
757        static Supplier<? extends @Nullable PrintStream> output = () -> System.err;
758
759        private final LogFormatter formatter = LogFormatter.builder() //
760                .text("[")
761                .level()
762                .text("] - RAINBOW_GUM")
763                .text(" - ")
764                .add(LogFormatter.of((b, e) -> {
765                        b.append(gumLoggerName(e.loggerName()));
766                }))
767                .text(" - ")
768                .message()
769                .textIfThrowable(" ")
770                .throwable()
771                .newline()
772                .build();
773
774        /**
775         * Reports a logging system event at its original level.
776         * @param event event to log.
777         */
778        static void error(LogEvent event) {
779                INSTANCE.log(event);
780        }
781
782        /**
783         * Logs an error in the logging system.
784         * @param loggerName derived from class.
785         * @param throwable error to log.
786         */
787        static void error(Class<?> loggerName, Throwable throwable) {
788                String m = Objects.requireNonNullElse(throwable.getMessage(), "exception");
789                error(loggerName, m, throwable);
790        }
791
792        /**
793         * Logs an error in the logging system.
794         * @param loggerName derived from class.
795         * @param message error message.
796         * @param throwable error to log.
797         */
798        static void error(Class<?> loggerName, String message, Throwable throwable) {
799                var currentThread = Thread.currentThread();
800                var event = LogEvent.of(Instant.now(), currentThread.getName(), currentThread.threadId(), Level.ERROR,
801                                loggerName.getName(), message, KeyValues.of(), throwable);
802                error(event);
803        }
804
805        private static String gumLoggerName(String value) {
806                String prefix = "io.jstach.rainbowgum.";
807                if (value.startsWith(prefix)) {
808                        value = value.substring(prefix.length());
809                }
810                return value;
811        }
812
813        @Override
814        public void log(LogEvent event) {
815                var err = output.get();
816                if (err != null) {
817                        var sb = new StringBuilder();
818                        formatter.format(sb, event);
819                        err.append(sb);
820                }
821        }
822
823}