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, and
221                 * any other unread key is information, since it may be for a component type that
222                 * was not chosen.
223                 */
224                INFO,
225                /**
226                 * Every unread key is a warning, so {@code logging.alerts.fail=warning} fails on
227                 * any of them.
228                 */
229                @EnumAlias("warn")
230                WARNING,
231                /**
232                 * Every unread key is an error, so {@code logging.alerts.fail=error} fails on any
233                 * of them.
234                 */
235                ERROR;
236
237                static UnusedKeyLevel parse(String value) {
238                        return switch (value.toLowerCase(java.util.Locale.ROOT)) {
239                                case "warn" -> WARNING;
240                                default -> LogProperty.enumValue(UnusedKeyLevel.class, value, "warn");
241                        };
242                }
243
244                Level level(boolean suggested) {
245                        return switch (this) {
246                                case INFO -> suggested ? Level.WARNING : Level.INFO;
247                                case WARNING -> Level.WARNING;
248                                case ERROR -> Level.ERROR;
249                        };
250                }
251
252        }
253
254        /**
255         * What {@link #start(LogConfig)} does if, at that moment, at least one error alert
256         * has been recorded and still zero {@link Listener}s are registered - the situation
257         * Logback's {@code StatusManager} calls an unobserved status: nothing is watching, so
258         * whatever went wrong during property loading or a
259         * {@link io.jstach.rainbowgum.spi.RainbowGumServiceProvider.Configurator} would
260         * otherwise only ever have reached the individual, easy to miss stderr lines each
261         * {@link #alert(LogEvent)} call already produces.
262         * <p>
263         * Deliberately keyed as
264         * {@value LogProperties#ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY} (an enum, not a
265         * boolean) since "fail" without "dump" first would throw away the only diagnostic
266         * evidence of why it failed.
267         */
268        enum UnobservedErrorsAction {
269
270                /**
271                 * Do nothing beyond the immediate reporting of warning and error events.
272                 */
273                NONE,
274                /**
275                 * Default. Report the whole backlog to {@code MetaLog} as one clearly labeled
276                 * block, the same way Logback auto-installs a console listener and prints its
277                 * accumulated status if nothing else is watching by the end of configuration -
278                 * RainbowGum still starts.
279                 */
280                DUMP,
281                /**
282                 * Same reporting as {@link #DUMP}, but then throws, so
283                 * {@link LogConfig.Builder#build()} (and therefore whatever is building a
284                 * {@link RainbowGum} from it) never completes. Goes further than Logback ever
285                 * does - an explicit, opt-in choice of integrity over resilience for deployments
286                 * where a silently-broken bootstrap is worse than refusing to start.
287                 */
288                FAIL;
289
290                static UnobservedErrorsAction parse(String value) {
291                        return LogProperty.enumValue(UnobservedErrorsAction.class, value);
292                }
293
294        }
295
296}
297
298/*
299 * Handed to PropertiesProviders. Buffers alerts until the configured LogAlerts exists,
300 * then drainTo replays the buffer into it and forwards every later call, so a provider
301 * may keep this instance and alert after provideProperties returns. Replay and swap
302 * happen under one lock so a concurrent alert is neither lost nor reordered. Listener
303 * registration is never supported: providers record alerts but cannot observe them.
304 */
305final class SwappableLogAlerts implements LogAlerts {
306
307        private final List<LogEvent> events = new ArrayList<>();
308
309        private long total;
310
311        private volatile @Nullable LogAlerts delegate;
312
313        @Override
314        public void alert(LogEvent event) {
315                var d = delegate;
316                if (d == null) {
317                        synchronized (this) {
318                                d = delegate;
319                                if (d == null) {
320                                        events.add(event.freeze());
321                                        total++;
322                                        return;
323                                }
324                        }
325                }
326                d.alert(event);
327        }
328
329        synchronized void drainTo(LogAlerts target) {
330                if (delegate != null) {
331                        throw new IllegalStateException("Alerts have already been drained");
332                }
333                for (var event : events) {
334                        target.alert(event);
335                }
336                events.clear();
337                delegate = target;
338        }
339
340        @Override
341        public synchronized List<LogEvent> dump() {
342                var d = delegate;
343                return d != null ? d.dump() : List.copyOf(events);
344        }
345
346        @Override
347        public synchronized void clear() {
348                var d = delegate;
349                if (d != null) {
350                        d.clear();
351                }
352                else {
353                        events.clear();
354                }
355        }
356
357        @Override
358        public synchronized Stats stats() {
359                var d = delegate;
360                return d != null ? d.stats() : new Stats(total, events.size(), Integer.MAX_VALUE);
361        }
362
363        @Override
364        public AutoCloseable addListener(Listener listener) {
365                throw new UnsupportedOperationException("Properties providers cannot register alert listeners");
366        }
367
368        @Override
369        public void start(LogConfig config) {
370                throw new UnsupportedOperationException("Properties provider alerts cannot be started");
371        }
372
373        @Override
374        public synchronized void close() {
375                // The drained delegate belongs to LogConfig, so only an undrained buffer is
376                // dropped.
377                if (delegate == null) {
378                        events.clear();
379                }
380        }
381
382}
383
384final class DefaultLogAlerts implements LogAlerts {
385
386        private final LogEvent[] ring;
387
388        private int start = 0;
389
390        private int size = 0;
391
392        private final AtomicLong total = new AtomicLong();
393
394        private final AtomicLong errors = new AtomicLong();
395
396        private final ReentrantLock lock = new ReentrantLock();
397
398        private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();
399
400        /*
401         * Kept apart from listeners.isEmpty() deliberately: DefaultLogConfig's own
402         * metrics-bridge listener (see its constructor) is always registered before
403         * start(...) ever runs, and a passive in-process counter nobody has wired an exporter
404         * to is not "someone is watching" in the sense UnobservedErrorsAction cares about -
405         * only a real addListener(Listener) call (this class's public API) flips this.
406         */
407        private volatile boolean hasExternalListener = false;
408
409        private final LogEventFactory eventFactory = LogEventFactory.of(DefaultLogAlerts.class.getName());
410
411        /*
412         * Resolved once, at construction (same as capacity) rather than inside start(...):
413         * unlike capacity this does not shape any constructor-time state, but LogProperties
414         * is available just as early, and start(LogConfig) is really about announcing "I have
415         * started" (config is there because a few edge cases needed it, not because this
416         * needed to be a start-time lookup).
417         */
418        private final UnobservedErrorsAction unobservedErrorsAction;
419
420        /*
421         * Private and deliberately not defensive - of(LogProperties) below is the only
422         * caller, and it has already validated both fields (capacity > 0, a resolvable
423         * UnobservedErrorsAction) before ever reaching this constructor. Re-checking here
424         * would be defensive programming duplicating what was already a real, reported
425         * validation failure one call up - see of(...)'s own comment.
426         */
427        private final FailLevel failLevel;
428
429        /*
430         * Names what set the fail level, for the failure message: the property, or
431         * logging.debug=help raising it.
432         */
433        private final String failLevelSource;
434
435        private final UnusedKeyLevel unusedKeyLevel;
436
437        /*
438         * Startup failure is independent of the bounded diagnostic queue. Keep the first
439         * failure and the count until startup ends, even if clear() or eviction removes it
440         * from the queue. All three fields are protected by lock.
441         */
442        private long startupFailures;
443
444        private @Nullable LogEvent firstStartupFailure;
445
446        private boolean startupComplete;
447
448        private DefaultLogAlerts(int capacity, UnobservedErrorsAction unobservedErrorsAction, FailLevel failLevel,
449                        String failLevelSource, UnusedKeyLevel unusedKeyLevel) {
450                this.ring = new LogEvent[capacity];
451                this.unobservedErrorsAction = unobservedErrorsAction;
452                this.failLevel = failLevel;
453                this.failLevelSource = failLevelSource;
454                this.unusedKeyLevel = unusedKeyLevel;
455        }
456
457        FailLevel failLevel() {
458                return failLevel;
459        }
460
461        UnusedKeyLevel unusedKeyLevel() {
462                return unusedKeyLevel;
463        }
464
465        /*
466         * Checks the startup failure count and lists retained failing alerts, including the
467         * first failure even if it left the queue. Called when LogConfig is built and right
468         * after Rainbow Gum starts.
469         */
470        void failIfAlerted(String phase) {
471                checkStartup(phase, false);
472        }
473
474        void finishStartup() {
475                checkStartup("starting", true);
476        }
477
478        private void checkStartup(String phase, boolean complete) {
479                lock.lock();
480                try {
481                        if (startupComplete) {
482                                return;
483                        }
484                        startupComplete = complete;
485                        var first = firstStartupFailure;
486                        if (first == null) {
487                                return;
488                        }
489                        var failing = new ArrayList<>(dump().stream().filter(e -> failLevel.fails(e.level())).toList());
490                        if (!failing.contains(first)) {
491                                failing.addFirst(first);
492                        }
493                        var sb = new StringBuilder();
494                        sb.append(startupFailures)
495                                .append(" alert(s) at ")
496                                .append(failLevel.name().toLowerCase(Locale.ROOT))
497                                .append(" or above were recorded while ")
498                                .append(phase)
499                                .append(" and ")
500                                .append(failLevelSource)
501                                .append(":");
502                        for (var e : failing) {
503                                sb.append("\n[").append(e.level()).append("] ").append(e.message());
504                        }
505                        if (startupFailures > failing.size()) {
506                                sb.append("\n")
507                                        .append(startupFailures - failing.size())
508                                        .append(" additional failing alert(s) are no longer retained.");
509                        }
510                        throw new IllegalStateException(sb.toString());
511                }
512                finally {
513                        if (complete) {
514                                firstStartupFailure = null;
515                        }
516                        lock.unlock();
517                }
518        }
519
520        /*
521         * The fail level as resolved from properties, before LogAlerts exists, so LogConfig
522         * can decide whether to record property reads. Resolved again, and validated, by
523         * of(...).
524         */
525        static FailLevel failLevelOr(LogProperties properties, FailLevel fallback) {
526                var value = properties.valueOrNull(LogProperties.ALERTS_FAIL_PROPERTY);
527                try {
528                        return value == null ? fallback : FailLevel.parse(value);
529                }
530                catch (RuntimeException e) {
531                        return fallback;
532                }
533        }
534
535        /*
536         * The unused key level as resolved from properties, before LogAlerts exists, like
537         * failLevelOr. An invalid value is reported by of(...).
538         */
539        static UnusedKeyLevel unusedKeyLevelOr(LogProperties properties, UnusedKeyLevel fallback) {
540                var value = properties.valueOrNull(LogProperties.ALERTS_UNUSED_KEY_PROPERTY);
541                try {
542                        return value == null ? fallback : UnusedKeyLevel.parse(value);
543                }
544                catch (RuntimeException e) {
545                        return fallback;
546                }
547        }
548
549        /*
550         * Package-friend static factory: resolves and validates both
551         * LogProperties#ALERTS_CAPACITY_PROPERTY and
552         * LogProperties#ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY against one shared Validator
553         * - a mistake in both at once is reported together instead of only the first one
554         * found - then constructs. requirePositiveCapacity is a plain int -> int validating
555         * transform (not an object construction) specifically so it can sit inside map(...)
556         * without tripping CheckerFramework's Initialization Checker, which does not refine a
557         * `new Foo(...)` expression back to @Initialized when it appears directly inside a
558         * lambda body passed through a generic method like map(...) - the one and only `new
559         * DefaultLogAlerts(...)` call stays a plain, unconditional statement here instead.
560         */
561        static DefaultLogAlerts of(LogProperties properties, FailLevel failLevelFallback,
562                        UnusedKeyLevel unusedKeyLevelFallback, boolean help) {
563                var validator = LogProperty.Validator.of(LogAlerts.class);
564                var failLevelResult = properties.forKey(LogProperties.ALERTS_FAIL_PROPERTY)
565                        .ofString()
566                        .map(FailLevel::parse)
567                        .or(failLevelFallback)
568                        .validateIfError(validator);
569                var unobservedErrorsActionResult = properties.forKey(LogProperties.ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY)
570                        .ofString()
571                        .map(UnobservedErrorsAction::parse)
572                        .or(UnobservedErrorsAction.DUMP)
573                        .validateIfError(validator);
574                var unusedKeyLevelResult = properties.forKey(LogProperties.ALERTS_UNUSED_KEY_PROPERTY)
575                        .ofString()
576                        .map(UnusedKeyLevel::parse)
577                        .or(unusedKeyLevelFallback)
578                        .validateIfError(validator);
579                var capacityResult = properties.forKey(LogProperties.ALERTS_CAPACITY_PROPERTY)
580                        .ofInt()
581                        .or(LogAlerts.DEFAULT_CAPACITY)
582                        .map(DefaultLogAlerts::requirePositiveCapacity)
583                        .validateIfError(validator);
584                validator.validate();
585                var failLevel = failLevelResult.value();
586                String failLevelSource = LogProperties.ALERTS_FAIL_PROPERTY + "=" + failLevel.name().toLowerCase(Locale.ROOT);
587                if (help && failLevel != FailLevel.WARNING) {
588                        failLevel = FailLevel.WARNING;
589                        failLevelSource = "logging.debug=help";
590                }
591                return new DefaultLogAlerts(capacityResult.value(), unobservedErrorsActionResult.value(), failLevel,
592                                failLevelSource, unusedKeyLevelResult.value());
593        }
594
595        private static int requirePositiveCapacity(int capacity) {
596                if (capacity <= 0) {
597                        throw new IllegalArgumentException("capacity should be greater than 0");
598                }
599                return capacity;
600        }
601
602        @Override
603        public void alert(LogEvent event) {
604                var frozen = event.freeze();
605                total.incrementAndGet();
606                if (frozen.level() == Level.ERROR) {
607                        errors.incrementAndGet();
608                }
609                lock.lock();
610                try {
611                        if (!startupComplete && failLevel.fails(frozen.level())) {
612                                startupFailures++;
613                                if (firstStartupFailure == null) {
614                                        firstStartupFailure = frozen;
615                                }
616                        }
617                        if (size < ring.length) {
618                                ring[(start + size) % ring.length] = frozen;
619                                size++;
620                        }
621                        else {
622                                ring[start] = frozen;
623                                start = (start + 1) % ring.length;
624                        }
625                }
626                finally {
627                        lock.unlock();
628                }
629                for (var listener : listeners) {
630                        try {
631                                listener.onAlert(frozen);
632                        }
633                        catch (Exception e) {
634                                MetaLog.INSTANCE.log(eventFactory.eventNoArg(Level.ERROR, "LogAlerts.Listener threw", e));
635                        }
636                }
637                if (frozen.level() == Level.WARNING || frozen.level() == Level.ERROR) {
638                        MetaLog.INSTANCE.log(frozen);
639                }
640        }
641
642        @Override
643        public AutoCloseable addListener(Listener listener) {
644                listeners.add(listener);
645                hasExternalListener = true;
646                return () -> listeners.remove(listener);
647        }
648
649        /*
650         * DefaultLogConfig's own metrics-bridge listener goes through this instead of
651         * addListener(Listener) - see hasExternalListener's own comment for why.
652         */
653        void addInternalListener(Listener listener) {
654                listeners.add(listener);
655        }
656
657        @Override
658        public void start(LogConfig config) {
659                if (unobservedErrorsAction == UnobservedErrorsAction.NONE || hasExternalListener || errors.get() == 0) {
660                        return;
661                }
662                var backlog = dump();
663                MetaLog.error(eventFactory.eventNoArg(Level.ERROR,
664                                backlog.size() + " alert(s) were recorded before any LogAlerts.Listener was registered - "
665                                                + "dumping the backlog now since nothing else will see it:",
666                                null));
667                for (var event : backlog) {
668                        MetaLog.error(event);
669                }
670                if (unobservedErrorsAction == UnobservedErrorsAction.FAIL) {
671                        throw new IllegalStateException(
672                                        backlog.size() + " alert(s) were recorded before any LogAlerts.Listener was registered and "
673                                                        + LogProperties.ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY + "=FAIL - refusing to start.");
674                }
675        }
676
677        @Override
678        public void close() {
679                listeners.clear();
680                lock.lock();
681                try {
682                        startupComplete = true;
683                        firstStartupFailure = null;
684                }
685                finally {
686                        lock.unlock();
687                }
688        }
689
690        @Override
691        public List<LogEvent> dump() {
692                lock.lock();
693                try {
694                        List<LogEvent> list = new ArrayList<>(size);
695                        for (int i = 0; i < size; i++) {
696                                var e = ring[(start + i) % ring.length];
697                                if (e != null) {
698                                        list.add(e);
699                                }
700                        }
701                        return List.copyOf(list);
702                }
703                finally {
704                        lock.unlock();
705                }
706        }
707
708        @Override
709        public void clear() {
710                lock.lock();
711                try {
712                        Arrays.fill(ring, null);
713                        start = 0;
714                        size = 0;
715                }
716                finally {
717                        lock.unlock();
718                }
719        }
720
721        @Override
722        public Stats stats() {
723                lock.lock();
724                try {
725                        return new Stats(total.get(), size, ring.length);
726                }
727                finally {
728                        lock.unlock();
729                }
730        }
731
732}
733
734/**
735 * Logging about logging. This is the static, always-available entry point used by code
736 * that cannot easily reach a {@link LogConfig} (e.g. before a {@link RainbowGum} is
737 * bound), and always reports directly (currently to stderr) rather than forwarding to a
738 * bound {@link RainbowGum}'s {@link LogConfig#alerts()} - alerts is a small bounded ring
739 * buffer meant to hold a curated, recent view of noteworthy logging-system problems, and
740 * {@link MetaLog} is used from enough different low level failure paths that routing it
741 * there risked flooding/evicting genuine alerts with whatever volume of things end up
742 * calling this class.
743 * <p>
744 * Components that already have (or can easily capture) a {@link LogConfig} - for example
745 * via {@link LogProvider} or {@link LogLifecycle#start(LogConfig)} - should prefer
746 * {@link LogConfig#alerts()} directly instead of this class.
747 *
748 * @author agentgt
749 */
750@SuppressWarnings("ImmutableEnumChecker")
751enum MetaLog implements LogEventLogger {
752
753        INSTANCE;
754
755        static Supplier<? extends @Nullable PrintStream> output = () -> System.err;
756
757        private final LogFormatter formatter = LogFormatter.builder() //
758                .text("[")
759                .level()
760                .text("] - RAINBOW_GUM")
761                .text(" - ")
762                .add(LogFormatter.of((b, e) -> {
763                        b.append(gumLoggerName(e.loggerName()));
764                }))
765                .text(" - ")
766                .message()
767                .textIfThrowable(" ")
768                .throwable()
769                .newline()
770                .build();
771
772        /**
773         * Reports a logging system event at its original level.
774         * @param event event to log.
775         */
776        static void error(LogEvent event) {
777                INSTANCE.log(event);
778        }
779
780        /**
781         * Logs an error in the logging system.
782         * @param loggerName derived from class.
783         * @param throwable error to log.
784         */
785        static void error(Class<?> loggerName, Throwable throwable) {
786                String m = Objects.requireNonNullElse(throwable.getMessage(), "exception");
787                error(loggerName, m, throwable);
788        }
789
790        /**
791         * Logs an error in the logging system.
792         * @param loggerName derived from class.
793         * @param message error message.
794         * @param throwable error to log.
795         */
796        static void error(Class<?> loggerName, String message, Throwable throwable) {
797                var currentThread = Thread.currentThread();
798                var event = LogEvent.of(Instant.now(), currentThread.getName(), currentThread.threadId(), Level.ERROR,
799                                loggerName.getName(), message, KeyValues.of(), throwable);
800                error(event);
801        }
802
803        private static String gumLoggerName(String value) {
804                String prefix = "io.jstach.rainbowgum.";
805                if (value.startsWith(prefix)) {
806                        value = value.substring(prefix.length());
807                }
808                return value;
809        }
810
811        @Override
812        public void log(LogEvent event) {
813                var err = output.get();
814                if (err != null) {
815                        var sb = new StringBuilder();
816                        formatter.format(sb, event);
817                        err.append(sb);
818                }
819        }
820
821}