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