RainbowGumSystemLoggerFinder.java
package io.jstach.rainbowgum.systemlogger;
import java.lang.System.Logger;
import java.util.Locale;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import io.jstach.rainbowgum.LogProperties;
import io.jstach.rainbowgum.LogProperty;
import io.jstach.rainbowgum.LogRouter;
import io.jstach.rainbowgum.LoggerAPI;
import io.jstach.rainbowgum.RainbowGum;
import io.jstach.rainbowgum.spi.RainbowGumServiceProvider;
/**
* Abstract System Logger Finder to allow users to create their own custom
* System.LoggerFinder. <strong>This implementation does not cache System Loggers</strong>
* by name!
* <p>
* <b>GraalVM native image</b>: this module bundles a {@code native-image.properties} (at
* {@code META-INF/native-image/io.jstach.rainbowgum/rainbowgum-systemlogger/}) with
* {@code --initialize-at-build-time} for {@code RouterProvider} plus the core classes
* ({@code GlobalLogRouter}, {@code StaticLevelResolver}) its {@code LogRouter.global()}
* fallback touches on first real use. See {@link #routerProvider} for why this is still
* needed even though this class resolves its router lazily rather than eagerly in its
* constructor, and why what gets frozen in is just the cheap "nothing bound yet" state,
* not anything environment-specific. native-image's own embedded configuration discovery
* picks this up from this module's jar automatically, no extra plugin or flag needed on
* the consuming side.
*
* @see #INITIALIZE_RAINBOW_GUM_PROPERTY
*/
public abstract class RainbowGumSystemLoggerFinder extends System.LoggerFinder {
/**
* Initialization flag.
* @see InitOption
*/
public static final String INITIALIZE_RAINBOW_GUM_PROPERTY = LogProperties.ROOT_PREFIX + "systemlogger.initialize";
private final Supplier<? extends InitOption> optSupplier;
/*
* Resolved lazily, on the first getLogger(...) call, not eagerly in the constructor.
* Constructing a System.LoggerFinder happens through java.util.ServiceLoader, which
* the JDK itself may trigger from all sorts of incidental static initialization (see
* rainbowgum-jdk's own module javadoc); under GraalVM native-image's default
* build-time class initialization in particular, an eager constructor here would
* freeze whatever InitOption/RainbowGum state happened to resolve during the build
* into the image, using build-time system properties and a build-time
* RainbowGum.getOrNull() check, not the real ones the running image would see. A
* benign race (two threads computing the same idempotent value once each) is fine
* here, matching InitRouterProvider's own existing lazy resolution of its RainbowGum
* supplier just below.
*
* This alone does not make a custom System.LoggerFinder fully invisible to
* native-image's build-time analysis, since the JDK's own internals call
* System.getLogger(...) incidentally, for their own diagnostics, from all sorts of
* unrelated code paths that end up reachable during a real build (observed triggers:
* java.time/java.util.Locale formatting, and separately
* com.oracle.svm.core.jdk.TrustStoreManagerFeature loading the default trust store at
* build time); whichever registered LoggerFinder is on the classpath gets swept up
* regardless of how lazy its own construction is. What laziness here does buy: since
* getLogger(...) only ever runs this exact code path for real, what gets resolved and
* frozen into the image heap as a side effect is always the correct, fresh answer:
* when RainbowGumServiceProvider.RainbowGumEagerLoad exists (SLF4J's own facade
* implements it), that answer is just LogRouter.global()'s ambient, not-yet-bound
* queuing router, the same "nothing bound yet" state any freshly started JVM begins
* in, not anything environment-specific baked in from the build. That still pulls in
* a handful of core classes (GlobalLogRouter, StaticLevelResolver) that also need
* flagging, confirmed by hand against a real GraalVM build; see this module's own
* bundled native-image.properties for the exact list, and rainbowgum-jdk's own
* SystemLoggingFactory javadoc for the companion flags reached via
* LogProperties.findGlobalProperties() instead. Not every class in the whole
* io.jstach.rainbowgum package needs this treatment, only these specific ones.
*/
private volatile @Nullable RouterProvider routerProvider;
/**
* Values (case is ignored) for {@value #INITIALIZE_RAINBOW_GUM_PROPERTY}.
*/
public enum InitOption {
/**
* Will not initialize rainbow gum.
*/
FALSE,
/**
* Will initialize rainbow gum.
*/
TRUE,
/**
* (default) Will check if there are implementations of
* {@link RainbowGumServiceProvider.RainbowGumEagerLoad} and if there are not will
* load rainbow gum.
*/
CHECK,
/**
* Will reuse an existing rainbow gum or fail.
*/
REUSE;
/**
* Parses an init option from a property value.
* @param input from properties.
* @return init option.
*/
public static InitOption parse(String input) {
if (input.isBlank())
return FALSE;
return LogProperty.enumValue(InitOption.class, input, "blank");
}
}
/**
* Creates the logger inder based on init option.
* @param optSupplier can be resolved with {@link #initOption(LogProperties)}.
*/
protected RainbowGumSystemLoggerFinder(Supplier<? extends InitOption> optSupplier) {
this.optSupplier = optSupplier;
}
private RouterProvider routerProvider() {
var rp = this.routerProvider;
if (rp != null) {
return rp;
}
try {
var opt = optSupplier.get();
rp = switch (opt) {
case FALSE -> n -> LogRouter.global();
case TRUE -> new InitRouterProvider(RainbowGum::of);
case CHECK -> {
if (RainbowGumServiceProvider.RainbowGumEagerLoad.exists()) {
yield n -> LogRouter.global();
}
yield new InitRouterProvider(RainbowGum::of);
}
case REUSE -> new InitRouterProvider(() -> {
var gum = RainbowGum.getOrNull();
if (gum == null) {
throw new IllegalStateException(
"SystemLogging was configured to reuse a loaded Rainbow Gum but none was found. "
+ INITIALIZE_RAINBOW_GUM_PROPERTY + "=" + opt);
}
return gum;
});
};
this.routerProvider = rp;
return rp;
}
catch (Exception e) {
// We have to do this because it because very difficult
// to determine why the System Logging fails as it does not even print the
// exception.
var gum = RainbowGum.getOrNull();
if (gum != null) {
gum.config().alerts().error(getClass(), "Failed to create System.LoggerFinder", e);
}
else {
System.err.println("[ERROR] - RAINBOW_GUM Failed to create System.LoggerFinder");
e.printStackTrace();
}
throw e;
}
}
@Override
public Logger getLogger(String name, Module module) {
var router = routerProvider().router(name);
if (!router.isChangeable(name)) {
var level = router.levelResolver().resolveLevel(name);
return LevelSystemLogger.of(name, level, router.route(name, level));
}
return RainbowGumSystemLogger.of(name, router);
}
/**
* Gets the init option from properties.
* @param properties usually system properties.
* @return initialization option.
*/
protected static InitOption initOption(LogProperties properties) {
return properties.forKey(INITIALIZE_RAINBOW_GUM_PROPERTY)
.ofString()
.map(InitOption::parse)
.or(InitOption.CHECK)
.validateNow(RainbowGumSystemLoggerFinder.class);
}
private interface RouterProvider {
LogRouter.RootRouter router(String loggerName);
}
private static class InitRouterProvider implements RouterProvider {
private final Supplier<RainbowGum> supplier;
private volatile @Nullable RainbowGum gum = null;
InitRouterProvider(Supplier<RainbowGum> supplier) {
super();
this.supplier = supplier;
}
@Override
public LogRouter.RootRouter router(String loggerName) {
LogRouter.RootRouter router;
RainbowGum gum = this.gum;
if (gum == null) {
gum = this.gum = supplier.get();
}
gum.config().loggerRegistry().registerLoggerName(LoggerAPI.Standard.SYSTEM_LOGGER, loggerName);
if (gum.config().changePublisher().isEnabled(loggerName)) {
router = LogRouter.global();
}
else {
router = gum.router();
}
return router;
}
}
}