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 extends Router> routes, LevelResolver levelResolver) {
@@ -327,8 +345,9 @@ static InternalRootRouter of(List extends Router> 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));