001package io.jstach.rainbowgum;
002
003import java.lang.System.Logger.Level;
004import java.util.ArrayList;
005import java.util.Collection;
006import java.util.Collections;
007import java.util.EnumSet;
008import java.util.IdentityHashMap;
009import java.util.List;
010import java.util.Locale;
011import java.util.Objects;
012import java.util.Set;
013import java.util.concurrent.ConcurrentLinkedQueue;
014import java.util.concurrent.CopyOnWriteArrayList;
015import java.util.concurrent.locks.ReentrantLock;
016import java.util.function.Consumer;
017import java.util.function.Function;
018
019import org.jspecify.annotations.Nullable;
020
021import io.jstach.rainbowgum.LogConfig.ChangePublisher.ChangeType;
022import io.jstach.rainbowgum.LogPublisher.PublisherFactory;
023import io.jstach.rainbowgum.LogRouter.RootRouter;
024import io.jstach.rainbowgum.LogRouter.Route;
025import io.jstach.rainbowgum.LogRouter.Router;
026import io.jstach.rainbowgum.annotation.CaseChanging;
027
028/**
029 * Routes messages to a publisher by providing a {@link Route} from a logger name and
030 * level.
031 */
032public sealed interface LogRouter extends LogLifecycle {
033
034        /**
035         * Finds a route.
036         * @param loggerName topic.
037         * @param level level.
038         * @return route never <code>null</code>.
039         */
040        public Route route(String loggerName, java.lang.System.Logger.Level level);
041
042        /**
043         * Global router which is always available.
044         * @return global root router.
045         */
046        public static RootRouter global() {
047                return GlobalLogRouter.INSTANCE;
048        }
049
050        /**
051         * A route is similar to a SLF4J Logger or System Logger but has a much simpler
052         * contract.
053         *
054         * The proper usage of Route in most cases is to call {@link #isEnabled()} before
055         * calling {@link #log(LogEvent)}.
056         *
057         * That is {@link #log(LogEvent)} does not do any checking if the event is allowed
058         * furthermore by first checking if {@link #isEnabled()} is true one can decide
059         * whether or not to create a {@link LogEvent}.
060         */
061        public sealed interface Route extends LogEventLogger {
062
063                /**
064                 * Determines if {@link #log(LogEvent)} maybe called.
065                 * @return true if log can be called.
066                 */
067                public boolean isEnabled();
068
069                /**
070                 * Route singletons and utilities.
071                 */
072                @CaseChanging
073                public enum Routes implements Route {
074
075                        /**
076                         * A route that is a NOOP and is always disabled.
077                         */
078                        NotFound {
079                                @Override
080                                public void log(LogEvent event) {
081                                }
082
083                                @Override
084                                public boolean isEnabled() {
085                                        return false;
086                                }
087
088                        };
089
090                }
091
092        }
093
094        /**
095         * Router flags for adhoc router customization.
096         */
097        @CaseChanging
098        public enum RouteFlag {
099
100                /**
101                 * The route will not use the global level resolver and will only use the level
102                 * config directly on the router.
103                 */
104                IGNORE_GLOBAL_LEVEL_RESOLVER;
105
106                static Set<RouteFlag> parse(Collection<String> value) {
107                        if (value.isEmpty()) {
108                                return EnumSet.noneOf(RouteFlag.class);
109                        }
110                        var s = EnumSet.noneOf(RouteFlag.class);
111                        for (var v : value) {
112                                s.add(parse(v));
113                        }
114                        return s;
115                }
116
117                static RouteFlag parse(String value) {
118                        return LogProperty.enumValue(RouteFlag.class, value);
119                }
120
121        }
122
123        /**
124         * Root router is a router that has child routers.
125         */
126        sealed interface RootRouter extends LogRouter permits InternalRootRouter {
127
128                /**
129                 * The sink to dispatch events to for a logger name, with <strong>no level based
130                 * gating</strong> - unlike {@link #route(String, java.lang.System.Logger.Level)},
131                 * the returned {@link LogEventLogger} always accepts
132                 * {@link LogEventLogger#log(LogEvent)} regardless of what level is currently
133                 * enabled. Use this when level gating is already handled independently (e.g. a
134                 * logger facade caching its own resolved level and re-checking it on every call)
135                 * and a level would otherwise have to be made up just to obtain a working sink.
136                 * @param loggerName topic.
137                 * @return event logger, never <code>null</code>.
138                 */
139                public LogEventLogger eventLogger(String loggerName);
140
141                /**
142                 * Level resolver to find levels for a log name.
143                 * @return level resolver.
144                 */
145                public LevelResolver levelResolver();
146
147                /**
148                 * Register for a router changes usually involving the level resolver being
149                 * updated by config.
150                 * @param router consumer that will be called with the updated router.
151                 * @apiNote its best to use the router in the consumer accept argument and not
152                 * this router even if they are usually the same.
153                 */
154                public void onChange(Consumer<? super RootRouter> router);
155
156                /**
157                 * If a logger name configuration is changeable.
158                 * @param loggerName logger name.
159                 * @return true if config changes are allowed.
160                 */
161                public boolean isChangeable(String loggerName);
162
163        }
164
165        /**
166         * Router routes messages to a publisher.
167         */
168        sealed interface Router extends LogRouter, LogEventLogger {
169
170                /**
171                 * The router name given if no router is explicitly declared.
172                 */
173                public static final String DEFAULT_ROUTER_NAME = LogProperties.DEFAULT_NAME;
174
175                /**
176                 * Level resolver.
177                 * @return level resolver.
178                 */
179                LevelResolver levelResolver();
180
181                /**
182                 * Log publisher.
183                 * @return publisher.
184                 */
185                LogPublisher publisher();
186
187                /**
188                 * Whether or not the publisher is synchronous. A synchronous publisher generally
189                 * blocks.
190                 * @return true if the publisher is synchronous.
191                 */
192                default boolean synchronous() {
193                        return publisher().synchronous();
194                }
195
196                @Override
197                default void log(LogEvent event) {
198                        /*
199                         * Only async publishers need a frozen (immutable, defensively copied) event.
200                         * A synchronous publisher fully encodes and writes the event on the calling
201                         * thread before this call returns, so there is no other thread that could
202                         * observe or race with further mutation (e.g. MDC.put/remove) of the live
203                         * event data - freezing there would just be wasted allocation on the hot
204                         * path. Freeze is a noop if the event is already frozen.
205                         */
206                        if (!synchronous()) {
207                                event = event.freeze();
208                        }
209                        publisher().log(event);
210                }
211
212                /**
213                 * Creates a builder from config.
214                 * @param name name of the route which the publisher will inherit.
215                 * @param config config.
216                 * @return builder.
217                 */
218                static Builder builder(String name, LogConfig config) {
219                        /*
220                         * Eagerly triggers LogProperties' own key-parameter-value validation now:
221                         * needed because a fully-explicit route (explicit appenders, level,
222                         * publisher) never triggers a single keyed property lookup on its own, so
223                         * without this a malformed name would otherwise never be validated. Wrapped
224                         * into a ValidationException the same way a generated builder's constructor
225                         * is, so a bad name reports the same exception type regardless of whether it
226                         * came from logging.routes (RainbowGum.Builder's own validateNames call) or a
227                         * direct programmatic builder(name, config) call like this one.
228                         */
229                        try {
230                                LogProperties.interpolateNamedKey(LogProperties.ROUTE_LEVEL_PREFIX, name);
231                        }
232                        catch (IllegalArgumentException e) {
233                                throw LogProperty.ValidationException.of(LogRouter.class, e);
234                        }
235                        return new Builder(name, config);
236                }
237
238                /**
239                 * Creates a router.
240                 *
241                 * @see AbstractRouter
242                 */
243                @FunctionalInterface
244                public interface RouterFactory {
245
246                        /**
247                         * Builder will call this factory if passed in.
248                         * @param publisher publisher from builder.
249                         * @param levelResolver level resolver from builder.
250                         * @param name name from builder.
251                         * @param config config from builder.
252                         * @return router.
253                         */
254                        public Router create(LogPublisher publisher, LevelResolver levelResolver, String name, LogConfig config);
255
256                        /**
257                         * Creates a router from a function.
258                         * @param function <strong>the function may return <code>null</code></strong>
259                         * which indicates to ignore.
260                         * @return factory.
261                         */
262                        static RouterFactory of(@SuppressWarnings("exports") Function<LogEvent, @Nullable LogEvent> function) {
263                                return (pub, lr, n, c) -> new AbstractRouter(n, pub, lr) {
264                                        @Override
265                                        protected @Nullable LogEvent transformOrNull(LogEvent event) {
266                                                return function.apply(event);
267                                        }
268                                };
269                        }
270
271                }
272
273                /**
274                 * Router builder.
275                 */
276                public final class Builder extends LevelResolver.AbstractBuilder<Builder> implements LogConfig.ConfigSupport {
277
278                        private final LogConfig config;
279
280                        private final String name;
281
282                        private final EnumSet<RouteFlag> flags = EnumSet.noneOf(RouteFlag.class);
283
284                        private RouterFactory factory = (publisher, levelResolver, n, c) -> new SimpleRouter(n, publisher,
285                                        levelResolver);
286
287                        private List<LogProvider<LogAppender>> appenders = new ArrayList<>();
288
289                        private Builder(String name, LogConfig config) {
290                                this.name = Objects.requireNonNull(name);
291                                this.config = config;
292                        }
293
294                        private @Nullable PublisherFactory publisher;
295
296                        @Override
297                        protected Builder self() {
298                                return this;
299                        }
300
301                        /**
302                         * Gets the currently bound config.
303                         * @return config.
304                         */
305                        @Override
306                        public LogConfig config() {
307                                return this.config;
308                        }
309
310                        /**
311                         * Adds a flag.
312                         * @param flag flag added if not already added.
313                         * @return this.
314                         */
315                        public Builder flag(RouteFlag flag) {
316                                flags.add(flag);
317                                return this;
318                        }
319
320                        /**
321                         * Adds an appender by giving an appender builder to a consumer.
322                         * @param name appender name.
323                         * @param consumer consumer.
324                         * @return this builder.
325                         */
326                        public Builder appender(String name, Consumer<LogAppender.Builder> consumer) {
327                                var builder = LogAppender.builder(name);
328                                consumer.accept(builder);
329                                this.appenders.add(builder.build());
330                                return this;
331                        }
332
333                        /**
334                         * Adds an appender.
335                         * @param appender appender provider.
336                         * @return this builder.
337                         */
338                        public Builder appender(LogProvider<LogAppender> appender) {
339                                this.appenders.add(Objects.requireNonNull(appender));
340                                return self();
341                        }
342
343                        /**
344                         * Sets the publisher. Only one publisher to router.
345                         * @param publisher publisher.
346                         * @return this builder.
347                         */
348                        public Builder publisher(PublisherFactory publisher) {
349                                this.publisher = publisher;
350                                return self();
351                        }
352
353                        /**
354                         * Factory to use for creating the router.
355                         * @param factory router factory.
356                         * @return this.
357                         */
358                        public Builder factory(RouterFactory factory) {
359                                this.factory = Objects.requireNonNull(factory);
360                                return this;
361                        }
362
363                        Router build() {
364                                return build(this.factory);
365                        }
366
367                        /**
368                         * Builds the router.
369                         * @param factory to create the router.
370                         * @return router.
371                         */
372                        Router build(RouterFactory factory) {
373                                String name = this.name;
374                                flags.addAll(rawValue(config.properties()
375                                        .forKey(LogProperties.ROUTE_FLAGS_PROPERTY, name)
376                                        .ofList()
377                                        .map(RouteFlag::parse)
378                                        .or(EnumSet.noneOf(RouteFlag.class))).value());
379                                String routerLevelPrefix = LogProperties.interpolateNamedKey(LogProperties.ROUTE_LEVEL_PREFIX, name);
380
381                                /*
382                                 * The route's own level config is resolved in isolation from the global
383                                 * level resolver so that a route level override takes priority per logger
384                                 * name instead of merging with global by prefix specificity (a more
385                                 * specific global override would otherwise win over a coarser route
386                                 * override even though the route is meant to take precedence). If the
387                                 * route has no opinion at all for a given name (resolves to Level.ALL)
388                                 * resolution falls through to the global resolver for that name, unless
389                                 * IGNORE_GLOBAL_LEVEL_RESOLVER is set in which case global is never
390                                 * consulted.
391                                 */
392                                var routeLevelResolverBuilder = LevelResolver.builder();
393                                var currentConfig = buildLevelConfigOrNull();
394                                if (currentConfig != null) {
395                                        routeLevelResolverBuilder.config(currentConfig);
396                                }
397                                routeLevelResolverBuilder.config(config.properties(), routerLevelPrefix);
398                                /*
399                                 * config.levelResolver() (the global resolver) has no caching of its own
400                                 * - the previous single merged CompositeLevelConfig relied on being
401                                 * wrapped in one outer cache for the whole route+global composition.
402                                 * LevelResolver.Builder.build() restores that guarantee (level resolvers
403                                 * are cached / static, see LevelResolver's class documentation) for the
404                                 * combined route+global resolver below.
405                                 */
406                                if (!flags.contains(RouteFlag.IGNORE_GLOBAL_LEVEL_RESOLVER)) {
407                                        routeLevelResolverBuilder.fallback(config.levelResolver());
408                                }
409                                var levelResolver = routeLevelResolverBuilder.build().provide(name, config);
410
411                                /*
412                                 * The root level might not be set by the route or globally, even when the
413                                 * route sets levels for some loggers.
414                                 */
415                                var level = levelResolver.resolveLevel("");
416                                Objects.requireNonNull(level);
417                                if (level == System.Logger.Level.ALL) {
418                                        /*
419                                         * Neither the route nor the global level resolver have a root level.
420                                         * Without this fallback loggers they have no opinion on would be ALL,
421                                         * which the builders and other places turn into TRACE.
422                                         */
423                                        levelResolver = LevelResolver.builder()
424                                                .resolver(levelResolver)
425                                                .fallback(StaticLevelResolver.INFO)
426                                                .build()
427                                                .provide(name, config);
428                                }
429
430                                var _levelResolver = levelResolver;
431                                /*
432                                 * This routers level resolver needs to be notified if config changes.
433                                 */
434                                config.changePublisher().subscribe(c -> {
435                                        _levelResolver.clear();
436                                });
437                                var publisher = this.publisher;
438
439                                List<LogProvider<LogAppender>> appenders = new ArrayList<>(this.appenders);
440                                if (appenders.isEmpty()) {
441                                        DefaultAppenderRegistry.appenders(config, name) //
442                                                .stream() //
443                                                .forEach(appenders::add);
444                                }
445
446                                if (publisher == null) {
447                                        var properties = config.properties();
448                                        publisher = rawValue(properties.forKey(LogProperties.ROUTE_PUBLISHER_PROPERTY, name)
449                                                .ofProviderRef()
450                                                .map(r -> config.publisherRegistry().provide(r))
451                                                .or(() -> LogPublisher.SyncLogPublisher.builder().build())).value();
452                                }
453
454                                var apps = new LogAppender.Appenders(name, config, appenders);
455                                var pub = publisher.create(name, config, apps);
456                                /*
457                                 * Register the publisher and its appenders for lookup like status checks
458                                 * (see LogReporter). Registering "apps" itself rather than its resolved
459                                 * list lets a lookup happen after the fact regardless of whether the
460                                 * publisher called asList()/asSingle() eagerly here or lazily later, and
461                                 * Appenders.resolvedOrNull() returns null if the publisher never calls
462                                 * either (meaning it does not use the appenders this ordinary way at
463                                 * all).
464                                 */
465                                config.serviceRegistry().put(LogPublisher.class, name, pub);
466                                config.serviceRegistry().put(LogAppender.Appenders.class, name, apps);
467                                return factory.create(pub, levelResolver, name, config);
468                        }
469
470                        /*
471                         * Both flags and publisher above already have a fallback applied (Missing is
472                         * impossible), but a present-but-malformed value must still surface its raw,
473                         * unwrapped exception - not a "Validation failed for Builder:" wrapping,
474                         * which would also change the exception's public type away from
475                         * PropertyConvertException (see ConfigFailureTest's bogus-scheme publisher
476                         * case). LogProperty.Result no longer exposes value() itself (a bare Result
477                         * might still be Missing), so this just narrows to
478                         * LogProperty.ValidatedResult - true for every concrete Result - without
479                         * going through a Validator at all.
480                         */
481                        private static <T> LogProperty.ValidatedResult<T> rawValue(LogProperty.Result<T> result) {
482                                return switch (result) {
483                                        case LogProperty.ValidatedResult<T> vr -> vr;
484                                };
485                        }
486
487                }
488
489        }
490
491        /**
492         * A user supplied router usually for filtering or transforming purposes. The method
493         * required to implement is {@link #transformOrNull(LogEvent)}.
494         *
495         * @see RouterFactory
496         */
497        @SuppressWarnings("javadoc") // TODO Eclipse bug.
498        public non-sealed abstract class AbstractRouter implements Router, Route {
499
500                private final String name;
501
502                private final LogPublisher publisher;
503
504                private final LevelResolver levelResolver;
505
506                /**
507                 * Router tuple of publisher and resolver.
508                 * @param name router name not <code>null</code>.
509                 * @param publisher not <code>null</code>.
510                 * @param levelResolver not <code>null</code>.
511                 */
512                protected AbstractRouter(String name, LogPublisher publisher, LevelResolver levelResolver) {
513                        super();
514                        this.name = name;
515                        this.publisher = publisher;
516                        this.levelResolver = levelResolver;
517                }
518
519                @Override
520                public final boolean synchronous() {
521                        return Router.super.synchronous();
522                }
523
524                @Override
525                public final void log(LogEvent event) {
526                        var e = transformOrNull(event);
527                        if (e != null) {
528                                Router.super.log(e);
529                        }
530                }
531
532                /**
533                 * Implement for custom filtering or transformation.
534                 * @param event usually from logging facade.
535                 * @return event to be pushed to logger or not if <code>null</code>.
536                 */
537                protected abstract @Nullable LogEvent transformOrNull(LogEvent event);
538
539                @Override
540                public final Route route(String loggerName, Level level) {
541                        if (levelResolver().isEnabled(loggerName, level)) {
542                                return this;
543                        }
544                        return Route.Routes.NotFound;
545                }
546
547                @Override
548                public final void start(LogConfig config) {
549                        publisher.start(config);
550                }
551
552                @Override
553                public final void close() {
554                        publisher.close();
555
556                }
557
558                @Override
559                public final boolean isEnabled() {
560                        return true;
561                }
562
563                @Override
564                public final LevelResolver levelResolver() {
565                        return this.levelResolver;
566                }
567
568                @Override
569                public final LogPublisher publisher() {
570                        return this.publisher;
571                }
572
573                @Override
574                public String toString() {
575                        return getClass() + "[name=" + name + ", publisher=" + publisher + ", levelResolver=" + levelResolver + "]";
576                }
577
578        }
579
580}
581
582record SimpleRouter(String name, LogPublisher publisher, LevelResolver levelResolver) implements Router, Route {
583
584        @Override
585        public void close() {
586                publisher.close();
587        }
588
589        @Override
590        public Route route(String loggerName, java.lang.System.Logger.Level level) {
591                if (levelResolver().isEnabled(loggerName, level)) {
592                        return this;
593                }
594                return Route.Routes.NotFound;
595        }
596
597        @Override
598        public boolean isEnabled() {
599                return true;
600        }
601
602        @Override
603        public void start(LogConfig config) {
604                publisher.start(config);
605        }
606
607}
608
609sealed interface InternalRootRouter extends RootRouter {
610
611        /**
612         * Sets the root router.
613         * @param router root router.
614         */
615        static void setRouter(RootRouter router) {
616                if (GlobalLogRouter.INSTANCE == router) {
617                        return;
618                }
619                GlobalLogRouter.INSTANCE.drain((InternalRootRouter) router);
620        }
621
622        static InternalRootRouter of(List<? extends Router> routes, LogConfig config) {
623
624                if (routes.isEmpty()) {
625                        throw new IllegalArgumentException("atleast one route is required");
626                }
627                List<Router> sorted = new ArrayList<>();
628                /*
629                 * We add the async routers first
630                 */
631                Set<Router> matched = Collections.newSetFromMap(new IdentityHashMap<Router, Boolean>());
632                for (var r : routes) {
633                        if (!r.synchronous()) {
634                                if (matched.add(r)) {
635                                        sorted.add(r);
636                                }
637                        }
638                }
639                for (var r : routes) {
640                        if (r.synchronous()) {
641                                if (matched.add(r)) {
642                                        sorted.add(r);
643                                }
644                        }
645                }
646                routes = sorted;
647                LevelResolver.Builder resolverBuilder = LevelResolver.builder();
648                routes.stream().map(Router::levelResolver).forEach(resolverBuilder::resolver);
649                var globalLevelResolver = resolverBuilder.build().provide("", config);
650
651                Router[] array = routes.toArray(new Router[] {});
652
653                RouteChangePublisher changePublisher = new RouteChangePublisher(
654                                s -> config.changePublisher().allowedChanges(s).contains(ChangeType.LEVEL));
655                if (array.length == 1) {
656                        var r = array[0];
657                        return new SingleRootRouter(r, changePublisher);
658                }
659                return new CompositeLogRouter(array, globalLevelResolver, changePublisher);
660        }
661
662        default boolean isEnabled(String loggerName, java.lang.System.Logger.Level level) {
663                return route(loggerName, level).isEnabled();
664
665        }
666
667        default void drain(InternalRootRouter delegate) {
668
669        }
670
671        @Override
672        default void onChange(Consumer<? super RootRouter> router) {
673                changePublisher().add(router);
674        }
675
676        public RouteChangePublisher changePublisher();
677
678        @Override
679        default boolean isChangeable(String loggerName) {
680                return changePublisher().loggerChangeable.apply(loggerName);
681        }
682
683        final class RouteChangePublisher {
684
685                private final Collection<Consumer<? super RootRouter>> consumers = new CopyOnWriteArrayList<Consumer<? super RootRouter>>();
686
687                private final Function<String, Boolean> loggerChangeable;
688
689                RouteChangePublisher(Function<String, Boolean> loggerChangeable) {
690                        super();
691                        this.loggerChangeable = loggerChangeable;
692                }
693
694                void add(Consumer<? super RootRouter> consumer) {
695                        consumers.add(consumer);
696                }
697
698                void publish(RootRouter router) {
699                        for (var c : consumers) {
700                                c.accept(router);
701                        }
702                }
703
704                void transferTo(RouteChangePublisher changePublisher) {
705                        changePublisher.consumers.addAll(consumers);
706                        this.consumers.clear();
707                }
708
709        }
710
711}
712
713record SingleRootRouter(Router router, RouteChangePublisher changePublisher) implements InternalRootRouter {
714
715        @Override
716        public void start(LogConfig config) {
717                router.start(config);
718        }
719
720        @Override
721        public void close() {
722                router.close();
723        }
724
725        @Override
726        public LevelResolver levelResolver() {
727                return router.levelResolver();
728        }
729
730        @Override
731        public Route route(String loggerName, Level level) {
732                return router.route(loggerName, level);
733        }
734
735        @Override
736        public LogEventLogger eventLogger(String loggerName) {
737                return router;
738        }
739
740}
741
742@SuppressWarnings("ArrayRecordComponent") // TODO revisit perf
743record CompositeLogRouter(Router[] routers, LevelResolver levelResolver,
744                RouteChangePublisher changePublisher) implements InternalRootRouter, Route {
745
746        @Override
747        public Route route(String loggerName, Level level) {
748                /*
749                 * The assumption is the provided level resolver is a cached composite level
750                 * resolver of all the routers.
751                 */
752                if (levelResolver.isEnabled(loggerName, level)) {
753                        return this;
754                }
755                return Routes.NotFound;
756        }
757
758        @Override
759        public LogEventLogger eventLogger(String loggerName) {
760                /*
761                 * log(LogEvent) below already fans out to every child router regardless of how it
762                 * was reached, so this composite is already its own sink - no per-child selection
763                 * happens until log() actually runs.
764                 */
765                return this;
766        }
767
768        @Override
769        public void log(LogEvent event) {
770                for (var router : routers) {
771                        var route = router.route(event.loggerName(), event.level());
772                        if (route.isEnabled()) {
773                                /*
774                                 * We assume async routers are earlier in the array.
775                                 */
776                                if (!router.synchronous()) {
777                                        /*
778                                         * Now all events are frozen from here onward to guarantee that the
779                                         * synchronous routers see the same thing as the async routers.
780                                         *
781                                         * Freeze is a noop if it already frozen.
782                                         */
783                                        event = event.freeze();
784                                }
785                                route.log(event);
786                        }
787                }
788        }
789
790        @Override
791        public boolean isEnabled() {
792                return true;
793        }
794
795        @Override
796        public void start(LogConfig config) {
797                for (var r : routers) {
798                        r.start(config);
799                }
800        }
801
802        @Override
803        public void close() {
804                for (var r : routers) {
805                        r.close();
806                }
807        }
808}
809
810final class QueueEventsRouter implements InternalRootRouter, Route {
811
812        private final ConcurrentLinkedQueue<LogEvent> events = new ConcurrentLinkedQueue<>();
813
814        private final LevelResolver levelResolver;
815
816        private final LevelResolver errorLevelResolver;
817
818        private final RouteChangePublisher changePublisher = new RouteChangePublisher(s -> true);
819
820        static QueueEventsRouter of() {
821                var validator = LogProperty.Validator.of(QueueEventsRouter.class);
822                var queueLevel = queueLevel().validate(validator);
823                var errorLevel = errorLevel().validate(validator);
824                validator.validate();
825                return new QueueEventsRouter(StaticLevelResolver.of(queueLevel.value()),
826                                StaticLevelResolver.of(errorLevel.value()));
827        }
828
829        private static LogProperty.Result<Level> queueLevel() {
830                var properties = LogProperties.StandardProperties.SYSTEM_PROPERTIES;
831                return properties.forKey(LogProperties.GLOBAL_QUEUE_LEVEL_PROPERTY)
832                        .ofString()
833                        .map(LevelResolver::parseLevel)
834                        .or(Level.INFO);
835        }
836
837        private static final LogProperty.Result<Level> errorLevel() {
838                var properties = LogProperties.StandardProperties.SYSTEM_PROPERTIES;
839                return properties.forKey(LogProperties.GLOBAL_QUEUE_ERROR_PROPERTY)
840                        .ofString()
841                        .map(LevelResolver::parseLevel)
842                        .or(Level.ERROR);
843        }
844
845        private QueueEventsRouter(LevelResolver levelResolver, LevelResolver errorLevelResolver) {
846                super();
847                this.levelResolver = levelResolver;
848                this.errorLevelResolver = errorLevelResolver;
849        }
850
851        @Override
852        public LevelResolver levelResolver() {
853                return levelResolver;
854        }
855
856        @Override
857        public Route route(String loggerName, Level level) {
858                if (levelResolver.isEnabled(loggerName, level)) {
859                        return this;
860                }
861                return Routes.NotFound;
862        }
863
864        @Override
865        public LogEventLogger eventLogger(String loggerName) {
866                return this;
867        }
868
869        @Override
870        public void start(LogConfig config) {
871        }
872
873        @Override
874        public void close() {
875                events.clear();
876        }
877
878        @Override
879        public void log(LogEvent event) {
880                events.add(event);
881                if (errorLevelResolver.isEnabled(event.loggerName(), event.level())) {
882                        MetaLog.error(event);
883                }
884        }
885
886        @Override
887        public void drain(InternalRootRouter delegate) {
888                LogEvent e;
889                while ((e = this.events.poll()) != null) {
890                        delegate.route(e.loggerName(), e.level()).log(e);
891                }
892        }
893
894        @Override
895        public boolean isEnabled() {
896                return true;
897        }
898
899        @Override
900        public RouteChangePublisher changePublisher() {
901                return this.changePublisher;
902        }
903
904}
905
906@SuppressWarnings("ImmutableEnumChecker")
907enum GlobalLogRouter implements InternalRootRouter, Route {
908
909        INSTANCE;
910
911        private volatile InternalRootRouter delegate = QueueEventsRouter.of();
912
913        private final ReentrantLock drainLock = new ReentrantLock();
914
915        @Override
916        public LevelResolver levelResolver() {
917                return delegate.levelResolver();
918        }
919
920        @Override
921        public boolean isEnabled() {
922                return true;
923        }
924
925        @Override
926        public void log(LogEvent event) {
927                InternalRootRouter d = delegate;
928                if (d instanceof QueueEventsRouter q) {
929                        q.log(event);
930                }
931                else {
932                        var route = d.route(event.loggerName(), event.level());
933                        if (route.isEnabled()) {
934                                route.log(event);
935                        }
936                }
937        }
938
939        @Override
940        public Route route(String loggerName, Level level) {
941                return this.delegate.route(loggerName, level);
942        }
943
944        @Override
945        public LogEventLogger eventLogger(String loggerName) {
946                return this.delegate.eventLogger(loggerName);
947        }
948
949        @Override
950        public boolean isEnabled(String loggerName, Level level) {
951                return this.delegate.isEnabled(loggerName, level);
952        }
953
954        @Override
955        public RouteChangePublisher changePublisher() {
956                return this.delegate.changePublisher();
957        }
958
959        @Override
960        public void drain(InternalRootRouter delegate) {
961                _drain(delegate);
962        }
963
964        private InternalRootRouter _drain(InternalRootRouter delegate) {
965                if (delegate == this) {
966                        throw new IllegalStateException("bug");
967                }
968                drainLock.lock();
969                try {
970                        var original = this.delegate;
971                        if (original instanceof QueueEventsRouter q) {
972                                q.changePublisher().transferTo(delegate.changePublisher());
973                                delegate.changePublisher().publish(delegate);
974                        }
975                        this.delegate = Objects.requireNonNull(delegate);
976                        original.drain(delegate);
977                        return original;
978                }
979                finally {
980                        drainLock.unlock();
981                }
982        }
983
984        @Override
985        public void start(LogConfig config) {
986        }
987
988        @Override
989        public void close() {
990                var d = _drain(QueueEventsRouter.of());
991                d.close();
992        }
993
994}