From 61eb407d8da26602a29ab37d41e33412cb603ad5 Mon Sep 17 00:00:00 2001 From: Adam Gent Date: Wed, 24 Jan 2024 16:40:03 -0500 Subject: [PATCH] Fix shutdown and event builder --- .../java/io/jstach/rainbowgum/LogEvent.java | 288 ++++++++++++++++-- .../io/jstach/rainbowgum/LogLifecycle.java | 37 ++- .../io/jstach/rainbowgum/LogProperties.java | 16 +- .../java/io/jstach/rainbowgum/LogRouter.java | 186 +++++++---- .../java/io/jstach/rainbowgum/RainbowGum.java | 94 +++++- .../io/jstach/rainbowgum/RainbowGumTest.java | 21 +- .../java/io/jstach/rainbowgum/RouterTest.java | 6 +- .../json/encoder/GelfEncoderConfigurator.java | 2 +- 8 files changed, 549 insertions(+), 101 deletions(-) diff --git a/core/src/main/java/io/jstach/rainbowgum/LogEvent.java b/core/src/main/java/io/jstach/rainbowgum/LogEvent.java index 24e47bb8..71d667da 100644 --- a/core/src/main/java/io/jstach/rainbowgum/LogEvent.java +++ b/core/src/main/java/io/jstach/rainbowgum/LogEvent.java @@ -4,10 +4,15 @@ import java.io.UncheckedIOException; import java.lang.System.Logger.Level; import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; import org.eclipse.jdt.annotation.Nullable; import io.jstach.rainbowgum.KeyValues.MutableKeyValues; +import io.jstach.rainbowgum.LogEvent.Builder; +import io.jstach.rainbowgum.LogRouter.Router; /** * A LogEvent is a container for a single call to a logger. An event should not created @@ -77,7 +82,7 @@ public static LogEvent of(System.Logger.Level level, String loggerName, String m String threadName = currentThread.getName(); long threadId = currentThread.threadId(); return new OneArgLogEvent(timeStamp, threadName, threadId, level, loggerName, message, keyValues, - messageFormatter, arg1); + messageFormatter, null, arg1); } /** @@ -101,7 +106,7 @@ public static LogEvent of(System.Logger.Level level, String loggerName, String m String threadName = currentThread.getName(); long threadId = currentThread.threadId(); return new TwoArgLogEvent(timeStamp, threadName, threadId, level, loggerName, message, keyValues, - messageFormatter, arg1, arg2); + messageFormatter, null, arg1, arg2); } /** @@ -125,7 +130,7 @@ public static LogEvent ofArgs(System.Logger.Level level, String loggerName, Stri String threadName = currentThread.getName(); long threadId = currentThread.threadId(); return new ArrayArgLogEvent(timeStamp, threadName, threadId, level, loggerName, message, keyValues, - messageFormatter, args); + messageFormatter, null, args); } /** @@ -219,6 +224,257 @@ default void formattedMessage(Appendable a) { */ public LogEvent freeze(Instant timestamp); + /** + * A builder to create events by calling {@link Router#eventBuilder(String, Level)}. + * {@link #log()} should be called at the very end otherwise the + * event will not be logged. + */ + public sealed interface Builder { + + /** + * Timestamp when the event was created. + * @param timestamp time the event happened. + * @return this. + */ + public Builder timestamp(Instant timestamp); + + /** + * Add an argument to the event being built. + * @param argSupplier supplier will be called immediatly if this event is to be + * logged. + * @return this. + */ + Builder arg(Supplier argSupplier); + + /** + * Add an argument to the event being built. + * @param arg maybe null. + * @return this. + */ + Builder arg(@Nullable Object arg); + + /** + * Sets the message formatter which interpolates argument place holders. + * @param messageFormatter formatter if not set + * {@link LogMessageFormatter.StandardMessageFormatter#SLF4J} will be used. + * @return this. + */ + public Builder messageFormatter(LogMessageFormatter messageFormatter); + + /** + * Name of the thread. + * @param threadName name of thread. + * @return this. + * @apiNote this maybe empty and often is if virtual threads are used. + */ + public Builder threadName(String threadName); + + /** + * Thread id. + * @param threadId {@link Thread#getId()}. + * @return this. + */ + public Builder threadId(long threadId); + + /** + * Unformatted message. + * @param message unformatted message. + * @return unformatted message + */ + public Builder message(String message); + + /** + * Throwable at the time of the event passed from the logger. + * @param throwable exception at the time of the event. + * @return if the event does not have a throwable null will be + * returned. + */ + public Builder throwable(@Nullable Throwable throwable); + + /** + * Key values that usually come from MDC or an SLF4J Event Builder. + * @param keyValues will use the passed in keyvalues for the event. + * @return key values. + */ + public Builder keyValues(KeyValues keyValues); + + /** + * Will log the event with the current values. + * @return true if accepted. + */ + public boolean log(); + + } + +} + +final class LogEventBuilder implements LogEvent.Builder { + + private final LogEventLogger logger; + + private final Level level; + + private final String loggerName; + + private Instant timestamp = Instant.now(); + + private String threadName = Thread.currentThread().getName(); + + private long threadId = Thread.currentThread().threadId(); + + private String message = ""; + + private @Nullable Throwable throwable; + + private KeyValues keyValues = KeyValues.of(); + + private @Nullable List args = null; + + private LogMessageFormatter messageFormatter = LogMessageFormatter.StandardMessageFormatter.SLF4J; + + LogEventBuilder(LogEventLogger logger, Level level, String loggerName) { + super(); + this.logger = logger; + this.level = level; + this.loggerName = loggerName; + } + + @Override + public Builder timestamp(Instant timestamp) { + this.timestamp = timestamp; + return this; + } + + @Override + public Builder threadName(String threadName) { + this.threadName = threadName; + return this; + } + + @Override + public Builder threadId(long threadId) { + this.threadId = threadId; + return this; + } + + @Override + public Builder message(String message) { + this.message = message; + return this; + } + + @Override + public Builder messageFormatter(LogMessageFormatter messageFormatter) { + this.messageFormatter = messageFormatter; + return this; + } + + @Override + public Builder throwable(@Nullable Throwable throwable) { + this.throwable = throwable; + return this; + } + + @Override + public Builder keyValues(KeyValues keyValues) { + this.keyValues = keyValues; + return this; + } + + @Override + public Builder arg(Object arg) { + var list = this.args; + if (list == null) { + list = this.args = new ArrayList<>(); + } + list.add(arg); + return this; + } + + @Override + public Builder arg(Supplier argSupplier) { + return arg(argSupplier.get()); + } + + @Override + public boolean log() { + List args = this.args; + if (args == null) { + logger.log(new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues, + throwable)); + return true; + } + int size = args.size(); + LogEvent event = switch (size) { + case 0 -> + new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues, throwable); + case 1 -> new OneArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues, + messageFormatter, throwable, args.get(0)); + case 2 -> new TwoArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues, + messageFormatter, throwable, args.get(0), args.get(1)); + default -> new ArrayArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues, + messageFormatter, throwable, args.toArray()); + }; + logger.log(event); + return true; + } + +} + +enum NoOpLogEventBuilder implements LogEvent.Builder { + + NOOP; + + @Override + public Builder timestamp(Instant timestamp) { + return this; + } + + @Override + public Builder threadName(String threadName) { + return this; + } + + @Override + public Builder threadId(long threadId) { + return this; + } + + @Override + public Builder message(String message) { + return this; + } + + @Override + public Builder throwable(@Nullable Throwable throwable) { + return this; + } + + @Override + public Builder keyValues(KeyValues keyValues) { + return this; + } + + @Override + public Builder messageFormatter(LogMessageFormatter messageFormatter) { + return this; + } + + @Override + public boolean log() { + return false; + } + + @Override + public Builder arg(Supplier argSupplier) { + return this; + } + + @Override + public Builder arg(@Nullable Object arg) { + return this; + } + } enum EmptyLogEvent implements LogEvent { @@ -291,7 +547,7 @@ public LogEvent freeze(Instant timestamp) { } record OneArgLogEvent(Instant timestamp, String threadName, long threadId, System.Logger.Level level, String loggerName, - String message, KeyValues keyValues, LogMessageFormatter messageFormatter, + String message, KeyValues keyValues, LogMessageFormatter messageFormatter, @Nullable Throwable throwable, @Nullable Object arg1) implements LogEvent { public void formattedMessage(StringBuilder sb) { @@ -302,10 +558,6 @@ public int argCount() { return 1; } - public @Nullable Throwable throwable() { - return null; - } - public LogEvent freeze() { return freeze(timestamp); } @@ -314,14 +566,14 @@ public LogEvent freeze(Instant timestamp) { StringBuilder sb = new StringBuilder(message.length()); formattedMessage(sb); return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, sb.toString(), - keyValues.freeze(), null); + keyValues.freeze(), throwable); } } record TwoArgLogEvent(Instant timestamp, String threadName, long threadId, System.Logger.Level level, String loggerName, - String message, KeyValues keyValues, LogMessageFormatter messageFormatter, @Nullable Object arg1, - @Nullable Object arg2) implements LogEvent { + String message, KeyValues keyValues, LogMessageFormatter messageFormatter, @Nullable Throwable throwable, + @Nullable Object arg1, @Nullable Object arg2) implements LogEvent { public void formattedMessage(StringBuilder sb) { messageFormatter.format(sb, message, arg1, arg2); @@ -331,10 +583,6 @@ public int argCount() { return 2; } - public @Nullable Throwable throwable() { - return null; - } - public LogEvent freeze() { return freeze(timestamp); } @@ -343,13 +591,13 @@ public LogEvent freeze(Instant timestamp) { StringBuilder sb = new StringBuilder(message.length()); formattedMessage(sb); return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, sb.toString(), - keyValues.freeze(), null); + keyValues.freeze(), throwable); } } record ArrayArgLogEvent(Instant timestamp, String threadName, long threadId, System.Logger.Level level, String loggerName, String message, KeyValues keyValues, LogMessageFormatter messageFormatter, - Object[] args) implements LogEvent { + @Nullable Throwable throwable, Object[] args) implements LogEvent { public void formattedMessage(StringBuilder sb) { messageFormatter.formatArray(sb, message, args); @@ -359,10 +607,6 @@ public int argCount() { return args.length; } - public @Nullable Throwable throwable() { - return null; - } - public LogEvent freeze() { return freeze(timestamp); } @@ -371,7 +615,7 @@ public LogEvent freeze(Instant timestamp) { StringBuilder sb = new StringBuilder(message.length()); formattedMessage(sb); return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, sb.toString(), - keyValues.freeze(), null); + keyValues.freeze(), throwable); } } diff --git a/core/src/main/java/io/jstach/rainbowgum/LogLifecycle.java b/core/src/main/java/io/jstach/rainbowgum/LogLifecycle.java index 3c91225e..2b51a43b 100644 --- a/core/src/main/java/io/jstach/rainbowgum/LogLifecycle.java +++ b/core/src/main/java/io/jstach/rainbowgum/LogLifecycle.java @@ -1,5 +1,8 @@ package io.jstach.rainbowgum; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -19,6 +22,12 @@ public interface LogLifecycle extends AutoCloseable { } +interface Shutdownable { + + public void shutdown(); + +} + final class ShutdownManager { private static final ReentrantReadWriteLock staticLock = new ReentrantReadWriteLock(); @@ -45,14 +54,40 @@ static void addShutdownHook(AutoCloseable hook) { } } + static void removeShutdownHook(AutoCloseable hook) { + staticLock.writeLock().lock(); + try { + shutdownHooks.removeIf(h -> h == hook); + } + finally { + staticLock.writeLock().unlock(); + } + + } + + /* + * This is for unit testing. + */ + static List shutdownHooks() { + return List.copyOf(shutdownHooks); + } + private static void runShutdownHooks() { /* * We do not lock here since we are in the shutdown thread luckily shutdownHooks * is thread safe */ + var found = Collections.newSetFromMap(new IdentityHashMap<>()); for (var hook : shutdownHooks) { try { - hook.close(); + if (found.add(hook)) { + if (hook instanceof Shutdownable shut) { + shut.shutdown(); + } + else { + hook.close(); + } + } } catch (Exception e) { MetaLog.error(Defaults.class, e); diff --git a/core/src/main/java/io/jstach/rainbowgum/LogProperties.java b/core/src/main/java/io/jstach/rainbowgum/LogProperties.java index fdba1d85..5a314aa1 100644 --- a/core/src/main/java/io/jstach/rainbowgum/LogProperties.java +++ b/core/src/main/java/io/jstach/rainbowgum/LogProperties.java @@ -620,7 +620,7 @@ final static class PropertyMissingException extends NoSuchElementException imple } private static PropertyConvertException throwPropertyError(String key, Exception e) { - throw new PropertyConvertException(key, "Error for property. key: " + key, e); + throw new PropertyConvertException(key, "Error for property. key: '" + key + "', " + e.getMessage(), e); } private static RuntimeException throwMissingError(List keys) { @@ -698,6 +698,20 @@ static String interpolateKey(String name, Map parameters) { return name; } + /** + * Remove key prefix from the key. Prefix should end in "." but this method does not + * check that. + * @param key key whose prefix will be removed. + * @param prefix property name prefix. + * @return prefix removed if the string is prefixed. + */ + static String removeKeyPrefix(String key, String prefix) { + if (key.startsWith(prefix)) { + return key.substring(prefix.length()); + } + return key; + } + /** * Property prefix parameter pattern. */ diff --git a/core/src/main/java/io/jstach/rainbowgum/LogRouter.java b/core/src/main/java/io/jstach/rainbowgum/LogRouter.java index a7967ad8..96960962 100644 --- a/core/src/main/java/io/jstach/rainbowgum/LogRouter.java +++ b/core/src/main/java/io/jstach/rainbowgum/LogRouter.java @@ -37,6 +37,23 @@ public sealed interface LogRouter extends LogLifecycle { */ public Route route(String loggerName, java.lang.System.Logger.Level level); + /** + * Creates (or reuses in the case of logging off) an event builder. + * @param loggerName logger name of the event. + * @param level level that the event should be set to. + * @return builder. + * @apiNote using the builder is slightly slower and more garbage than just manually + * checking {@link Route#isEnabled()} and constructing the event using the LogEvent + * static "of" factory methods. + */ + default LogEvent.Builder eventBuilder(String loggerName, Level level) { + var route = route(loggerName, level); + if (route.isEnabled()) { + return new LogEventBuilder(route, level, loggerName); + } + return NoOpLogEventBuilder.NOOP; + } + /** * Global router which is always available. * @return global root router. @@ -81,6 +98,7 @@ public void log(LogEvent event) { public boolean isEnabled() { return false; } + } } @@ -127,7 +145,7 @@ private static Logger logger(RootRouter router, String loggerName) { if (!(router instanceof InternalRootRouter r)) { throw new IllegalArgumentException("bug"); } - return new TinyLogger(loggerName, r); + return new DefaultSystemLogger(loggerName, r); } } @@ -250,7 +268,7 @@ Router build() { var apps = appenders.stream().map(a -> a.provide(config)).toList(); var pub = publisher.provide(config, apps); - return new SimpleRoute(pub, levelResolver); + return new SimpleRouter(pub, levelResolver); } } @@ -259,7 +277,7 @@ Router build() { } -record SimpleRoute(LogPublisher publisher, LevelResolver levelResolver) implements Router, Route { +record SimpleRouter(LogPublisher publisher, LevelResolver levelResolver) implements Router, Route { @Override public void close() { @@ -295,7 +313,7 @@ static void setRouter(RootRouter router) { if (GlobalLogRouter.INSTANCE == router) { throw new IllegalArgumentException(); } - GlobalLogRouter.INSTANCE.drain(router); + GlobalLogRouter.INSTANCE.drain((InternalRootRouter) router); } static InternalRootRouter of(List routes, LevelResolver levelResolver) { @@ -327,8 +345,9 @@ static InternalRootRouter of(List routes, LevelResolver levelR Router[] array = routes.toArray(new Router[] {}); - if (array.length == 1 && array[0].synchronous()) { - return new SingleSyncRootRouter(array[0]); + if (array.length == 1) { + var r = array[0]; + return r.synchronous() ? new SingleSyncRootRouter(r) : new SingleAsyncRootRouter(r); } return new CompositeLogRouter(array, globalLevelResolver); } @@ -342,6 +361,10 @@ default void log(String loggerName, java.lang.System.Logger.Level level, String log(loggerName, level, message, null); } + default void drain(InternalRootRouter delegate) { + + } + } record SingleSyncRootRouter(Router router) implements InternalRootRouter { @@ -367,6 +390,33 @@ public Route route(String loggerName, Level level) { } +record SingleAsyncRootRouter(Router router) implements InternalRootRouter { + + @Override + public void start(LogConfig config) { + router.start(config); + } + + @Override + public void close() { + router.close(); + } + + public LevelResolver levelResolver() { + return router.levelResolver(); + } + + @Override + public Route route(String loggerName, Level level) { + return router.route(loggerName, level); + } + + public void log(LogEvent event) { + router.log(event.freeze()); + } + +} + record CompositeLogRouter(Router[] routers, LevelResolver levelResolver) implements InternalRootRouter, Route { @Override @@ -444,15 +494,63 @@ public void start(LogConfig config) { } -enum GlobalLogRouter implements InternalRootRouter, Route { - - INSTANCE; +final class QueueEventsRouter implements InternalRootRouter, Route { private final ConcurrentLinkedQueue events = new ConcurrentLinkedQueue<>(); private static final LevelResolver INFO_RESOLVER = InternalLevelResolver.of(Level.INFO); - private volatile @Nullable InternalRootRouter delegate = null; + @Override + public LevelResolver levelResolver() { + return INFO_RESOLVER; + } + + @Override + public Route route(String loggerName, Level level) { + if (INFO_RESOLVER.isEnabled(loggerName, level)) { + return this; + } + return Routes.NotFound; + } + + @Override + public void start(LogConfig config) { + } + + @Override + public void close() { + events.clear(); + } + + @Override + public void log(LogEvent event) { + events.add(event); + if (event.level().getSeverity() >= Level.ERROR.getSeverity()) { + 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; + } + +} + +enum GlobalLogRouter implements InternalRootRouter, Route { + + INSTANCE; + + private volatile InternalRootRouter delegate = new QueueEventsRouter(); private final ReentrantLock drainLock = new ReentrantLock(); @@ -462,11 +560,7 @@ static boolean isShutdownEvent(String loggerName, java.lang.System.Logger.Level @Override public LevelResolver levelResolver() { - RootRouter d = delegate; - if (d != null) { - return d.levelResolver(); - } - return INFO_RESOLVER; + return delegate.levelResolver(); } @Override @@ -476,8 +570,11 @@ public boolean isEnabled() { @Override public void log(LogEvent event) { - RootRouter d = delegate; - if (d != null) { + InternalRootRouter d = delegate; + if (d instanceof QueueEventsRouter q) { + q.log(event); + } + else { String loggerName = event.loggerName(); var level = event.level(); var route = d.route(event.loggerName(), event.level()); @@ -488,64 +585,32 @@ public void log(LogEvent event) { d.close(); } } - else { - events.add(event); - if (event.level().getSeverity() >= Level.ERROR.getSeverity()) { - MetaLog.error(event); - } - } - } @Override public Route route(String loggerName, Level level) { - RootRouter d = delegate; - if (d != null) { - return d.route(loggerName, level); - } - if (INFO_RESOLVER.isEnabled(loggerName, level)) { - return this; - } - return Routes.NotFound; + return this.delegate.route(loggerName, level); } @Override public boolean isEnabled(String loggerName, Level level) { - InternalRootRouter d = delegate; - if (d != null) { - return d.isEnabled(loggerName, level); - } - return INFO_RESOLVER.isEnabled(loggerName, level); + return this.delegate.isEnabled(loggerName, level); } public void log(String loggerName, java.lang.System.Logger.Level level, String message, @Nullable Throwable cause) { InternalRootRouter d = delegate; - if (d != null) { - d.log(loggerName, level, message, cause); - if (isShutdownEvent(loggerName, level)) { - d.close(); - } - } - else { - var event = LogEvent.of(level, loggerName, message, cause); - events.add(event); - if (event.level().getSeverity() >= Level.ERROR.getSeverity()) { - MetaLog.error(event); - } + d.log(loggerName, level, message, cause); + if (isShutdownEvent(loggerName, level)) { + d.close(); } } - public void drain(RootRouter delegate) { - if (!(delegate instanceof InternalRootRouter r)) { - throw new IllegalArgumentException("bug"); - } + public void drain(InternalRootRouter delegate) { drainLock.lock(); try { - this.delegate = Objects.requireNonNull(r); - LogEvent e; - while ((e = this.events.poll()) != null) { - delegate.route(e.loggerName(), e.level()).log(e); - } + var original = this.delegate; + this.delegate = Objects.requireNonNull(delegate); + original.drain(delegate); } finally { drainLock.unlock(); @@ -558,17 +623,18 @@ public void start(LogConfig config) { @Override public void close() { + this.delegate.close(); } } -class TinyLogger implements System.Logger { +class DefaultSystemLogger implements System.Logger { private final String name; private final InternalRootRouter router; - public TinyLogger(String name, InternalRootRouter router) { + public DefaultSystemLogger(String name, InternalRootRouter router) { super(); this.name = name; this.router = router; diff --git a/core/src/main/java/io/jstach/rainbowgum/RainbowGum.java b/core/src/main/java/io/jstach/rainbowgum/RainbowGum.java index 876968d9..c570bd3e 100644 --- a/core/src/main/java/io/jstach/rainbowgum/RainbowGum.java +++ b/core/src/main/java/io/jstach/rainbowgum/RainbowGum.java @@ -4,12 +4,12 @@ import java.util.List; import java.util.Objects; import java.util.ServiceLoader; +import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Consumer; import java.util.function.Supplier; -import io.jstach.rainbowgum.LogPublisher.PublisherProvider; import io.jstach.rainbowgum.LogRouter.RootRouter; import io.jstach.rainbowgum.LogRouter.Router; import io.jstach.rainbowgum.spi.RainbowGumServiceProvider; @@ -111,9 +111,18 @@ default RainbowGum start() { return this; } - default void close() { - router().close(); - } + /** + * Unique id of rainbow gum instance. + * @return random id created on creation. + */ + public UUID instanceId(); + + /** + * Will close the rainbowgum and all registered components as well as removed from the + * shutdown hooks. If the rainbow gum is set as global it will no longer be global and + * replaced with the bootstrapping in memory queue. {@inheritDoc} + */ + public void close(); /** * This append call is mainly for testing as it does not avoid making events that do @@ -202,13 +211,41 @@ public Builder route(Consumer consumer) { * @return an unstarted {@link RainbowGum}. */ public RainbowGum build() { + return build(UUID.randomUUID()); + } + + /** + * Builds an unstarted {@link RainbowGum}. + * @return an unstarted {@link RainbowGum}. + */ + private RainbowGum build(UUID instanceId) { var routes = this.routes; var config = this.config; if (routes.isEmpty()) { routes = List.of(Router.builder(config).build()); } var root = InternalRootRouter.of(routes, config.levelResolver()); - return new SimpleRainbowGum(config, root); + return new SimpleRainbowGum(config, root, instanceId); + } + + /** + * Builds, starts and sets the RainbowGum as the global one picked up by logging + * facades. + * @return started and set rainbow that can be used in a try-close. + */ + public RainbowGum set() { + UUID instanceId = UUID.randomUUID(); + RainbowGum.set(() -> build(instanceId)); + var gum = RainbowGum.of(); + /* + * TODO this is a hack. The holder lock should be used to make this not + * happen. + */ + if (!instanceId.equals(gum.instanceId())) { + throw new IllegalStateException("Another rainbow gum registered itself as the global. " + + "This is rare reace condition and probably a bug"); + } + return gum; } } @@ -256,6 +293,22 @@ static RainbowGum get() { } + static boolean remove(RainbowGum gum) { + lock.writeLock().lock(); + try { + var original = rainbowGum; + if (original != gum) { + return false; + } + rainbowGum = null; + supplier = RainbowGumServiceProvider::provide; + return true; + } + finally { + lock.writeLock().unlock(); + } + } + static void set(Supplier rainbowGumSupplier) { Objects.requireNonNull(rainbowGumSupplier); if (lock.writeLock().isHeldByCurrentThread()) { @@ -281,7 +334,7 @@ private static void start(RainbowGum gum) { } -final class SimpleRainbowGum implements RainbowGum { +final class SimpleRainbowGum implements RainbowGum, Shutdownable { private final LogConfig config; @@ -289,16 +342,19 @@ final class SimpleRainbowGum implements RainbowGum { private final AtomicInteger state = new AtomicInteger(0); + private final UUID instanceId; + private static final int INIT = 0; private static final int STARTED = 1; private static final int CLOSED = 2; - public SimpleRainbowGum(LogConfig config, RootRouter router) { + public SimpleRainbowGum(LogConfig config, RootRouter router, UUID instanceId) { super(); this.config = config; this.router = router; + this.instanceId = instanceId; } public LogConfig config() { @@ -318,14 +374,36 @@ public RainbowGum start() { throw new IllegalStateException("Cannot start. This rainbowgum is " + stateLabel(current)); } + @Override + public UUID instanceId() { + return this.instanceId; + } + @Override public void close() { if (state.compareAndSet(STARTED, CLOSED)) { - RainbowGum.super.close(); + RainbowGumHolder.remove(this); + try { + shutdown(); + } + finally { + ShutdownManager.removeShutdownHook(this); + } return; } } + @Override + public void shutdown() { + router().close(); + } + + @Override + public String toString() { + return "SimpleRainbowGum [instanceId=" + instanceId + ", config=" + config + ", router=" + router + ", state=" + + stateLabel(state.get()) + "]"; + } + private static String stateLabel(int state) { return switch (state) { case INIT -> "created"; diff --git a/core/src/test/java/io/jstach/rainbowgum/RainbowGumTest.java b/core/src/test/java/io/jstach/rainbowgum/RainbowGumTest.java index c1ed05c4..daa9ff36 100644 --- a/core/src/test/java/io/jstach/rainbowgum/RainbowGumTest.java +++ b/core/src/test/java/io/jstach/rainbowgum/RainbowGumTest.java @@ -1,5 +1,6 @@ package io.jstach.rainbowgum; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -73,22 +74,32 @@ void testFormatterBuilder() throws Exception { // }) // .output(logFile) // .build(); - + GlobalLogRouter.INSTANCE.log("stuff", Level.WARNING, "first"); try (var gum = RainbowGum.builder().route(r -> { r.publisher(PublisherProvider.async().build()); r.appender(sysout); r.level(Level.WARNING, "stuff"); - }).build().start()) { + }).set()) { + + assertEquals(1, ShutdownManager.shutdownHooks().size()); - gum.router().log("stuff", Level.INFO, "Stuff", null); - gum.router().log("stuff", Level.ERROR, "bad", null); + var router = gum.router(); + router.log("stuff", Level.INFO, "Stuff", null); + router.log("stuff", Level.ERROR, "bad", null); - boolean enabled = gum.router().route("stuff", Level.INFO).isEnabled(); + router.eventBuilder("stuff", Level.WARNING) // + .message("builder info - {}") // + .arg("hello") // + .log(); + + boolean enabled = router.route("stuff", Level.INFO).isEnabled(); assertFalse(enabled); Thread.sleep(50); } + assertEquals(0, ShutdownManager.shutdownHooks().size()); + } } diff --git a/core/src/test/java/io/jstach/rainbowgum/RouterTest.java b/core/src/test/java/io/jstach/rainbowgum/RouterTest.java index 0b22fb59..12d992a4 100644 --- a/core/src/test/java/io/jstach/rainbowgum/RouterTest.java +++ b/core/src/test/java/io/jstach/rainbowgum/RouterTest.java @@ -21,7 +21,7 @@ void testSingleRouter() throws Exception { LevelResolver resolver = InternalLevelResolver.of(Map.of("stuff", Level.INFO, "", Level.DEBUG)); var publisher = new TestSyncPublisher(); @SuppressWarnings("resource") - var router = new SimpleRoute(publisher, resolver); + var router = new SimpleRouter(publisher, resolver); var route = router.route("stuff.crap", Level.DEBUG); assertFalse(route.isEnabled()); assertTrue(router.route("blah", Level.DEBUG).isEnabled()); @@ -33,11 +33,11 @@ void testCompositeRouter() throws Exception { LevelResolver resolver1 = InternalLevelResolver.of(Map.of("stuff", Level.INFO, "", Level.DEBUG)); var publisher1 = new TestSyncPublisher(); - var router1 = new SimpleRoute(publisher1, resolver1); + var router1 = new SimpleRouter(publisher1, resolver1); LevelResolver resolver2 = InternalLevelResolver.of(Map.of("stuff", Level.DEBUG, "", Level.WARNING)); var publisher2 = new TestSyncPublisher(); - var router2 = new SimpleRoute(publisher2, resolver2); + var router2 = new SimpleRouter(publisher2, resolver2); var root = InternalRootRouter.of(List.of(router1, router2), InternalLevelResolver.of(Level.ERROR)); diff --git a/rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/encoder/GelfEncoderConfigurator.java b/rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/encoder/GelfEncoderConfigurator.java index 533435b4..e9179b4d 100644 --- a/rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/encoder/GelfEncoderConfigurator.java +++ b/rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/encoder/GelfEncoderConfigurator.java @@ -39,7 +39,7 @@ public LogEncoder provide(URI uri, String name, LogProperties properties) { query = query == null ? "" : query; var uriProperties = LogProperties.builder() .fromURIQuery(query) - .renameKey(s -> s.replace(prefix, "")) + .renameKey(s -> LogProperties.removeKeyPrefix(s, prefix)) .build(); LogProperties combined = LogProperties.of(List.of(uriProperties, properties));