001package io.jstach.rainbowgum;
002
003import java.lang.ref.WeakReference;
004import java.util.ArrayList;
005import java.util.List;
006import java.util.Objects;
007import java.util.Optional;
008import java.util.Queue;
009import java.util.ServiceLoader;
010import java.util.UUID;
011import java.util.concurrent.ConcurrentLinkedQueue;
012import java.util.concurrent.atomic.AtomicInteger;
013import java.util.concurrent.locks.ReentrantReadWriteLock;
014import java.util.function.Consumer;
015import java.util.function.Function;
016import java.util.function.Supplier;
017
018import org.jspecify.annotations.Nullable;
019
020import io.jstach.rainbowgum.LogRouter.RootRouter;
021import io.jstach.rainbowgum.LogRouter.Router;
022import io.jstach.rainbowgum.spi.RainbowGumServiceProvider;
023
024//@formatter:off
025/**
026 * The main entry point and configuration of RainbowGum logging.
027 * <p>
028 * RainbowGum logging loads configuration through the service loader. While you can
029 * manually set RainbowGum using {@link #set(Supplier)} it is better to register
030 * implementations through the ServiceLoader so that RainbowGum will load prior to any
031 * external logging.
032 * <p>
033 * To register a custom RainbowGum:
034 *
035 *
036{@snippet class="snippets.RainbowGumProviderExample" region="provider" :
037
038class RainbowGumProviderExample implements RainbowGumProvider {
039
040        @Override
041        public Optional<RainbowGum> provide(LogConfig config) {
042
043                Integer bufferSize = config.properties() //
044                        .forKey("logging.custom.async.bufferSize")
045                        .ofInt()
046                        .or(1024)
047                        .validateNow(RainbowGumProviderExample.class);
048
049                LogProvider<LogOutput> output = (name, cfg) -> cfg.properties()
050                        .forKey("logging.custom.output")
051                        .ofProvider(LogOutput::of)
052                        .or(LogOutput.ofStandardOut())
053                        .validateNow(RainbowGumProviderExample.class)
054                        .provide(name, cfg);
055
056                var gum = RainbowGum.builder() //
057                        .route(r -> {
058                                r.publisher(PublisherFactory //
059                                        .async() //
060                                        .bufferSize(bufferSize) //
061                                        .build());
062                                r.appender("console", a -> {
063                                        a.output(output);
064                                });
065                                r.level(Level.INFO);
066                        })
067                        .build();
068
069                return Optional.of(gum);
070        }
071
072}
073}
074 * <p>
075 * <strong>Initialization order</strong> - most of the actual work happens in
076 * {@link LogConfig.Builder#build()}, {@link LogRouter.Router.Builder}, and
077 * {@code LogAppenderRegistry}; this is the exact sequence for anyone debugging a
078 * configuration or writing a {@link RainbowGumServiceProvider.RainbowGumProvider}:
079 * <ol>
080 * <li>{@link LogConfig.Builder#build()} builds the shared {@link LogConfig}:
081 * <ol>
082 * <li>Resolve the {@link ServiceRegistry} (default: a new empty one) and
083 * {@link LogProperties} - manually added
084 * {@link RainbowGumServiceProvider.PropertiesProvider}s, else (if a
085 * {@link java.util.ServiceLoader} was supplied) SPI-discovered ones, with system
086 * properties always layered in as a final fallback.</li>
087 * <li>Build {@link LogAlerts} and {@link LogMetrics} from properties.</li>
088 * <li>Build the global level resolver ({@code logging.level.*} and
089 * {@code logging.group.*}, plus anything added directly on the builder).</li>
090 * <li>Construct the {@link LogConfig} itself - this is also where a few
091 * process-wide flags get latched (config change support, the appender lock, and
092 * thread-local-disabled globals) and the default output/encoder/publisher
093 * registries are created.</li>
094 * <li>Run every {@link RainbowGumServiceProvider.Configurator} (manually added,
095 * then SPI-discovered), highest {@linkplain RainbowGumServiceProvider.Configurator#priority()
096 * priority} first. A configurator returning {@code false} is retried in a later
097 * pass (see {@link RainbowGumServiceProvider#PASSES}) - useful if it depends on
098 * another configurator running first.</li>
099 * <li>Start {@link LogAlerts} - deliberately last, so a configurator's own alert
100 * listener is already registered before alerts decides whether an unobserved
101 * error should refuse startup.</li>
102 * </ol>
103 * </li>
104 * <li>{@link Builder#build()} builds each named {@link LogRouter.Router} (a single
105 * route named {@value LogProperties#DEFAULT_NAME}, unless {@code logging.routes}
106 * names several) via {@link LogRouter.Router.Builder}'s own (package-private)
107 * {@code build()}:
108 * <ol>
109 * <li>Resolve route flags ({@code logging.route.<name>.flags}).</li>
110 * <li>Build a route-scoped level resolver: anything configured directly on the
111 * route builder, layered over {@code logging.route.<name>.level.*}, falling back
112 * to the global level resolver from step 1 (skipped entirely if the route flag
113 * {@code IGNORE_GLOBAL_LEVEL_RESOLVER} is set) - and finally to {@code INFO} if
114 * nothing at all resolves a level.</li>
115 * <li>Resolve appenders - only if none were added programmatically on the route
116 * builder (that check is all-or-nothing: adding even one appender in code skips
117 * property-based resolution entirely). See <strong>Appender resolution</strong>
118 * below.</li>
119 * <li>Resolve the publisher - {@code logging.route.<name>.publisher}, falling
120 * back to a synchronous publisher if unset.</li>
121 * <li>Hand the resolved appenders to the publisher and register the publisher in
122 * the {@link ServiceRegistry}.</li>
123 * </ol>
124 * </li>
125 * <li>{@link #start()} starts the root router, which starts each route's
126 * publisher, which starts each of its appenders, which starts its output (e.g.
127 * opening a file).</li>
128 * </ol>
129 * <strong>Appender resolution</strong> - the part most often mistaken for a bug,
130 * since no appenders are ever added programmatically in the common case:
131 * <ol>
132 * <li>{@code logging.route.<name>.appenders} names the route's appenders; for the
133 * {@value LogProperties#DEFAULT_NAME} route only, a missing value falls back to
134 * the unprefixed {@code logging.appenders}.</li>
135 * <li>If that is <em>still</em> missing: for the default route only, the list
136 * defaults to {@code [file, console]} if {@code logging.file.name} (or Spring
137 * Boot's convention) is set, otherwise just {@code [console]}. A non-default
138 * route with no appenders configured anywhere is a startup failure, not a silent
139 * no-op.</li>
140 * <li>Each named appender then resolves independently. {@code file} and
141 * {@code console} are the two built-in special cases, each with its own output
142 * property/fallback ({@code logging.file.name} then
143 * {@code logging.appender.file.output} for {@code file};
144 * {@code logging.appender.console.output} falling back to standard-out for
145 * {@code console}). Any other name is fully generic:
146 * {@code logging.appender.<name>.output}/{@code .encoder}/{@code .flags}/
147 * {@code .type}, with the output's own URI scheme choosing which registered
148 * {@link LogOutput.OutputProvider} builds it.</li>
149 * <li>An encoder is only looked up if the resolved output does not already
150 * implement {@link LogEncoder} itself (rare, e.g. some structured outputs);
151 * otherwise it is used as-is and the {@code .encoder} property is never
152 * consulted.</li>
153 * </ol>
154 */
155//@formatter:on
156@SuppressWarnings("InvalidInlineTag")
157public sealed interface RainbowGum extends AutoCloseable, LogEventLogger {
158
159        /**
160         * Gets the currently statically bound RainbowGum and will try to load and find one if
161         * there is none currently bound. <strong> This is a blocking operation as in locks
162         * are used and will block indefinitely till a gum has loaded. </strong> If that is
163         * not desired see {@link #getOrNull()}.
164         * @return current RainbowGum or new loaded one.
165         */
166        public static RainbowGum of() {
167                return RainbowGumHolder.get();
168        }
169
170        /**
171         * Gets the currently statically bound RainbowGum or <code>null</code> if none are
172         * <strong>finished binding</strong>. Unlike {@link #of()} this will never load or
173         * start an instance but rather just gets the currently bound one. It will also never
174         * block and does not wait if one is currently being loaded.
175         * @return current RainbowGum or <code>null</code>
176         */
177        public static @Nullable RainbowGum getOrNull() {
178                return RainbowGumHolder.current();
179        }
180
181        /**
182         * Provides the service loader default based RainbowGum.
183         * @return RainbowGum.
184         */
185        public static RainbowGum defaults() {
186                return RainbowGumServiceProvider.provide();
187        }
188
189        /**
190         * Creates a RainbowGum that will <strong>ALWAYS</strong> use the global router.
191         * @param config config.
192         * @return rainbow gum that always uses global router.
193         */
194        public static RainbowGum queued(LogConfig config) {
195                return new SimpleRainbowGum(config, LogRouter.global(), UUID.randomUUID());
196        }
197
198        /**
199         * Sets the global default RainbowGum. The supplied {@link RainbowGum} must not
200         * already be started.
201         * @param supplier the supplier will be memoized when {@linkplain Supplier#get()
202         * accessed} and {@link RainbowGum#start()} will be called.
203         */
204        public static void set(Supplier<RainbowGum> supplier) {
205                RainbowGumHolder.set(supplier);
206        }
207
208        /**
209         * Subscribes to notification of a new RainbowGum becoming the globally bound one (see
210         * {@link #set(Supplier)}, {@link #set(RainbowGum)}, {@link Builder#set()}). This is
211         * primarily for components like SLF4J's logger factory that are themselves
212         * bootstrapped once (often before "the real" RainbowGum has loaded, e.g. a
213         * framework's own pre-boot sequence) and otherwise have no way of finding out that a
214         * different RainbowGum has since replaced the one they captured.
215         * <p>
216         * Only a {@linkplain java.lang.ref.WeakReference weak reference} to
217         * <code>consumer</code> is retained, so subscribing does not by itself keep the
218         * consumer (or whatever it is attached to) reachable - the caller is responsible for
219         * keeping a strong reference to <code>consumer</code> for as long as it needs to keep
220         * receiving notifications, otherwise it may silently stop being called once garbage
221         * collected.
222         * @param consumer called with the newly bound global RainbowGum. Never called while
223         * any RainbowGum internal lock is held.
224         */
225        public static void onGlobalChange(Consumer<RainbowGum> consumer) {
226                RainbowGumHolder.globalChangePublisher.add(consumer);
227        }
228
229        /**
230         * Sets a Rainbow Gum and provides a supplier that will start it globally.
231         * @param gum that will be set immediatly.
232         * @return supplier that will set, get and start the supplied gum and if it is not the
233         * same will throw {@link IllegalStateException}.
234         */
235        public static Supplier<? extends RainbowGum> set(RainbowGum gum) {
236                UUID instanceId = gum.instanceId();
237                RainbowGum.set(() -> gum);
238                return () -> {
239                        var of = RainbowGum.of();
240                        /*
241                         * TODO this is a hack. The holder lock should be used to make this not
242                         * happen.
243                         */
244                        if (!instanceId.equals(of.instanceId())) {
245                                throw new IllegalStateException("Another rainbow gum registered itself as the global. "
246                                                + "This is rare reace condition and probably a bug");
247                        }
248                        return of;
249                };
250        }
251
252        /**
253         * The config associated with this instance.
254         * @return config.
255         */
256        public LogConfig config();
257
258        /**
259         * The router that will route log messages to publishers.
260         * @return router
261         */
262        public RootRouter router();
263
264        /**
265         * Starts the rainbow gum and returns it. It is returned for try-with usage
266         * convenience.
267         * @return the started RainbowGum.
268         */
269        default RainbowGum start() {
270                router().start(config());
271                return this;
272        }
273
274        /**
275         * Unique id of rainbow gum instance.
276         * @return random id created on creation.
277         */
278        public UUID instanceId();
279
280        /**
281         * Will close the RainbowGum and all registered components as well as removed from the
282         * shutdown hooks. If the rainbow gum is set as global it will no longer be global and
283         * replaced with the bootstrapping in memory queue. {@inheritDoc}
284         */
285        @Override
286        public void close();
287
288        /**
289         * This append call is mainly for testing as it does not avoid making events that do
290         * not need to be made if no logging needs to be done. {@inheritDoc}
291         */
292        @Override
293        default void log(LogEvent event) {
294                var r = router().route(event.loggerName(), event.level());
295                if (r.isEnabled()) {
296                        r.log(event);
297                }
298        }
299
300        /**
301         * Use to build a custom {@link RainbowGum} which will use the {@link LogConfig}
302         * provided by the service loader.
303         * @return builder.
304         */
305        public static Builder builder() {
306                ServiceLoader<RainbowGumServiceProvider> loader = ServiceLoader.load(RainbowGumServiceProvider.class);
307                var config = RainbowGumServiceProvider.provideConfig(loader);
308                return builder(config);
309        }
310
311        /**
312         * Use to build a custom {@link RainbowGum} with supplied config.
313         * @param config the config
314         * @return builder.
315         * @see #builder()
316         */
317        public static Builder builder(LogConfig config) {
318                return new Builder(config);
319        }
320
321        /**
322         * Use to build a custom {@link RainbowGum} with supplied config.
323         * @param config consumer that has first argument as config builder.
324         * @return builder.
325         * @see #builder()
326         * @apiNote this method is for ergonomic fluent reasons.
327         */
328        public static Builder builder(Consumer<? super LogConfig.Builder> config) {
329                var b = LogConfig.builder();
330                config.accept(b);
331                return builder(b.build());
332        }
333
334        /**
335         * RainbowGum Builder.
336         */
337        public class Builder {
338
339                private final LogConfig config;
340
341                private final List<Router> routes = new ArrayList<>();
342
343                private Builder(LogConfig config) {
344                        this.config = config;
345                }
346
347                /**
348                 * Adds a router.
349                 * @param route a router.
350                 * @return builder.
351                 */
352                public Builder route(Router route) {
353                        this.routes.add(route);
354                        return this;
355                }
356
357                /**
358                 * Adds a route by using a consumer of the route builder.
359                 * @param name name of router.
360                 * @param consumer consumer is passed router builder. The consumer does not need
361                 * to call {@link Router#builder(String,LogConfig)}
362                 * @return builder.
363                 * @see io.jstach.rainbowgum.LogRouter.Router.Builder
364                 */
365                public Builder route(String name, Consumer<Router.Builder> consumer) {
366                        var builder = Router.builder(name, config);
367                        consumer.accept(builder);
368                        return route(builder.build());
369                }
370
371                /**
372                 * Adds a route by using a consumer of the route builder.
373                 * @param consumer consumer is passed router builder. The consumer does not need
374                 * to call {@link Router#builder(String,LogConfig)}
375                 * @return builder.
376                 * @see io.jstach.rainbowgum.LogRouter.Router.Builder
377                 */
378                public Builder route(Consumer<Router.Builder> consumer) {
379                        var builder = Router.builder(Router.DEFAULT_ROUTER_NAME, config);
380                        consumer.accept(builder);
381                        return route(builder.build());
382                }
383
384                /**
385                 * Builds an un-started {@link RainbowGum}.
386                 * @return an un-started {@link RainbowGum}.
387                 */
388                public RainbowGum build() {
389                        return build(UUID.randomUUID());
390                }
391
392                /**
393                 * Builds an un-started {@link RainbowGum}.
394                 * @param instanceId unique id for rainbow gum instance.
395                 * @return an un-started {@link RainbowGum}.
396                 */
397                private RainbowGum build(UUID instanceId) {
398                        var routes = this.routes;
399                        var config = this.config;
400                        if (routes.isEmpty()) {
401                                var routeNamesResult = config.properties() //
402                                        .forKey(LogProperties.ROUTES_PROPERTY)
403                                        .ofList()
404                                        .or(List.of());
405                                List<String> routeNames = routeNamesResult.validateNow(Builder.class);
406                                /*
407                                 * Validated as a plain list of names, before any of them is used to build
408                                 * a Router, so a bad name is reported against this list property itself,
409                                 * not deferred until some unrelated {name}-keyed property (e.g. this
410                                 * route's own level) happens to interpolate it first.
411                                 */
412                                LogProperty.Validator.validateNames(LogRouter.class, "route", routeNamesResult, routeNames);
413                                if (routeNames.isEmpty()) {
414                                        routes = List.of(Router.builder(Router.DEFAULT_ROUTER_NAME, config).build());
415                                }
416                                else {
417                                        routes = routeNames.stream().map(n -> Router.builder(n, config).build()).toList();
418                                }
419                        }
420                        var root = InternalRootRouter.of(routes, config);
421                        config.changePublisher().subscribe(c -> {
422                                root.changePublisher().publish(root);
423                        });
424                        return new SimpleRainbowGum(config, root, instanceId);
425                }
426
427                /**
428                 * Builds, starts and sets the RainbowGum as the global one picked up by logging
429                 * facades.
430                 * @return started and set rainbow that can be used in a try-close.
431                 */
432                public RainbowGum set() {
433                        UUID instanceId = UUID.randomUUID();
434                        RainbowGum.set(() -> build(instanceId));
435                        var gum = RainbowGum.of();
436                        /*
437                         * TODO this is a hack. The holder lock should be used to make this not
438                         * happen.
439                         */
440                        if (!instanceId.equals(gum.instanceId())) {
441                                throw new IllegalStateException("Another rainbow gum registered itself as the global. "
442                                                + "This is rare reace condition and probably a bug");
443                        }
444                        return gum;
445                }
446
447                /**
448                 * Removes the currently set Rainbow Gum which will close it if one exists and
449                 * will be replaced by the default provision process if {@link RainbowGum#of()} is
450                 * called before {@link #set()}.
451                 * @apiNote This is largely an internal detail for unit testing the provision
452                 * process.
453                 */
454                public void unset() {
455                        RainbowGumHolder.remove(null);
456                }
457
458                /**
459                 * For returning an optional for the LogProvider contract.
460                 * @return optional that always has a rainbow gum.
461                 * @apiNote this method is for ergonomics.
462                 */
463                public Optional<RainbowGum> optional() {
464                        return Optional.of(this.build());
465                }
466
467                /**
468                 * For returning an optional for the LogProvider contract.
469                 * @param condition condition to check if this rainbow gum should be used.
470                 * @return optional rainbow gum.
471                 * @apiNote this method is for ergonomics.
472                 */
473                public Optional<RainbowGum> optional(Function<? super LogConfig, Boolean> condition) {
474                        var cond = condition.apply(config);
475                        if (cond) {
476                                return Optional.of(this.build());
477                        }
478                        return Optional.empty();
479                }
480
481        }
482
483}
484
485final class RainbowGumHolder {
486
487        /*
488         * TODO perhaps a StampedLock would be better performance wise.
489         */
490        private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
491
492        private static Supplier<RainbowGum> supplier = RainbowGumServiceProvider::provide;
493
494        private static volatile @Nullable RainbowGum rainbowGum = null;
495
496        static final GlobalChangePublisher globalChangePublisher = new GlobalChangePublisher();
497
498        static @Nullable RainbowGum current() {
499                if (lock.readLock().tryLock()) {
500                        try {
501                                return rainbowGum;
502                        }
503                        finally {
504                                lock.readLock().unlock();
505                        }
506                }
507                return null;
508        }
509
510        static RainbowGum get() {
511                lock.readLock().lock();
512                try {
513                        var r = rainbowGum;
514                        if (r != null) {
515                                return r;
516                        }
517                }
518                finally {
519                        lock.readLock().unlock();
520                }
521                if (lock.writeLock().isHeldByCurrentThread()) {
522                        throw new IllegalStateException("RainbowGum component tried to log too early. "
523                                        + "This is usually caused by dependencies calling logging.");
524                }
525                RainbowGum newlyStarted = null;
526                lock.writeLock().lock();
527                try {
528                        var r = rainbowGum;
529                        if (r != null) {
530                                return r;
531                        }
532                        r = supplier.get();
533                        start(r);
534                        rainbowGum = r;
535                        newlyStarted = r;
536                        return r;
537
538                }
539                finally {
540                        lock.writeLock().unlock();
541                        /*
542                         * Published outside of the write lock so a subscriber can never deadlock or
543                         * trip the "tried to log too early" reentrancy guard above by, say, trying to
544                         * read a volatile field it owns - it is not calling back into this class.
545                         */
546                        if (newlyStarted != null) {
547                                globalChangePublisher.publish(newlyStarted);
548                        }
549                }
550
551        }
552
553        static boolean remove(@Nullable RainbowGum gum) {
554                lock.writeLock().lock();
555                try {
556                        var original = rainbowGum;
557                        if (gum != null) {
558                                if (original != gum) {
559                                        return false;
560                                }
561                        }
562                        /*
563                         * Reset the global router
564                         */
565                        if (original != null) {
566                                LogRouter.global().close();
567                        }
568                        rainbowGum = null;
569                        supplier = RainbowGumServiceProvider::provide;
570                        return true;
571                }
572                finally {
573                        lock.writeLock().unlock();
574                }
575        }
576
577        static void set(Supplier<RainbowGum> rainbowGumSupplier) {
578                Objects.requireNonNull(rainbowGumSupplier);
579                if (lock.writeLock().isHeldByCurrentThread()) {
580                        throw new IllegalStateException("RainbowGum component tried to log too early. "
581                                        + "This is usually caused by dependencies calling logging.");
582                }
583                lock.writeLock().lock();
584                try {
585                        rainbowGum = null;
586                        supplier = rainbowGumSupplier;
587                }
588                finally {
589                        lock.writeLock().unlock();
590                }
591        }
592
593        private static void start(RainbowGum gum) {
594                Objects.requireNonNull(gum);
595                ShutdownManager.addShutdownHook(gum);
596                gum.start();
597                InternalRootRouter.setRouter(gum.router());
598        }
599
600}
601
602/*
603 * Unlike LogConfig.ChangePublisher/LogRouter.RouteChangePublisher (both scoped to a
604 * single RainbowGum/router instance and torn down with it) this is a JVM-lifetime static
605 * registry, so consumers are held only weakly - otherwise every SLF4J logger factory (or
606 * any other subscriber) ever created over the life of the JVM, e.g. across many
607 * short-lived RainbowGums in tests, would be pinned forever.
608 */
609final class GlobalChangePublisher {
610
611        private final Queue<WeakReference<Consumer<RainbowGum>>> consumers = new ConcurrentLinkedQueue<>();
612
613        void add(Consumer<RainbowGum> consumer) {
614                consumers.add(new WeakReference<>(consumer));
615        }
616
617        void publish(RainbowGum gum) {
618                for (var it = consumers.iterator(); it.hasNext();) {
619                        var consumer = it.next().get();
620                        if (consumer == null) {
621                                it.remove();
622                        }
623                        else {
624                                consumer.accept(gum);
625                        }
626                }
627        }
628
629}
630
631final class SimpleRainbowGum implements RainbowGum, Shutdownable {
632
633        private final LogConfig config;
634
635        private final RootRouter router;
636
637        private final AtomicInteger state = new AtomicInteger(0);
638
639        private final UUID instanceId;
640
641        private static final int INIT = 0;
642
643        private static final int STARTED = 1;
644
645        private static final int CLOSED = 2;
646
647        public SimpleRainbowGum(LogConfig config, RootRouter router, UUID instanceId) {
648                super();
649                this.config = config;
650                this.router = router;
651                this.instanceId = instanceId;
652        }
653
654        @Override
655        public LogConfig config() {
656                return this.config;
657        }
658
659        @Override
660        public RootRouter router() {
661                return this.router;
662        }
663
664        @Override
665        public RainbowGum start() {
666                int current;
667                if ((current = state.compareAndExchange(INIT, STARTED)) == INIT) {
668                        return RainbowGum.super.start();
669                }
670                throw new IllegalStateException("Cannot start. This rainbowgum is " + stateLabel(current));
671        }
672
673        @Override
674        public UUID instanceId() {
675                return this.instanceId;
676        }
677
678        @Override
679        public void close() {
680                if (state.compareAndSet(STARTED, CLOSED)) {
681                        RainbowGumHolder.remove(this);
682                        try {
683                                shutdown();
684                        }
685                        finally {
686                                ShutdownManager.removeShutdownHook(this);
687                        }
688                        return;
689                }
690        }
691
692        @Override
693        public void shutdown() {
694                router().close();
695                config().alerts().close();
696                config().serviceRegistry().close();
697        }
698
699        @Override
700        public String toString() {
701                return "SimpleRainbowGum [instanceId=" + instanceId + ", config=" + config + ", router=" + router + ", state="
702                                + stateLabel(state.get()) + "]";
703        }
704
705        private static String stateLabel(int state) {
706                return switch (state) {
707                        case INIT -> "created";
708                        case STARTED -> "started";
709                        case CLOSED -> "closed";
710                        default -> {
711                                throw new IllegalArgumentException("" + state);
712                        }
713                };
714        }
715
716}