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