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}