LogRouter.java

package io.jstach.rainbowgum;

import java.lang.System.Logger.Level;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.function.Function;

import org.jspecify.annotations.Nullable;

import io.jstach.rainbowgum.LogConfig.ChangePublisher.ChangeType;
import io.jstach.rainbowgum.LogPublisher.PublisherFactory;
import io.jstach.rainbowgum.LogRouter.RootRouter;
import io.jstach.rainbowgum.LogRouter.Route;
import io.jstach.rainbowgum.LogRouter.Router;
import io.jstach.rainbowgum.annotation.CaseChanging;

/**
 * Routes messages to a publisher by providing a {@link Route} from a logger name and
 * level.
 */
public sealed interface LogRouter extends LogLifecycle {

	/**
	 * Finds a route.
	 * @param loggerName topic.
	 * @param level level.
	 * @return route never <code>null</code>.
	 */
	public Route route(String loggerName, java.lang.System.Logger.Level level);

	/**
	 * Global router which is always available.
	 * @return global root router.
	 */
	public static RootRouter global() {
		return GlobalLogRouter.INSTANCE;
	}

	/**
	 * A route is similar to a SLF4J Logger or System Logger but has a much simpler
	 * contract.
	 *
	 * The proper usage of Route in most cases is to call {@link #isEnabled()} before
	 * calling {@link #log(LogEvent)}.
	 *
	 * That is {@link #log(LogEvent)} does not do any checking if the event is allowed
	 * furthermore by first checking if {@link #isEnabled()} is true one can decide
	 * whether or not to create a {@link LogEvent}.
	 */
	public sealed interface Route extends LogEventLogger {

		/**
		 * Determines if {@link #log(LogEvent)} maybe called.
		 * @return true if log can be called.
		 */
		public boolean isEnabled();

		/**
		 * Route singletons and utilities.
		 */
		@CaseChanging
		public enum Routes implements Route {

			/**
			 * A route that is a NOOP and is always disabled.
			 */
			NotFound {
				@Override
				public void log(LogEvent event) {
				}

				@Override
				public boolean isEnabled() {
					return false;
				}

			};

		}

	}

	/**
	 * Router flags for adhoc router customization.
	 */
	@CaseChanging
	public enum RouteFlag {

		/**
		 * The route will not use the global level resolver and will only use the level
		 * config directly on the router.
		 */
		IGNORE_GLOBAL_LEVEL_RESOLVER;

		static Set<RouteFlag> parse(Collection<String> value) {
			if (value.isEmpty()) {
				return EnumSet.noneOf(RouteFlag.class);
			}
			var s = EnumSet.noneOf(RouteFlag.class);
			for (var v : value) {
				s.add(parse(v));
			}
			return s;
		}

		static RouteFlag parse(String value) {
			return LogProperty.enumValue(RouteFlag.class, value);
		}

	}

	/**
	 * Root router is a router that has child routers.
	 */
	sealed interface RootRouter extends LogRouter permits InternalRootRouter {

		/**
		 * The sink to dispatch events to for a logger name, with <strong>no level based
		 * gating</strong> - unlike {@link #route(String, java.lang.System.Logger.Level)},
		 * the returned {@link LogEventLogger} always accepts
		 * {@link LogEventLogger#log(LogEvent)} regardless of what level is currently
		 * enabled. Use this when level gating is already handled independently (e.g. a
		 * logger facade caching its own resolved level and re-checking it on every call)
		 * and a level would otherwise have to be made up just to obtain a working sink.
		 * @param loggerName topic.
		 * @return event logger, never <code>null</code>.
		 */
		public LogEventLogger eventLogger(String loggerName);

		/**
		 * Level resolver to find levels for a log name.
		 * @return level resolver.
		 */
		public LevelResolver levelResolver();

		/**
		 * Register for a router changes usually involving the level resolver being
		 * updated by config.
		 * @param router consumer that will be called with the updated router.
		 * @apiNote its best to use the router in the consumer accept argument and not
		 * this router even if they are usually the same.
		 */
		public void onChange(Consumer<? super RootRouter> router);

		/**
		 * If a logger name configuration is changeable.
		 * @param loggerName logger name.
		 * @return true if config changes are allowed.
		 */
		public boolean isChangeable(String loggerName);

	}

	/**
	 * Router routes messages to a publisher.
	 */
	sealed interface Router extends LogRouter, LogEventLogger {

		/**
		 * The router name given if no router is explicitly declared.
		 */
		public static final String DEFAULT_ROUTER_NAME = LogProperties.DEFAULT_NAME;

		/**
		 * Level resolver.
		 * @return level resolver.
		 */
		LevelResolver levelResolver();

		/**
		 * Log publisher.
		 * @return publisher.
		 */
		LogPublisher publisher();

		/**
		 * Whether or not the publisher is synchronous. A synchronous publisher generally
		 * blocks.
		 * @return true if the publisher is synchronous.
		 */
		default boolean synchronous() {
			return publisher().synchronous();
		}

		@Override
		default void log(LogEvent event) {
			/*
			 * Only async publishers need a frozen (immutable, defensively copied) event.
			 * A synchronous publisher fully encodes and writes the event on the calling
			 * thread before this call returns, so there is no other thread that could
			 * observe or race with further mutation (e.g. MDC.put/remove) of the live
			 * event data - freezing there would just be wasted allocation on the hot
			 * path. Freeze is a noop if the event is already frozen.
			 */
			if (!synchronous()) {
				event = event.freeze();
			}
			publisher().log(event);
		}

		/**
		 * Creates a builder from config.
		 * @param name name of the route which the publisher will inherit.
		 * @param config config.
		 * @return builder.
		 */
		static Builder builder(String name, LogConfig config) {
			/*
			 * Eagerly triggers LogProperties' own key-parameter-value validation now:
			 * needed because a fully-explicit route (explicit appenders, level,
			 * publisher) never triggers a single keyed property lookup on its own, so
			 * without this a malformed name would otherwise never be validated. Wrapped
			 * into a ValidationException the same way a generated builder's constructor
			 * is, so a bad name reports the same exception type regardless of whether it
			 * came from logging.routes (RainbowGum.Builder's own validateNames call) or a
			 * direct programmatic builder(name, config) call like this one.
			 */
			try {
				LogProperties.interpolateNamedKey(LogProperties.ROUTE_LEVEL_PREFIX, name);
			}
			catch (IllegalArgumentException e) {
				throw LogProperty.ValidationException.of(LogRouter.class, e);
			}
			return new Builder(name, config);
		}

		/**
		 * Creates a router.
		 *
		 * @see AbstractRouter
		 */
		@FunctionalInterface
		public interface RouterFactory {

			/**
			 * Builder will call this factory if passed in.
			 * @param publisher publisher from builder.
			 * @param levelResolver level resolver from builder.
			 * @param name name from builder.
			 * @param config config from builder.
			 * @return router.
			 */
			public Router create(LogPublisher publisher, LevelResolver levelResolver, String name, LogConfig config);

			/**
			 * Creates a router from a function.
			 * @param function <strong>the function may return <code>null</code></strong>
			 * which indicates to ignore.
			 * @return factory.
			 */
			static RouterFactory of(@SuppressWarnings("exports") Function<LogEvent, @Nullable LogEvent> function) {
				return (pub, lr, n, c) -> new AbstractRouter(n, pub, lr) {
					@Override
					protected @Nullable LogEvent transformOrNull(LogEvent event) {
						return function.apply(event);
					}
				};
			}

		}

		/**
		 * Router builder.
		 */
		public final class Builder extends LevelResolver.AbstractBuilder<Builder> implements LogConfig.ConfigSupport {

			private final LogConfig config;

			private final String name;

			private final EnumSet<RouteFlag> flags = EnumSet.noneOf(RouteFlag.class);

			private RouterFactory factory = (publisher, levelResolver, n, c) -> new SimpleRouter(n, publisher,
					levelResolver);

			private List<LogProvider<LogAppender>> appenders = new ArrayList<>();

			private Builder(String name, LogConfig config) {
				this.name = Objects.requireNonNull(name);
				this.config = config;
			}

			private @Nullable PublisherFactory publisher;

			@Override
			protected Builder self() {
				return this;
			}

			/**
			 * Gets the currently bound config.
			 * @return config.
			 */
			@Override
			public LogConfig config() {
				return this.config;
			}

			/**
			 * Adds a flag.
			 * @param flag flag added if not already added.
			 * @return this.
			 */
			public Builder flag(RouteFlag flag) {
				flags.add(flag);
				return this;
			}

			/**
			 * Adds an appender by giving an appender builder to a consumer.
			 * @param name appender name.
			 * @param consumer consumer.
			 * @return this builder.
			 */
			public Builder appender(String name, Consumer<LogAppender.Builder> consumer) {
				var builder = LogAppender.builder(name);
				consumer.accept(builder);
				this.appenders.add(builder.build());
				return this;
			}

			/**
			 * Adds an appender.
			 * @param appender appender provider.
			 * @return this builder.
			 */
			public Builder appender(LogProvider<LogAppender> appender) {
				this.appenders.add(Objects.requireNonNull(appender));
				return self();
			}

			/**
			 * Sets the publisher. Only one publisher to router.
			 * @param publisher publisher.
			 * @return this builder.
			 */
			public Builder publisher(PublisherFactory publisher) {
				this.publisher = publisher;
				return self();
			}

			/**
			 * Factory to use for creating the router.
			 * @param factory router factory.
			 * @return this.
			 */
			public Builder factory(RouterFactory factory) {
				this.factory = Objects.requireNonNull(factory);
				return this;
			}

			Router build() {
				return build(this.factory);
			}

			/**
			 * Builds the router.
			 * @param factory to create the router.
			 * @return router.
			 */
			Router build(RouterFactory factory) {
				String name = this.name;
				flags.addAll(rawValue(config.properties()
					.forKey(LogProperties.ROUTE_FLAGS_PROPERTY, name)
					.ofList()
					.map(RouteFlag::parse)
					.or(EnumSet.noneOf(RouteFlag.class))).value());
				String routerLevelPrefix = LogProperties.interpolateNamedKey(LogProperties.ROUTE_LEVEL_PREFIX, name);

				/*
				 * The route's own level config is resolved in isolation from the global
				 * level resolver so that a route level override takes priority per logger
				 * name instead of merging with global by prefix specificity (a more
				 * specific global override would otherwise win over a coarser route
				 * override even though the route is meant to take precedence). If the
				 * route has no opinion at all for a given name (resolves to Level.ALL)
				 * resolution falls through to the global resolver for that name, unless
				 * IGNORE_GLOBAL_LEVEL_RESOLVER is set in which case global is never
				 * consulted.
				 */
				var routeLevelResolverBuilder = LevelResolver.builder();
				var currentConfig = buildLevelConfigOrNull();
				if (currentConfig != null) {
					routeLevelResolverBuilder.config(currentConfig);
				}
				routeLevelResolverBuilder.config(config.properties(), routerLevelPrefix);
				/*
				 * config.levelResolver() (the global resolver) has no caching of its own
				 * - the previous single merged CompositeLevelConfig relied on being
				 * wrapped in one outer cache for the whole route+global composition.
				 * LevelResolver.Builder.build() restores that guarantee (level resolvers
				 * are cached / static, see LevelResolver's class documentation) for the
				 * combined route+global resolver below.
				 */
				if (!flags.contains(RouteFlag.IGNORE_GLOBAL_LEVEL_RESOLVER)) {
					routeLevelResolverBuilder.fallback(config.levelResolver());
				}
				var levelResolver = routeLevelResolverBuilder.build().provide(name, config);

				/*
				 * The root level might not be set by the route or globally, even when the
				 * route sets levels for some loggers.
				 */
				var level = levelResolver.resolveLevel("");
				Objects.requireNonNull(level);
				if (level == System.Logger.Level.ALL) {
					/*
					 * Neither the route nor the global level resolver have a root level.
					 * Without this fallback loggers they have no opinion on would be ALL,
					 * which the builders and other places turn into TRACE.
					 */
					levelResolver = LevelResolver.builder()
						.resolver(levelResolver)
						.fallback(StaticLevelResolver.INFO)
						.build()
						.provide(name, config);
				}

				var _levelResolver = levelResolver;
				/*
				 * This routers level resolver needs to be notified if config changes.
				 */
				config.changePublisher().subscribe(c -> {
					_levelResolver.clear();
				});
				var publisher = this.publisher;

				List<LogProvider<LogAppender>> appenders = new ArrayList<>(this.appenders);
				if (appenders.isEmpty()) {
					DefaultAppenderRegistry.appenders(config, name) //
						.stream() //
						.forEach(appenders::add);
				}

				if (publisher == null) {
					var properties = config.properties();
					publisher = rawValue(properties.forKey(LogProperties.ROUTE_PUBLISHER_PROPERTY, name)
						.ofProviderRef()
						.map(r -> config.publisherRegistry().provide(r))
						.or(() -> LogPublisher.SyncLogPublisher.builder().build())).value();
				}

				var apps = new LogAppender.Appenders(name, config, appenders);
				var pub = publisher.create(name, config, apps);
				/*
				 * Register the publisher and its appenders for lookup like status checks
				 * (see LogReporter). Registering "apps" itself rather than its resolved
				 * list lets a lookup happen after the fact regardless of whether the
				 * publisher called asList()/asSingle() eagerly here or lazily later, and
				 * Appenders.resolvedOrNull() returns null if the publisher never calls
				 * either (meaning it does not use the appenders this ordinary way at
				 * all).
				 */
				config.serviceRegistry().put(LogPublisher.class, name, pub);
				config.serviceRegistry().put(LogAppender.Appenders.class, name, apps);
				return factory.create(pub, levelResolver, name, config);
			}

			/*
			 * Both flags and publisher above already have a fallback applied (Missing is
			 * impossible), but a present-but-malformed value must still surface its raw,
			 * unwrapped exception - not a "Validation failed for Builder:" wrapping,
			 * which would also change the exception's public type away from
			 * PropertyConvertException (see ConfigFailureTest's bogus-scheme publisher
			 * case). LogProperty.Result no longer exposes value() itself (a bare Result
			 * might still be Missing), so this just narrows to
			 * LogProperty.ValidatedResult - true for every concrete Result - without
			 * going through a Validator at all.
			 */
			private static <T> LogProperty.ValidatedResult<T> rawValue(LogProperty.Result<T> result) {
				return switch (result) {
					case LogProperty.ValidatedResult<T> vr -> vr;
				};
			}

		}

	}

	/**
	 * A user supplied router usually for filtering or transforming purposes. The method
	 * required to implement is {@link #transformOrNull(LogEvent)}.
	 *
	 * @see RouterFactory
	 */
	@SuppressWarnings("javadoc") // TODO Eclipse bug.
	public non-sealed abstract class AbstractRouter implements Router, Route {

		private final String name;

		private final LogPublisher publisher;

		private final LevelResolver levelResolver;

		/**
		 * Router tuple of publisher and resolver.
		 * @param name router name not <code>null</code>.
		 * @param publisher not <code>null</code>.
		 * @param levelResolver not <code>null</code>.
		 */
		protected AbstractRouter(String name, LogPublisher publisher, LevelResolver levelResolver) {
			super();
			this.name = name;
			this.publisher = publisher;
			this.levelResolver = levelResolver;
		}

		@Override
		public final boolean synchronous() {
			return Router.super.synchronous();
		}

		@Override
		public final void log(LogEvent event) {
			var e = transformOrNull(event);
			if (e != null) {
				Router.super.log(e);
			}
		}

		/**
		 * Implement for custom filtering or transformation.
		 * @param event usually from logging facade.
		 * @return event to be pushed to logger or not if <code>null</code>.
		 */
		protected abstract @Nullable LogEvent transformOrNull(LogEvent event);

		@Override
		public final Route route(String loggerName, Level level) {
			if (levelResolver().isEnabled(loggerName, level)) {
				return this;
			}
			return Route.Routes.NotFound;
		}

		@Override
		public final void start(LogConfig config) {
			publisher.start(config);
		}

		@Override
		public final void close() {
			publisher.close();

		}

		@Override
		public final boolean isEnabled() {
			return true;
		}

		@Override
		public final LevelResolver levelResolver() {
			return this.levelResolver;
		}

		@Override
		public final LogPublisher publisher() {
			return this.publisher;
		}

		@Override
		public String toString() {
			return getClass() + "[name=" + name + ", publisher=" + publisher + ", levelResolver=" + levelResolver + "]";
		}

	}

}

record SimpleRouter(String name, LogPublisher publisher, LevelResolver levelResolver) implements Router, Route {

	@Override
	public void close() {
		publisher.close();
	}

	@Override
	public Route route(String loggerName, java.lang.System.Logger.Level level) {
		if (levelResolver().isEnabled(loggerName, level)) {
			return this;
		}
		return Route.Routes.NotFound;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}

	@Override
	public void start(LogConfig config) {
		publisher.start(config);
	}

}

sealed interface InternalRootRouter extends RootRouter {

	/**
	 * Sets the root router.
	 * @param router root router.
	 */
	static void setRouter(RootRouter router) {
		if (GlobalLogRouter.INSTANCE == router) {
			return;
		}
		GlobalLogRouter.INSTANCE.drain((InternalRootRouter) router);
	}

	static InternalRootRouter of(List<? extends Router> routes, LogConfig config) {

		if (routes.isEmpty()) {
			throw new IllegalArgumentException("atleast one route is required");
		}
		List<Router> sorted = new ArrayList<>();
		/*
		 * We add the async routers first
		 */
		Set<Router> matched = Collections.newSetFromMap(new IdentityHashMap<Router, Boolean>());
		for (var r : routes) {
			if (!r.synchronous()) {
				if (matched.add(r)) {
					sorted.add(r);
				}
			}
		}
		for (var r : routes) {
			if (r.synchronous()) {
				if (matched.add(r)) {
					sorted.add(r);
				}
			}
		}
		routes = sorted;
		LevelResolver.Builder resolverBuilder = LevelResolver.builder();
		routes.stream().map(Router::levelResolver).forEach(resolverBuilder::resolver);
		var globalLevelResolver = resolverBuilder.build().provide("", config);

		Router[] array = routes.toArray(new Router[] {});

		RouteChangePublisher changePublisher = new RouteChangePublisher(
				s -> config.changePublisher().allowedChanges(s).contains(ChangeType.LEVEL));
		if (array.length == 1) {
			var r = array[0];
			return new SingleRootRouter(r, changePublisher);
		}
		return new CompositeLogRouter(array, globalLevelResolver, changePublisher);
	}

	default boolean isEnabled(String loggerName, java.lang.System.Logger.Level level) {
		return route(loggerName, level).isEnabled();

	}

	default void drain(InternalRootRouter delegate) {

	}

	@Override
	default void onChange(Consumer<? super RootRouter> router) {
		changePublisher().add(router);
	}

	public RouteChangePublisher changePublisher();

	@Override
	default boolean isChangeable(String loggerName) {
		return changePublisher().loggerChangeable.apply(loggerName);
	}

	final class RouteChangePublisher {

		private final Collection<Consumer<? super RootRouter>> consumers = new CopyOnWriteArrayList<Consumer<? super RootRouter>>();

		private final Function<String, Boolean> loggerChangeable;

		RouteChangePublisher(Function<String, Boolean> loggerChangeable) {
			super();
			this.loggerChangeable = loggerChangeable;
		}

		void add(Consumer<? super RootRouter> consumer) {
			consumers.add(consumer);
		}

		void publish(RootRouter router) {
			for (var c : consumers) {
				c.accept(router);
			}
		}

		void transferTo(RouteChangePublisher changePublisher) {
			changePublisher.consumers.addAll(consumers);
			this.consumers.clear();
		}

	}

}

record SingleRootRouter(Router router, RouteChangePublisher changePublisher) implements InternalRootRouter {

	@Override
	public void start(LogConfig config) {
		router.start(config);
	}

	@Override
	public void close() {
		router.close();
	}

	@Override
	public LevelResolver levelResolver() {
		return router.levelResolver();
	}

	@Override
	public Route route(String loggerName, Level level) {
		return router.route(loggerName, level);
	}

	@Override
	public LogEventLogger eventLogger(String loggerName) {
		return router;
	}

}

@SuppressWarnings("ArrayRecordComponent") // TODO revisit perf
record CompositeLogRouter(Router[] routers, LevelResolver levelResolver,
		RouteChangePublisher changePublisher) implements InternalRootRouter, Route {

	@Override
	public Route route(String loggerName, Level level) {
		/*
		 * The assumption is the provided level resolver is a cached composite level
		 * resolver of all the routers.
		 */
		if (levelResolver.isEnabled(loggerName, level)) {
			return this;
		}
		return Routes.NotFound;
	}

	@Override
	public LogEventLogger eventLogger(String loggerName) {
		/*
		 * log(LogEvent) below already fans out to every child router regardless of how it
		 * was reached, so this composite is already its own sink - no per-child selection
		 * happens until log() actually runs.
		 */
		return this;
	}

	@Override
	public void log(LogEvent event) {
		for (var router : routers) {
			var route = router.route(event.loggerName(), event.level());
			if (route.isEnabled()) {
				/*
				 * We assume async routers are earlier in the array.
				 */
				if (!router.synchronous()) {
					/*
					 * Now all events are frozen from here onward to guarantee that the
					 * synchronous routers see the same thing as the async routers.
					 *
					 * Freeze is a noop if it already frozen.
					 */
					event = event.freeze();
				}
				route.log(event);
			}
		}
	}

	@Override
	public boolean isEnabled() {
		return true;
	}

	@Override
	public void start(LogConfig config) {
		for (var r : routers) {
			r.start(config);
		}
	}

	@Override
	public void close() {
		for (var r : routers) {
			r.close();
		}
	}
}

final class QueueEventsRouter implements InternalRootRouter, Route {

	private final ConcurrentLinkedQueue<LogEvent> events = new ConcurrentLinkedQueue<>();

	private final LevelResolver levelResolver;

	private final LevelResolver errorLevelResolver;

	private final RouteChangePublisher changePublisher = new RouteChangePublisher(s -> true);

	static QueueEventsRouter of() {
		var validator = LogProperty.Validator.of(QueueEventsRouter.class);
		var queueLevel = queueLevel().validate(validator);
		var errorLevel = errorLevel().validate(validator);
		validator.validate();
		return new QueueEventsRouter(StaticLevelResolver.of(queueLevel.value()),
				StaticLevelResolver.of(errorLevel.value()));
	}

	private static LogProperty.Result<Level> queueLevel() {
		var properties = LogProperties.StandardProperties.SYSTEM_PROPERTIES;
		return properties.forKey(LogProperties.GLOBAL_QUEUE_LEVEL_PROPERTY)
			.ofString()
			.map(LevelResolver::parseLevel)
			.or(Level.INFO);
	}

	private static final LogProperty.Result<Level> errorLevel() {
		var properties = LogProperties.StandardProperties.SYSTEM_PROPERTIES;
		return properties.forKey(LogProperties.GLOBAL_QUEUE_ERROR_PROPERTY)
			.ofString()
			.map(LevelResolver::parseLevel)
			.or(Level.ERROR);
	}

	private QueueEventsRouter(LevelResolver levelResolver, LevelResolver errorLevelResolver) {
		super();
		this.levelResolver = levelResolver;
		this.errorLevelResolver = errorLevelResolver;
	}

	@Override
	public LevelResolver levelResolver() {
		return levelResolver;
	}

	@Override
	public Route route(String loggerName, Level level) {
		if (levelResolver.isEnabled(loggerName, level)) {
			return this;
		}
		return Routes.NotFound;
	}

	@Override
	public LogEventLogger eventLogger(String loggerName) {
		return this;
	}

	@Override
	public void start(LogConfig config) {
	}

	@Override
	public void close() {
		events.clear();
	}

	@Override
	public void log(LogEvent event) {
		events.add(event);
		if (errorLevelResolver.isEnabled(event.loggerName(), event.level())) {
			MetaLog.error(event);
		}
	}

	@Override
	public void drain(InternalRootRouter delegate) {
		LogEvent e;
		while ((e = this.events.poll()) != null) {
			delegate.route(e.loggerName(), e.level()).log(e);
		}
	}

	@Override
	public boolean isEnabled() {
		return true;
	}

	@Override
	public RouteChangePublisher changePublisher() {
		return this.changePublisher;
	}

}

@SuppressWarnings("ImmutableEnumChecker")
enum GlobalLogRouter implements InternalRootRouter, Route {

	INSTANCE;

	private volatile InternalRootRouter delegate = QueueEventsRouter.of();

	private final ReentrantLock drainLock = new ReentrantLock();

	@Override
	public LevelResolver levelResolver() {
		return delegate.levelResolver();
	}

	@Override
	public boolean isEnabled() {
		return true;
	}

	@Override
	public void log(LogEvent event) {
		InternalRootRouter d = delegate;
		if (d instanceof QueueEventsRouter q) {
			q.log(event);
		}
		else {
			var route = d.route(event.loggerName(), event.level());
			if (route.isEnabled()) {
				route.log(event);
			}
		}
	}

	@Override
	public Route route(String loggerName, Level level) {
		return this.delegate.route(loggerName, level);
	}

	@Override
	public LogEventLogger eventLogger(String loggerName) {
		return this.delegate.eventLogger(loggerName);
	}

	@Override
	public boolean isEnabled(String loggerName, Level level) {
		return this.delegate.isEnabled(loggerName, level);
	}

	@Override
	public RouteChangePublisher changePublisher() {
		return this.delegate.changePublisher();
	}

	@Override
	public void drain(InternalRootRouter delegate) {
		_drain(delegate);
	}

	private InternalRootRouter _drain(InternalRootRouter delegate) {
		if (delegate == this) {
			throw new IllegalStateException("bug");
		}
		drainLock.lock();
		try {
			var original = this.delegate;
			if (original instanceof QueueEventsRouter q) {
				q.changePublisher().transferTo(delegate.changePublisher());
				delegate.changePublisher().publish(delegate);
			}
			this.delegate = Objects.requireNonNull(delegate);
			original.drain(delegate);
			return original;
		}
		finally {
			drainLock.unlock();
		}
	}

	@Override
	public void start(LogConfig config) {
	}

	@Override
	public void close() {
		var d = _drain(QueueEventsRouter.of());
		d.close();
	}

}