001package io.jstach.rainbowgum;
002
003import java.lang.System.Logger.Level;
004import java.lang.ref.WeakReference;
005import java.util.ArrayDeque;
006import java.util.ArrayList;
007import java.util.List;
008import java.util.Objects;
009import java.util.Optional;
010import java.util.Queue;
011import java.util.ServiceLoader;
012import java.util.UUID;
013import java.util.concurrent.ConcurrentLinkedQueue;
014import java.util.concurrent.atomic.AtomicInteger;
015import java.util.concurrent.locks.ReentrantReadWriteLock;
016import java.util.function.Consumer;
017import java.util.function.Function;
018import java.util.function.Supplier;
019
020import org.jspecify.annotations.Nullable;
021
022import io.jstach.rainbowgum.LogRouter.RootRouter;
023import io.jstach.rainbowgum.LogRouter.Router;
024import io.jstach.rainbowgum.spi.RainbowGumServiceProvider;
025
026//@formatter:off
027/**
028 * The main entry point and configuration of RainbowGum logging.
029 * <p>
030 * RainbowGum logging loads configuration through the service loader. While you can
031 * manually set RainbowGum using {@link #set(Supplier)} it is better to register
032 * implementations through the ServiceLoader so that RainbowGum will load prior to any
033 * external logging.
034 * <p>
035 * To register a custom RainbowGum:
036 *
037 *
038{@snippet class="snippets.RainbowGumProviderExample" region="provider" :
039
040class RainbowGumProviderExample implements RainbowGumProvider {
041
042        @Override
043        public Optional<RainbowGum> provide(LogConfig config) {
044
045                Integer bufferSize = config.properties() //
046                        .forKey("logging.custom.async.bufferSize")
047                        .ofInt()
048                        .or(1024)
049                        .validateNow(RainbowGumProviderExample.class);
050
051                LogProvider<LogOutput> output = (name, cfg) -> cfg.properties()
052                        .forKey("logging.custom.output")
053                        .ofProvider(LogOutput::of)
054                        .or(LogOutput.ofStandardOut())
055                        .validateNow(RainbowGumProviderExample.class)
056                        .provide(name, cfg);
057
058                var gum = RainbowGum.builder() //
059                        .route(r -> {
060                                r.publisher(PublisherFactory //
061                                        .async() //
062                                        .bufferSize(bufferSize) //
063                                        .build());
064                                r.appender("console", a -> {
065                                        a.output(output);
066                                });
067                                r.level(Level.INFO);
068                        })
069                        .build();
070
071                return Optional.of(gum);
072        }
073
074}
075}
076 * <p>
077 * <strong>Initialization order</strong> - most of the actual work happens in
078 * {@link LogConfig.Builder#build()}, {@link LogRouter.Router.Builder}, and
079 * {@code LogAppenderRegistry}; this is the exact sequence for anyone debugging a
080 * configuration or writing a {@link RainbowGumServiceProvider.RainbowGumProvider}:
081 * <ol>
082 * <li>{@link LogConfig.Builder#build()} builds the shared {@link LogConfig}:
083 * <ol>
084 * <li>Resolve the {@link ServiceRegistry} (default: a new empty one) and
085 * {@link LogProperties} - manually added
086 * {@link RainbowGumServiceProvider.PropertiesProvider}s, else (if a
087 * {@link java.util.ServiceLoader} was supplied) SPI-discovered ones, with system
088 * properties always layered in as a final fallback. Providers receive a temporary
089 * {@link LogAlerts} collector.</li>
090 * <li>Build {@link LogAlerts} and {@link LogMetrics} from properties.</li>
091 * <li>Build the global level resolver ({@code logging.level.*} and
092 * {@code logging.group.*}, plus anything added directly on the builder).</li>
093 * <li>Construct the {@link LogConfig} itself and replay provider alerts into its
094 * configured {@link LogAlerts}. This is also where the {@link LogConfig#globals()
095 * globals} are resolved and the default output/encoder/publisher registries are
096 * created.</li>
097 * <li>Run every {@link RainbowGumServiceProvider.Configurator} (manually added,
098 * then SPI-discovered), highest {@linkplain RainbowGumServiceProvider.Configurator#priority()
099 * priority} first. A configurator returning {@code false} is retried in a later
100 * pass (see {@link RainbowGumServiceProvider#PASSES}) - useful if it depends on
101 * another configurator running first.</li>
102 * <li>Start {@link LogAlerts} - deliberately last, so a configurator's own alert
103 * listener is already registered before alerts decides whether an unobserved
104 * error should refuse startup.</li>
105 * </ol>
106 * </li>
107 * <li>{@link Builder#build()} builds each named {@link LogRouter.Router} (a single
108 * route named {@value LogProperties#DEFAULT_NAME}, unless {@code logging.routes}
109 * names several) via {@link LogRouter.Router.Builder}'s own (package-private)
110 * {@code build()}:
111 * <ol>
112 * <li>Resolve route flags ({@code logging.route.<name>.flags}).</li>
113 * <li>Build a route-scoped level resolver: anything configured directly on the
114 * route builder, layered over {@code logging.route.<name>.level.*}, falling back
115 * to the global level resolver from step 1 (skipped entirely if the route flag
116 * {@code IGNORE_GLOBAL_LEVEL_RESOLVER} is set) - and finally to {@code INFO} if
117 * nothing at all resolves a level.</li>
118 * <li>Resolve appenders - only if none were added programmatically on the route
119 * builder (that check is all-or-nothing: adding even one appender in code skips
120 * property-based resolution entirely). See <strong>Appender resolution</strong>
121 * below.</li>
122 * <li>Resolve the publisher - {@code logging.route.<name>.publisher.uri}, falling
123 * back to a synchronous publisher if unset.</li>
124 * <li>Hand the resolved appenders to the publisher and register the publisher in
125 * the {@link ServiceRegistry}.</li>
126 * </ol>
127 * </li>
128 * <li>{@link #start()} starts the root router, which starts each route's
129 * publisher, which starts each of its appenders, which starts its output (e.g.
130 * opening a file).</li>
131 * </ol>
132 * <strong>Appender resolution</strong> - the part most often mistaken for a bug,
133 * since no appenders are ever added programmatically in the common case:
134 * <ol>
135 * <li>{@code logging.route.<name>.appenders} names the route's appenders; for the
136 * {@value LogProperties#DEFAULT_NAME} route only, a missing value falls back to
137 * the unprefixed {@code logging.appenders}.</li>
138 * <li>If that is <em>still</em> missing: for the default route only, the list
139 * defaults to {@code [file, console]} if {@code logging.file.name} (or Spring
140 * Boot's convention) is set, otherwise just {@code [console]}. A non-default
141 * route with no appenders configured anywhere is a startup failure, not a silent
142 * no-op.</li>
143 * <li>Each named appender then resolves independently. {@code file} and
144 * {@code console} are the two built-in special cases, each with its own output
145 * property/fallback ({@code logging.file.name} then
146 * {@code logging.appender.file.output.uri} for {@code file};
147 * {@code logging.appender.console.output.uri} falling back to standard-out for
148 * {@code console}). Any other name is fully generic:
149 * {@code logging.appender.<name>.output.uri}/{@code .encoder.uri}/{@code .flags}/
150 * {@code .type}, with the output's own URI scheme choosing which registered
151 * {@link LogOutput.OutputProvider} builds it.</li>
152 * <li>An output can supply its own encoder by implementing
153 * {@link LogOutput.ProvidesEncoder} (rare, e.g. some structured outputs): with
154 * {@link LogOutput.ProvidesEncoder.Policy#MANDATORY} that encoder is used as-is
155 * and the {@code .encoder.uri} property is never consulted (configuring one anyway
156 * is a startup failure); with {@link LogOutput.ProvidesEncoder.Policy#DEFAULT}
157 * it is only a fallback and the {@code .encoder.uri} property, if set, still wins.
158 * An output that does not implement {@link LogOutput.ProvidesEncoder} at all is
159 * unaffected: the {@code .encoder.uri} property (or the registry's default for the
160 * output's type) always applies.</li>
161 * </ol>
162 */
163//@formatter:on
164@SuppressWarnings("InvalidInlineTag")
165public sealed interface RainbowGum extends AutoCloseable, LogEventLogger {
166
167        /**
168         * Gets the currently statically bound RainbowGum and will try to load and find one if
169         * there is none currently bound. <strong> This is a blocking operation as in locks
170         * are used and will block indefinitely till a gum has loaded. </strong> If that is
171         * not desired see {@link #getOrNull()}.
172         * @return current RainbowGum or new loaded one.
173         * @throws IllegalStateException if called on a thread that is starting the global
174         * RainbowGum, for example by logging while it loads. See {@link #ofOrNull()}.
175         */
176        public static RainbowGum of() {
177                var gum = RainbowGumHolder.getOrNullIfStarting();
178                if (gum == null) {
179                        throw new IllegalStateException("RainbowGum component tried to log too early. "
180                                        + "This is usually caused by dependencies calling logging.");
181                }
182                return gum;
183        }
184
185        /**
186         * Same as {@link #of()} except that it returns <code>null</code> instead of throwing
187         * when called on a thread that is starting the global RainbowGum, for example by
188         * logging while it loads. A logging facade getting <code>null</code> can use
189         * {@link LogRouter#global()}, which holds events until a RainbowGum is bound.
190         * @return current RainbowGum or new loaded one, or <code>null</code> if this thread
191         * is starting one.
192         */
193        public static @Nullable RainbowGum ofOrNull() {
194                return RainbowGumHolder.getOrNullIfStarting();
195        }
196
197        /**
198         * Gets the currently statically bound RainbowGum or <code>null</code> if none are
199         * <strong>finished binding</strong>. Unlike {@link #of()} this will never load or
200         * start an instance but rather just gets the currently bound one. It will also never
201         * block and does not wait if one is currently being loaded. This is a snapshot: the
202         * instance can be replaced or closed after it is read.
203         * @return current RainbowGum or <code>null</code>
204         */
205        public static @Nullable RainbowGum getOrNull() {
206                return RainbowGumHolder.current();
207        }
208
209        /**
210         * Provides the service loader default based RainbowGum.
211         * @return RainbowGum.
212         */
213        public static RainbowGum defaults() {
214                return RainbowGumServiceProvider.provide();
215        }
216
217        /**
218         * Creates a RainbowGum that will <strong>ALWAYS</strong> use the global router.
219         * @param config config.
220         * @return rainbow gum that always uses global router.
221         */
222        public static RainbowGum queued(LogConfig config) {
223                return new SimpleRainbowGum(config, LogRouter.global(), UUID.randomUUID());
224        }
225
226        /**
227         * Sets the global default RainbowGum. The supplied {@link RainbowGum} must not
228         * already be started.
229         * @param supplier the supplier will be memoized when {@linkplain Supplier#get()
230         * accessed} and {@link RainbowGum#start()} will be called.
231         */
232        public static void set(Supplier<RainbowGum> supplier) {
233                RainbowGumHolder.set(supplier);
234        }
235
236        /**
237         * Subscribes to changes of the globally bound RainbowGum: a new one becoming bound
238         * (see {@link #set(Supplier)}, {@link #set(RainbowGum)}, {@link Builder#set()}) or
239         * the bound one no longer being bound (closing it, {@link Builder#unset()}, or
240         * {@link #set(Supplier)} replacing it). This is primarily for components like SLF4J's
241         * logger factory that are themselves bootstrapped once (often before "the real"
242         * RainbowGum has loaded, e.g. a framework's own pre-boot sequence) and otherwise have
243         * no way of finding out that a different RainbowGum has since replaced the one they
244         * captured.
245         * <p>
246         * Only a {@linkplain java.lang.ref.WeakReference weak reference} to
247         * <code>consumer</code> is retained, so subscribing does not by itself keep the
248         * consumer (or whatever it is attached to) reachable - the caller is responsible for
249         * keeping a strong reference to <code>consumer</code> for as long as it needs to keep
250         * receiving notifications, otherwise it may silently stop being called once garbage
251         * collected.
252         * <p>
253         * The consumer receives what {@link #getOrNull()} returns after the change: the newly
254         * bound instance, or <code>null</code> when none is bound anymore. Removing when none
255         * is bound is not a change. Registering a supplier neither starts it nor closes the
256         * gum it replaces. Subscribing does not replay the currently bound instance.
257         * Notifications are delivered in the order of the changes. A concurrent or reentrant
258         * change queues its notification until the current delivery finishes, so its caller
259         * may return before subscribers have seen it. A subscriber throwing a runtime
260         * exception is reported through the internal error log and does not prevent later
261         * deliveries. A notification is not a guarantee that the supplied gum is still
262         * current or open when the consumer runs.
263         * @apiNote A consumer receiving <code>null</code> should drop what it holds rather
264         * than call {@link #of()}, which would start another gum while the application may be
265         * shutting down.
266         * @param consumer called with the newly bound global RainbowGum or <code>null</code>
267         * when none is bound. Never called while any RainbowGum internal lock is held.
268         */
269        public static void onGlobalChange(Consumer<@Nullable RainbowGum> consumer) {
270                RainbowGumHolder.globalChangePublisher.add(consumer);
271        }
272
273        /**
274         * Sets a Rainbow Gum and provides a supplier that will start it globally.
275         * @param gum that will be set immediatly.
276         * @return supplier that will set, get and start the supplied gum and if it is not the
277         * same will throw {@link IllegalStateException}.
278         */
279        public static Supplier<? extends RainbowGum> set(RainbowGum gum) {
280                UUID instanceId = gum.instanceId();
281                RainbowGum.set(() -> gum);
282                return () -> {
283                        var of = RainbowGum.of();
284                        /*
285                         * TODO this is a hack. The holder lock should be used to make this not
286                         * happen.
287                         */
288                        if (!instanceId.equals(of.instanceId())) {
289                                throw new IllegalStateException("Another rainbow gum registered itself as the global. "
290                                                + "This is rare reace condition and probably a bug");
291                        }
292                        return of;
293                };
294        }
295
296        /**
297         * The config associated with this instance.
298         * @return config.
299         */
300        public LogConfig config();
301
302        /**
303         * The router that will route log messages to publishers.
304         * @return router
305         */
306        public RootRouter router();
307
308        /**
309         * Starts the rainbow gum and returns it. It is returned for try-with usage
310         * convenience.
311         * @return the started RainbowGum.
312         */
313        default RainbowGum start() {
314                router().start(config());
315                var debugMode = config().debugMode();
316                if (debugMode == LogConfig.DebugModeType.ALL || debugMode == LogConfig.DebugModeType.HELP) {
317                        var report = config().reporter().report();
318                        MetaLog.error(LogEventFactory.of(RainbowGum.class.getName())
319                                .eventNoArg(Level.INFO, "Rainbow Gum started:\n" + report, null));
320                }
321                if (config() instanceof DefaultLogConfig c) {
322                        var check = c.unusedKeyCheck();
323                        if (check != null) {
324                                reportUnusedKeys(check);
325                        }
326                }
327                if (config().alerts() instanceof DefaultLogAlerts alerts) {
328                        try {
329                                alerts.finishStartup();
330                        }
331                        catch (RuntimeException e) {
332                                close();
333                                throw e;
334                        }
335                }
336                return this;
337        }
338
339        /*
340         * By default an unused key with a suggestion is almost always a typo, so it is a
341         * warning, which logging.alerts.fail=warning (and logging.debug=help) turns into a
342         * failure. So is an unread URI query parameter, since its URI chose the component.
343         * Any other is only information: the key may be for a component type that was not
344         * chosen, such as encoder options left behind by a profile that switches encoder.
345         * logging.alerts.unusedKey raises both for strict configurations.
346         */
347        private void reportUnusedKeys(UnusedKeyCheck check) {
348                var eventFactory = LogEventFactory.of(LogConfig.class.getName());
349                var unusedKeyLevel = config().alerts() instanceof DefaultLogAlerts alerts ? alerts.unusedKeyLevel()
350                                : LogAlerts.UnusedKeyLevel.INFO;
351                for (var u : check.unused()) {
352                        var level = unusedKeyLevel.level(u.likelyMistake());
353                        var event = eventFactory.eventNoArg(level, u.message(), null);
354                        config().alerts().alert(event);
355                        if (level == Level.INFO && config().debugMode().checksUnusedKeys()) {
356                                MetaLog.error(event);
357                        }
358                }
359        }
360
361        /**
362         * Unique id of rainbow gum instance.
363         * @return random id created on creation.
364         */
365        public UUID instanceId();
366
367        /**
368         * Will close the RainbowGum and all registered components as well as removed from the
369         * shutdown hooks. If the rainbow gum is set as global it will no longer be global and
370         * replaced with the bootstrapping in memory queue. {@inheritDoc}
371         */
372        @Override
373        public void close();
374
375        /**
376         * This append call is mainly for testing as it does not avoid making events that do
377         * not need to be made if no logging needs to be done. {@inheritDoc}
378         */
379        @Override
380        default void log(LogEvent event) {
381                var r = router().route(event.loggerName(), event.level());
382                if (r.isEnabled()) {
383                        r.log(event);
384                }
385        }
386
387        /**
388         * Use to build a custom {@link RainbowGum} which will use the {@link LogConfig}
389         * provided by the service loader, so installed modules are discovered.
390         * @return builder.
391         * @see #builder(Consumer)
392         */
393        public static Builder builder() {
394                ServiceLoader<RainbowGumServiceProvider> loader = ServiceLoader.load(RainbowGumServiceProvider.class);
395                var config = RainbowGumServiceProvider.provideConfig(loader);
396                return builder(config);
397        }
398
399        /**
400         * Use to build a custom {@link RainbowGum} with supplied config.
401         * @param config the config
402         * @return builder.
403         * @see #builder()
404         */
405        public static Builder builder(LogConfig config) {
406                return new Builder(config);
407        }
408
409        /**
410         * Use to build a custom {@link RainbowGum} whose config starts like
411         * {@link #builder()}: the config builder already uses the service loader, so
412         * installed modules are discovered, and the consumer changes only what it needs to.
413         * Calling {@link LogConfig.Builder#serviceLoader(ServiceLoader)} in the consumer
414         * replaces the service loader. To build without the service loader use
415         * {@link #builder(LogConfig)} with a config from {@link LogConfig#builder()}.
416         * @param config consumer that has first argument as config builder.
417         * @return builder.
418         * @see #builder()
419         */
420        public static Builder builder(Consumer<? super LogConfig.Builder> config) {
421                var b = LogConfig.builder().serviceLoader(ServiceLoader.load(RainbowGumServiceProvider.class));
422                config.accept(b);
423                return builder(b.build());
424        }
425
426        /**
427         * RainbowGum Builder.
428         */
429        public class Builder {
430
431                private final LogConfig config;
432
433                private final List<Router> routes = new ArrayList<>();
434
435                private Builder(LogConfig config) {
436                        this.config = config;
437                }
438
439                /**
440                 * Adds a router.
441                 * @param route a router.
442                 * @return builder.
443                 */
444                public Builder route(Router route) {
445                        bind();
446                        this.routes.add(route);
447                        return this;
448                }
449
450                /*
451                 * Routers provision outputs/appenders into the config as soon as they are built,
452                 * so the config must be claimed before that, not just at build().
453                 */
454                private void bind() {
455                        ((DefaultLogConfig) config).bind(this);
456                }
457
458                /**
459                 * Adds a route by using a consumer of the route builder.
460                 * @param name name of router.
461                 * @param consumer consumer is passed router builder. The consumer does not need
462                 * to call {@link Router#builder(String,LogConfig)}
463                 * @return builder.
464                 * @see io.jstach.rainbowgum.LogRouter.Router.Builder
465                 */
466                public Builder route(String name, Consumer<Router.Builder> consumer) {
467                        bind();
468                        var builder = Router.builder(name, config);
469                        consumer.accept(builder);
470                        return route(builder.build());
471                }
472
473                /**
474                 * Adds a route by using a consumer of the route builder.
475                 * @param consumer consumer is passed router builder. The consumer does not need
476                 * to call {@link Router#builder(String,LogConfig)}
477                 * @return builder.
478                 * @see io.jstach.rainbowgum.LogRouter.Router.Builder
479                 */
480                public Builder route(Consumer<Router.Builder> consumer) {
481                        bind();
482                        var builder = Router.builder(Router.DEFAULT_ROUTER_NAME, config);
483                        consumer.accept(builder);
484                        return route(builder.build());
485                }
486
487                /**
488                 * Builds an un-started {@link RainbowGum}.
489                 * @return an un-started {@link RainbowGum}.
490                 */
491                public RainbowGum build() {
492                        return build(UUID.randomUUID());
493                }
494
495                /**
496                 * Builds an un-started {@link RainbowGum}.
497                 * @param instanceId unique id for rainbow gum instance.
498                 * @return an un-started {@link RainbowGum}.
499                 */
500                private RainbowGum build(UUID instanceId) {
501                        ((DefaultLogConfig) config).markBuilt(this);
502                        var routes = this.routes;
503                        var config = this.config;
504                        if (routes.isEmpty()) {
505                                var routeNamesResult = config.properties() //
506                                        .forKey(LogProperties.ROUTES_PROPERTY)
507                                        .ofList()
508                                        .or(List.of());
509                                List<String> routeNames = routeNamesResult.validateNow(Builder.class);
510                                /*
511                                 * Validated as a plain list of names, before any of them is used to build
512                                 * a Router, so a bad name is reported against this list property itself,
513                                 * not deferred until some unrelated {name}-keyed property (e.g. this
514                                 * route's own level) happens to interpolate it first.
515                                 */
516                                LogProperty.Validator.validateNames(LogRouter.class, "route", routeNamesResult, routeNames);
517                                if (routeNames.isEmpty()) {
518                                        routes = List.of(Router.builder(Router.DEFAULT_ROUTER_NAME, config).build());
519                                }
520                                else {
521                                        routes = routeNames.stream().map(n -> Router.builder(n, config).build()).toList();
522                                }
523                        }
524                        var root = InternalRootRouter.of(routes, config);
525                        config.changePublisher().subscribe(c -> {
526                                root.changePublisher().publish(root);
527                        });
528                        ((DefaultLogConfig) config).reporter().bind(root);
529                        return new SimpleRainbowGum(config, root, instanceId);
530                }
531
532                /**
533                 * Builds, starts and sets the RainbowGum as the global one picked up by logging
534                 * facades.
535                 * @return started and set rainbow that can be used in a try-close.
536                 */
537                public RainbowGum set() {
538                        UUID instanceId = UUID.randomUUID();
539                        RainbowGum.set(() -> build(instanceId));
540                        var gum = RainbowGum.of();
541                        /*
542                         * TODO this is a hack. The holder lock should be used to make this not
543                         * happen.
544                         */
545                        if (!instanceId.equals(gum.instanceId())) {
546                                throw new IllegalStateException("Another rainbow gum registered itself as the global. "
547                                                + "This is rare reace condition and probably a bug");
548                        }
549                        return gum;
550                }
551
552                /**
553                 * Removes the currently set Rainbow Gum which will close it if one exists and
554                 * will be replaced by the default provision process if {@link RainbowGum#of()} is
555                 * called before {@link #set()}.
556                 * @apiNote This is largely an internal detail for unit testing the provision
557                 * process.
558                 */
559                public void unset() {
560                        RainbowGumHolder.remove(null);
561                }
562
563                /**
564                 * For returning an optional for the LogProvider contract.
565                 * @return optional that always has a rainbow gum.
566                 * @apiNote this method is for ergonomics.
567                 */
568                public Optional<RainbowGum> optional() {
569                        return Optional.of(this.build());
570                }
571
572                /**
573                 * For returning an optional for the LogProvider contract.
574                 * @param condition condition to check if this rainbow gum should be used.
575                 * @return optional rainbow gum.
576                 * @apiNote this method is for ergonomics.
577                 */
578                public Optional<RainbowGum> optional(Function<? super LogConfig, Boolean> condition) {
579                        var cond = condition.apply(config);
580                        if (cond) {
581                                return Optional.of(this.build());
582                        }
583                        return Optional.empty();
584                }
585
586        }
587
588}
589
590final class RainbowGumHolder {
591
592        /*
593         * Serializes provisioning and changes to the supplier/router. Snapshot reads only
594         * need the volatile field; neither a read lock nor a stamp can pin a returned gum's
595         * lifetime after the read completes.
596         */
597        private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
598
599        private static Supplier<RainbowGum> supplier = RainbowGumServiceProvider::provide;
600
601        private static volatile @Nullable RainbowGum rainbowGum = null;
602
603        static final GlobalChangePublisher globalChangePublisher = new GlobalChangePublisher();
604
605        /*
606         * Lock free read for per event hot paths (see KeyValuesContributor.global()) where
607         * possibly seeing the previous gum while another one binds is harmless.
608         */
609        static @Nullable RainbowGum peek() {
610                return rainbowGum;
611        }
612
613        static @Nullable RainbowGum current() {
614                return rainbowGum;
615        }
616
617        /*
618         * Null when this thread holds the write lock, so is starting or removing a gum.
619         */
620        static @Nullable RainbowGum getOrNullIfStarting() {
621                lock.readLock().lock();
622                try {
623                        var r = rainbowGum;
624                        if (r != null) {
625                                return r;
626                        }
627                }
628                finally {
629                        lock.readLock().unlock();
630                }
631                if (lock.writeLock().isHeldByCurrentThread()) {
632                        return null;
633                }
634                lock.writeLock().lock();
635                try {
636                        var r = rainbowGum;
637                        if (r != null) {
638                                return r;
639                        }
640                        r = supplier.get();
641                        start(r);
642                        rainbowGum = r;
643                        globalChangePublisher.enqueue(Optional.of(r));
644                        return r;
645
646                }
647                finally {
648                        lock.writeLock().unlock();
649                        /*
650                         * Enqueue under the write lock to preserve binding order, but invoke user
651                         * callbacks only after releasing it. The publisher never holds its monitor
652                         * while calling a subscriber, including one that binds another gum.
653                         */
654                        drainGlobalChanges();
655                }
656
657        }
658
659        static boolean remove(@Nullable RainbowGum gum) {
660                lock.writeLock().lock();
661                try {
662                        var original = rainbowGum;
663                        if (gum != null && original != gum) {
664                                return false;
665                        }
666                        rainbowGum = null;
667                        supplier = RainbowGumServiceProvider::provide;
668                        if (original != null) {
669                                /*
670                                 * Unbound and queued before the global router is reset, so a failing
671                                 * output close still publishes the removal.
672                                 */
673                                globalChangePublisher.enqueue(Optional.empty());
674                                LogRouter.global().close();
675                        }
676                        return true;
677                }
678                finally {
679                        lock.writeLock().unlock();
680                        drainGlobalChanges();
681                }
682        }
683
684        static void set(Supplier<RainbowGum> rainbowGumSupplier) {
685                Objects.requireNonNull(rainbowGumSupplier);
686                if (lock.writeLock().isHeldByCurrentThread()) {
687                        throw new IllegalStateException("RainbowGum component tried to log too early. "
688                                        + "This is usually caused by dependencies calling logging.");
689                }
690                lock.writeLock().lock();
691                try {
692                        var original = rainbowGum;
693                        rainbowGum = null;
694                        supplier = rainbowGumSupplier;
695                        if (original != null) {
696                                globalChangePublisher.enqueue(Optional.empty());
697                        }
698                }
699                finally {
700                        lock.writeLock().unlock();
701                        drainGlobalChanges();
702                }
703        }
704
705        /*
706         * Component code can reenter remove() during startup or shutdown, so only the
707         * outermost lifecycle operation calls subscribers.
708         */
709        private static void drainGlobalChanges() {
710                if (!lock.writeLock().isHeldByCurrentThread()) {
711                        globalChangePublisher.drain();
712                }
713        }
714
715        private static void start(RainbowGum gum) {
716                Objects.requireNonNull(gum);
717                ShutdownManager.addShutdownHook(gum);
718                gum.start();
719                InternalRootRouter.setRouter(gum.router());
720        }
721
722}
723
724/*
725 * Unlike LogConfig.ChangePublisher/LogRouter.RouteChangePublisher (both scoped to a
726 * single RainbowGum/router instance and torn down with it) this is a JVM-lifetime static
727 * registry, so consumers are held only weakly - otherwise every SLF4J logger factory (or
728 * any other subscriber) ever created over the life of the JVM, e.g. across many
729 * short-lived RainbowGums in tests, would be pinned forever.
730 */
731final class GlobalChangePublisher {
732
733        private final Queue<WeakReference<Consumer<@Nullable RainbowGum>>> consumers = new ConcurrentLinkedQueue<>();
734
735        /*
736         * Both guarded by this. No callbacks may run while this monitor is held. Empty is a
737         * removal, as ArrayDeque does not allow null.
738         */
739        private final Queue<Optional<RainbowGum>> pending = new ArrayDeque<>();
740
741        private boolean draining;
742
743        void add(Consumer<@Nullable RainbowGum> consumer) {
744                Objects.requireNonNull(consumer);
745                consumers.add(new WeakReference<>(consumer));
746        }
747
748        synchronized void enqueue(Optional<RainbowGum> state) {
749                pending.add(state);
750        }
751
752        void drain() {
753                synchronized (this) {
754                        if (draining) {
755                                return;
756                        }
757                        draining = true;
758                }
759                try {
760                        while (true) {
761                                Optional<RainbowGum> state;
762                                synchronized (this) {
763                                        state = pending.poll();
764                                        if (state == null) {
765                                                draining = false;
766                                                return;
767                                        }
768                                }
769                                publish(state.orElse(null));
770                        }
771                }
772                catch (RuntimeException | Error e) {
773                        synchronized (this) {
774                                draining = false;
775                        }
776                        throw e;
777                }
778        }
779
780        private void publish(@Nullable RainbowGum gum) {
781                for (var it = consumers.iterator(); it.hasNext();) {
782                        var consumer = it.next().get();
783                        if (consumer == null) {
784                                it.remove();
785                        }
786                        else {
787                                try {
788                                        consumer.accept(gum);
789                                }
790                                catch (RuntimeException e) {
791                                        MetaLog.error(RainbowGum.class, "Global change subscriber threw", e);
792                                }
793                        }
794                }
795        }
796
797}
798
799final class SimpleRainbowGum implements RainbowGum, Shutdownable {
800
801        private final LogConfig config;
802
803        private final RootRouter router;
804
805        private final AtomicInteger state = new AtomicInteger(0);
806
807        private final UUID instanceId;
808
809        private static final int INIT = 0;
810
811        private static final int STARTED = 1;
812
813        private static final int CLOSED = 2;
814
815        public SimpleRainbowGum(LogConfig config, RootRouter router, UUID instanceId) {
816                super();
817                this.config = config;
818                this.router = router;
819                this.instanceId = instanceId;
820        }
821
822        @Override
823        public LogConfig config() {
824                return this.config;
825        }
826
827        @Override
828        public RootRouter router() {
829                return this.router;
830        }
831
832        @Override
833        public RainbowGum start() {
834                int current;
835                if ((current = state.compareAndExchange(INIT, STARTED)) == INIT) {
836                        return RainbowGum.super.start();
837                }
838                throw new IllegalStateException("Cannot start. This rainbowgum is " + stateLabel(current));
839        }
840
841        @Override
842        public UUID instanceId() {
843                return this.instanceId;
844        }
845
846        @Override
847        public void close() {
848                if (state.compareAndSet(STARTED, CLOSED)) {
849                        try {
850                                RainbowGumHolder.remove(this);
851                        }
852                        finally {
853                                try {
854                                        shutdown();
855                                }
856                                finally {
857                                        ShutdownManager.removeShutdownHook(this);
858                                }
859                        }
860                        return;
861                }
862        }
863
864        @Override
865        public void shutdown() {
866                /*
867                 * queued(config) borrows the global router. Only remove() resets it, as closing
868                 * it here could reset a replacement that is already bound.
869                 */
870                if (!(router instanceof GlobalLogRouter)) {
871                        router.close();
872                }
873                config().alerts().close();
874                config().serviceRegistry().close();
875        }
876
877        @Override
878        public String toString() {
879                return "SimpleRainbowGum [instanceId=" + instanceId + ", config=" + config + ", router=" + router + ", state="
880                                + stateLabel(state.get()) + "]";
881        }
882
883        private static String stateLabel(int state) {
884                return switch (state) {
885                        case INIT -> "created";
886                        case STARTED -> "started";
887                        case CLOSED -> "closed";
888                        default -> {
889                                throw new IllegalArgumentException("" + state);
890                        }
891                };
892        }
893
894}