001package io.jstach.rainbowgum.systemlogger; 002 003import java.lang.System.Logger; 004import java.util.Locale; 005import java.util.function.Supplier; 006 007import org.jspecify.annotations.Nullable; 008 009import io.jstach.rainbowgum.LogProperties; 010import io.jstach.rainbowgum.LogProperty; 011import io.jstach.rainbowgum.LogRouter; 012import io.jstach.rainbowgum.LoggerAPI; 013import io.jstach.rainbowgum.RainbowGum; 014import io.jstach.rainbowgum.spi.RainbowGumServiceProvider; 015 016/** 017 * Abstract System Logger Finder to allow users to create their own custom 018 * System.LoggerFinder. <strong>This implementation does not cache System Loggers</strong> 019 * by name! 020 * <p> 021 * <b>GraalVM native image</b>: this module bundles a {@code native-image.properties} (at 022 * {@code META-INF/native-image/io.jstach.rainbowgum/rainbowgum-systemlogger/}) with 023 * {@code --initialize-at-build-time} for {@code RouterProvider} plus the core classes 024 * ({@code GlobalLogRouter}, {@code StaticLevelResolver}) its {@code LogRouter.global()} 025 * fallback touches on first real use. See {@link #routerProvider} for why this is still 026 * needed even though this class resolves its router lazily rather than eagerly in its 027 * constructor, and why what gets frozen in is just the cheap "nothing bound yet" state, 028 * not anything environment-specific. native-image's own embedded configuration discovery 029 * picks this up from this module's jar automatically, no extra plugin or flag needed on 030 * the consuming side. 031 * 032 * @see #INITIALIZE_RAINBOW_GUM_PROPERTY 033 */ 034public abstract class RainbowGumSystemLoggerFinder extends System.LoggerFinder { 035 036 /** 037 * Initialization flag. 038 * @see InitOption 039 */ 040 public static final String INITIALIZE_RAINBOW_GUM_PROPERTY = LogProperties.ROOT_PREFIX + "systemlogger.initialize"; 041 042 private final Supplier<? extends InitOption> optSupplier; 043 044 /* 045 * Resolved lazily, on the first getLogger(...) call, not eagerly in the constructor. 046 * Constructing a System.LoggerFinder happens through java.util.ServiceLoader, which 047 * the JDK itself may trigger from all sorts of incidental static initialization (see 048 * rainbowgum-jdk's own module javadoc); under GraalVM native-image's default 049 * build-time class initialization in particular, an eager constructor here would 050 * freeze whatever InitOption/RainbowGum state happened to resolve during the build 051 * into the image, using build-time system properties and a build-time 052 * RainbowGum.getOrNull() check, not the real ones the running image would see. A 053 * benign race (two threads computing the same idempotent value once each) is fine 054 * here, matching InitRouterProvider's own existing lazy resolution of its RainbowGum 055 * supplier just below. 056 * 057 * This alone does not make a custom System.LoggerFinder fully invisible to 058 * native-image's build-time analysis, since the JDK's own internals call 059 * System.getLogger(...) incidentally, for their own diagnostics, from all sorts of 060 * unrelated code paths that end up reachable during a real build (observed triggers: 061 * java.time/java.util.Locale formatting, and separately 062 * com.oracle.svm.core.jdk.TrustStoreManagerFeature loading the default trust store at 063 * build time); whichever registered LoggerFinder is on the classpath gets swept up 064 * regardless of how lazy its own construction is. What laziness here does buy: since 065 * getLogger(...) only ever runs this exact code path for real, what gets resolved and 066 * frozen into the image heap as a side effect is always the correct, fresh answer: 067 * when RainbowGumServiceProvider.RainbowGumEagerLoad exists (SLF4J's own facade 068 * implements it), that answer is just LogRouter.global()'s ambient, not-yet-bound 069 * queuing router, the same "nothing bound yet" state any freshly started JVM begins 070 * in, not anything environment-specific baked in from the build. That still pulls in 071 * a handful of core classes (GlobalLogRouter, StaticLevelResolver) that also need 072 * flagging, confirmed by hand against a real GraalVM build; see this module's own 073 * bundled native-image.properties for the exact list, and rainbowgum-jdk's own 074 * SystemLoggingFactory javadoc for the companion flags reached via 075 * LogProperties.findGlobalProperties() instead. Not every class in the whole 076 * io.jstach.rainbowgum package needs this treatment, only these specific ones. 077 */ 078 private volatile @Nullable RouterProvider routerProvider; 079 080 /** 081 * Values (case is ignored) for {@value #INITIALIZE_RAINBOW_GUM_PROPERTY}. 082 */ 083 public enum InitOption { 084 085 /** 086 * Will not initialize rainbow gum. 087 */ 088 FALSE, 089 /** 090 * Will initialize rainbow gum. 091 */ 092 TRUE, 093 /** 094 * (default) Will check if there are implementations of 095 * {@link RainbowGumServiceProvider.RainbowGumEagerLoad} and if there are not will 096 * load rainbow gum. 097 */ 098 CHECK, 099 /** 100 * Will reuse an existing rainbow gum or fail. 101 */ 102 REUSE; 103 104 /** 105 * Parses an init option from a property value. 106 * @param input from properties. 107 * @return init option. 108 */ 109 public static InitOption parse(String input) { 110 if (input.isBlank()) 111 return FALSE; 112 return LogProperty.enumValue(InitOption.class, input, "blank"); 113 } 114 115 } 116 117 /** 118 * Creates the logger inder based on init option. 119 * @param optSupplier can be resolved with {@link #initOption(LogProperties)}. 120 */ 121 protected RainbowGumSystemLoggerFinder(Supplier<? extends InitOption> optSupplier) { 122 this.optSupplier = optSupplier; 123 } 124 125 private RouterProvider routerProvider() { 126 var rp = this.routerProvider; 127 if (rp != null) { 128 return rp; 129 } 130 try { 131 var opt = optSupplier.get(); 132 rp = switch (opt) { 133 case FALSE -> n -> LogRouter.global(); 134 case TRUE -> new InitRouterProvider(RainbowGum::of); 135 case CHECK -> { 136 if (RainbowGumServiceProvider.RainbowGumEagerLoad.exists()) { 137 yield n -> LogRouter.global(); 138 } 139 yield new InitRouterProvider(RainbowGum::of); 140 } 141 case REUSE -> new InitRouterProvider(() -> { 142 var gum = RainbowGum.getOrNull(); 143 if (gum == null) { 144 throw new IllegalStateException( 145 "SystemLogging was configured to reuse a loaded Rainbow Gum but none was found. " 146 + INITIALIZE_RAINBOW_GUM_PROPERTY + "=" + opt); 147 } 148 return gum; 149 }); 150 }; 151 this.routerProvider = rp; 152 return rp; 153 } 154 catch (Exception e) { 155 // We have to do this because it because very difficult 156 // to determine why the System Logging fails as it does not even print the 157 // exception. 158 var gum = RainbowGum.getOrNull(); 159 if (gum != null) { 160 gum.config().alerts().error(getClass(), "Failed to create System.LoggerFinder", e); 161 } 162 else { 163 System.err.println("[ERROR] - RAINBOW_GUM Failed to create System.LoggerFinder"); 164 e.printStackTrace(); 165 } 166 throw e; 167 } 168 } 169 170 @Override 171 public Logger getLogger(String name, Module module) { 172 var router = routerProvider().router(name); 173 if (!router.isChangeable(name)) { 174 var level = router.levelResolver().resolveLevel(name); 175 return LevelSystemLogger.of(name, level, router.route(name, level)); 176 } 177 return RainbowGumSystemLogger.of(name, router); 178 } 179 180 /** 181 * Gets the init option from properties. 182 * @param properties usually system properties. 183 * @return initialization option. 184 */ 185 protected static InitOption initOption(LogProperties properties) { 186 return properties.forKey(INITIALIZE_RAINBOW_GUM_PROPERTY) 187 .ofString() 188 .map(InitOption::parse) 189 .or(InitOption.CHECK) 190 .validateNow(RainbowGumSystemLoggerFinder.class); 191 } 192 193 private interface RouterProvider { 194 195 LogRouter.RootRouter router(String loggerName); 196 197 } 198 199 private static class InitRouterProvider implements RouterProvider { 200 201 private final Supplier<RainbowGum> supplier; 202 203 private volatile @Nullable RainbowGum gum = null; 204 205 InitRouterProvider(Supplier<RainbowGum> supplier) { 206 super(); 207 this.supplier = supplier; 208 } 209 210 @Override 211 public LogRouter.RootRouter router(String loggerName) { 212 LogRouter.RootRouter router; 213 RainbowGum gum = this.gum; 214 if (gum == null) { 215 gum = this.gum = supplier.get(); 216 } 217 gum.config().loggerRegistry().registerLoggerName(LoggerAPI.Standard.SYSTEM_LOGGER, loggerName); 218 if (gum.config().changePublisher().isEnabled(loggerName)) { 219 router = LogRouter.global(); 220 } 221 else { 222 router = gum.router(); 223 } 224 return router; 225 } 226 227 } 228 229}