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