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