001package io.jstach.rainbowgum;
002
003import java.lang.ref.WeakReference;
004import java.util.ArrayList;
005import java.util.List;
006import java.util.Objects;
007import java.util.Optional;
008import java.util.Queue;
009import java.util.ServiceLoader;
010import java.util.UUID;
011import java.util.concurrent.ConcurrentLinkedQueue;
012import java.util.concurrent.atomic.AtomicInteger;
013import java.util.concurrent.locks.ReentrantReadWriteLock;
014import java.util.function.Consumer;
015import java.util.function.Function;
016import java.util.function.Supplier;
017
018import org.eclipse.jdt.annotation.Nullable;
019
020import io.jstach.rainbowgum.LogProperty.Property;
021import io.jstach.rainbowgum.LogRouter.RootRouter;
022import io.jstach.rainbowgum.LogRouter.Router;
023import io.jstach.rainbowgum.spi.RainbowGumServiceProvider;
024
025//@formatter:off
026/**
027 * The main entry point and configuration of RainbowGum logging.
028 * <p>
029 * RainbowGum logging loads configuration through the service loader. While you can
030 * manually set RainbowGum using {@link #set(Supplier)} it is better to register
031 * implementations through the ServiceLoader so that RainbowGum will load prior to any
032 * external logging.
033 * <p>
034 * To register a custom RainbowGum:
035 *
036 *
037{@snippet class="snippets.RainbowGumProviderExample" region="provider" :
038
039class RainbowGumProviderExample implements RainbowGumProvider {
040
041        @Override
042        public Optional<RainbowGum> provide(LogConfig config) {
043
044                Property<Integer> bufferSize = Property.builder() //
045                        .ofInt()
046                        .orElse(1024)
047                        .build("logging.custom.async.bufferSize");
048
049                LogProvider<LogOutput> output = Property.builder()
050                        .ofProvider(LogOutput::of)
051                        .orElse(LogOutput.ofStandardOut())
052                        .withKey("logging.custom.output")
053                        .provider(o -> o);
054
055                var gum = RainbowGum.builder() //
056                        .route(r -> {
057                                r.publisher(PublisherFactory //
058                                        .async() //
059                                        .bufferSize(r.value(bufferSize)) //
060                                        .build());
061                                r.appender("console", a -> {
062                                        a.output(output);
063                                });
064                                r.level(Level.INFO);
065                        })
066                        .build();
067
068                return Optional.of(gum);
069        }
070
071}
072}
073 */
074//@formatter:on
075@SuppressWarnings("InvalidInlineTag")
076public sealed interface RainbowGum extends AutoCloseable, LogEventLogger {
077
078        /**
079         * Gets the currently statically bound RainbowGum and will try to load and find one if
080         * there is none currently bound. <strong> This is a blocking operation as in locks
081         * are used and will block indefinitely till a gum has loaded. </strong> If that is
082         * not desired see {@link #getOrNull()}.
083         * @return current RainbowGum or new loaded one.
084         */
085        public static RainbowGum of() {
086                return RainbowGumHolder.get();
087        }
088
089        /**
090         * Gets the currently statically bound RainbowGum or <code>null</code> if none are
091         * <strong>finished binding</strong>. Unlike {@link #of()} this will never load or
092         * start an instance but rather just gets the currently bound one. It will also never
093         * block and does not wait if one is currently being loaded.
094         * @return current RainbowGum or <code>null</code>
095         */
096        public static @Nullable RainbowGum getOrNull() {
097                return RainbowGumHolder.current();
098        }
099
100        /**
101         * Provides the service loader default based RainbowGum.
102         * @return RainbowGum.
103         */
104        public static RainbowGum defaults() {
105                return RainbowGumServiceProvider.provide();
106        }
107
108        /**
109         * Creates a RainbowGum that will <strong>ALWAYS</strong> use the global router.
110         * @param config config.
111         * @return rainbow gum that always uses global router.
112         */
113        public static RainbowGum queued(LogConfig config) {
114                return new SimpleRainbowGum(config, LogRouter.global(), UUID.randomUUID());
115        }
116
117        /**
118         * Sets the global default RainbowGum. The supplied {@link RainbowGum} must not
119         * already be started.
120         * @param supplier the supplier will be memoized when {@linkplain Supplier#get()
121         * accessed} and {@link RainbowGum#start()} will be called.
122         */
123        public static void set(Supplier<RainbowGum> supplier) {
124                RainbowGumHolder.set(supplier);
125        }
126
127        /**
128         * Subscribes to notification of a new RainbowGum becoming the globally bound one (see
129         * {@link #set(Supplier)}, {@link #set(RainbowGum)}, {@link Builder#set()}). This is
130         * primarily for components like SLF4J's logger factory that are themselves
131         * bootstrapped once (often before "the real" RainbowGum has loaded, e.g. a
132         * framework's own pre-boot sequence) and otherwise have no way of finding out that a
133         * different RainbowGum has since replaced the one they captured.
134         * <p>
135         * Only a {@linkplain java.lang.ref.WeakReference weak reference} to
136         * <code>consumer</code> is retained, so subscribing does not by itself keep the
137         * consumer (or whatever it is attached to) reachable - the caller is responsible for
138         * keeping a strong reference to <code>consumer</code> for as long as it needs to keep
139         * receiving notifications, otherwise it may silently stop being called once garbage
140         * collected.
141         * @param consumer called with the newly bound global RainbowGum. Never called while
142         * any RainbowGum internal lock is held.
143         */
144        public static void onGlobalChange(Consumer<RainbowGum> consumer) {
145                RainbowGumHolder.globalChangePublisher.add(consumer);
146        }
147
148        /**
149         * Sets a Rainbow Gum and provides a supplier that will start it globally.
150         * @param gum that will be set immediatly.
151         * @return supplier that will set, get and start the supplied gum and if it is not the
152         * same will throw {@link IllegalStateException}.
153         */
154        public static Supplier<? extends RainbowGum> set(RainbowGum gum) {
155                UUID instanceId = gum.instanceId();
156                RainbowGum.set(() -> gum);
157                return () -> {
158                        var of = RainbowGum.of();
159                        /*
160                         * TODO this is a hack. The holder lock should be used to make this not
161                         * happen.
162                         */
163                        if (!instanceId.equals(of.instanceId())) {
164                                throw new IllegalStateException("Another rainbow gum registered itself as the global. "
165                                                + "This is rare reace condition and probably a bug");
166                        }
167                        return of;
168                };
169        }
170
171        /**
172         * The config associated with this instance.
173         * @return config.
174         */
175        public LogConfig config();
176
177        /**
178         * The router that will route log messages to publishers.
179         * @return router
180         */
181        public RootRouter router();
182
183        /**
184         * Starts the rainbow gum and returns it. It is returned for try-with usage
185         * convenience.
186         * @return the started RainbowGum.
187         */
188        default RainbowGum start() {
189                router().start(config());
190                return this;
191        }
192
193        /**
194         * Unique id of rainbow gum instance.
195         * @return random id created on creation.
196         */
197        public UUID instanceId();
198
199        /**
200         * Will close the RainbowGum and all registered components as well as removed from the
201         * shutdown hooks. If the rainbow gum is set as global it will no longer be global and
202         * replaced with the bootstrapping in memory queue. {@inheritDoc}
203         */
204        @Override
205        public void close();
206
207        /**
208         * This append call is mainly for testing as it does not avoid making events that do
209         * not need to be made if no logging needs to be done. {@inheritDoc}
210         */
211        @Override
212        default void log(LogEvent event) {
213                var r = router().route(event.loggerName(), event.level());
214                if (r.isEnabled()) {
215                        r.log(event);
216                }
217        }
218
219        /**
220         * Use to build a custom {@link RainbowGum} which will use the {@link LogConfig}
221         * provided by the service loader.
222         * @return builder.
223         */
224        public static Builder builder() {
225                ServiceLoader<RainbowGumServiceProvider> loader = ServiceLoader.load(RainbowGumServiceProvider.class);
226                var config = RainbowGumServiceProvider.provideConfig(loader);
227                return builder(config);
228        }
229
230        /**
231         * Use to build a custom {@link RainbowGum} with supplied config.
232         * @param config the config
233         * @return builder.
234         * @see #builder()
235         */
236        public static Builder builder(LogConfig config) {
237                return new Builder(config);
238        }
239
240        /**
241         * Use to build a custom {@link RainbowGum} with supplied config.
242         * @param config consumer that has first argument as config builder.
243         * @return builder.
244         * @see #builder()
245         * @apiNote this method is for ergonomic fluent reasons.
246         */
247        public static Builder builder(Consumer<? super LogConfig.Builder> config) {
248                var b = LogConfig.builder();
249                config.accept(b);
250                return builder(b.build());
251        }
252
253        /**
254         * RainbowGum Builder.
255         */
256        public class Builder {
257
258                private final LogConfig config;
259
260                private final List<Router> routes = new ArrayList<>();
261
262                private Builder(LogConfig config) {
263                        this.config = config;
264                }
265
266                /**
267                 * Adds a router.
268                 * @param route a router.
269                 * @return builder.
270                 */
271                public Builder route(Router route) {
272                        this.routes.add(route);
273                        return this;
274                }
275
276                /**
277                 * Adds a route by using a consumer of the route builder.
278                 * @param name name of router.
279                 * @param consumer consumer is passed router builder. The consumer does not need
280                 * to call {@link Router#builder(String,LogConfig)}
281                 * @return builder.
282                 * @see io.jstach.rainbowgum.LogRouter.Router.Builder
283                 */
284                public Builder route(String name, Consumer<Router.Builder> consumer) {
285                        var builder = Router.builder(name, config);
286                        consumer.accept(builder);
287                        return route(builder.build());
288                }
289
290                /**
291                 * Adds a route by using a consumer of the route builder.
292                 * @param consumer consumer is passed router builder. The consumer does not need
293                 * to call {@link Router#builder(String,LogConfig)}
294                 * @return builder.
295                 * @see io.jstach.rainbowgum.LogRouter.Router.Builder
296                 */
297                public Builder route(Consumer<Router.Builder> consumer) {
298                        var builder = Router.builder(Router.DEFAULT_ROUTER_NAME, config);
299                        consumer.accept(builder);
300                        return route(builder.build());
301                }
302
303                /**
304                 * Builds an un-started {@link RainbowGum}.
305                 * @return an un-started {@link RainbowGum}.
306                 */
307                public RainbowGum build() {
308                        return build(UUID.randomUUID());
309                }
310
311                /**
312                 * Builds an un-started {@link RainbowGum}.
313                 * @param instanceId unique id for rainbow gum instance.
314                 * @return an un-started {@link RainbowGum}.
315                 */
316                private RainbowGum build(UUID instanceId) {
317                        var routes = this.routes;
318                        var config = this.config;
319                        if (routes.isEmpty()) {
320                                List<String> routeNames = Property.builder() //
321                                        .ofList()
322                                        .build(LogProperties.ROUTES_PROPERTY)
323                                        .get(config.properties())
324                                        .value(List.of());
325                                if (routeNames.isEmpty()) {
326                                        routes = List.of(Router.builder(Router.DEFAULT_ROUTER_NAME, config).build());
327                                }
328                                else {
329                                        routes = routeNames.stream().map(n -> Router.builder(n, config).build()).toList();
330                                }
331                        }
332                        var root = InternalRootRouter.of(routes, config);
333                        config.changePublisher().subscribe(c -> {
334                                root.changePublisher().publish(root);
335                        });
336                        return new SimpleRainbowGum(config, root, instanceId);
337                }
338
339                /**
340                 * Builds, starts and sets the RainbowGum as the global one picked up by logging
341                 * facades.
342                 * @return started and set rainbow that can be used in a try-close.
343                 */
344                public RainbowGum set() {
345                        UUID instanceId = UUID.randomUUID();
346                        RainbowGum.set(() -> build(instanceId));
347                        var gum = RainbowGum.of();
348                        /*
349                         * TODO this is a hack. The holder lock should be used to make this not
350                         * happen.
351                         */
352                        if (!instanceId.equals(gum.instanceId())) {
353                                throw new IllegalStateException("Another rainbow gum registered itself as the global. "
354                                                + "This is rare reace condition and probably a bug");
355                        }
356                        return gum;
357                }
358
359                /**
360                 * Removes the currently set Rainbow Gum which will close it if one exists and
361                 * will be replaced by the default provision process if {@link RainbowGum#of()} is
362                 * called before {@link #set()}.
363                 * @apiNote This is largely an internal detail for unit testing the provision
364                 * process.
365                 */
366                public void unset() {
367                        RainbowGumHolder.remove(null);
368                }
369
370                /**
371                 * For returning an optional for the LogProvider contract.
372                 * @return optional that always has a rainbow gum.
373                 * @apiNote this method is for ergonomics.
374                 */
375                public Optional<RainbowGum> optional() {
376                        return Optional.of(this.build());
377                }
378
379                /**
380                 * For returning an optional for the LogProvider contract.
381                 * @param condition condition to check if this rainbow gum should be used.
382                 * @return optional rainbow gum.
383                 * @apiNote this method is for ergonomics.
384                 */
385                public Optional<RainbowGum> optional(Function<? super LogConfig, Boolean> condition) {
386                        var cond = condition.apply(config);
387                        if (cond) {
388                                return Optional.of(this.build());
389                        }
390                        return Optional.empty();
391                }
392
393        }
394
395}
396
397final class RainbowGumHolder {
398
399        /*
400         * TODO perhaps a StampedLock would be better performance wise.
401         */
402        private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
403
404        private static Supplier<RainbowGum> supplier = RainbowGumServiceProvider::provide;
405
406        private static volatile @Nullable RainbowGum rainbowGum = null;
407
408        static final GlobalChangePublisher globalChangePublisher = new GlobalChangePublisher();
409
410        static @Nullable RainbowGum current() {
411                if (lock.readLock().tryLock()) {
412                        try {
413                                return rainbowGum;
414                        }
415                        finally {
416                                lock.readLock().unlock();
417                        }
418                }
419                return null;
420        }
421
422        static RainbowGum get() {
423                lock.readLock().lock();
424                try {
425                        var r = rainbowGum;
426                        if (r != null) {
427                                return r;
428                        }
429                }
430                finally {
431                        lock.readLock().unlock();
432                }
433                if (lock.writeLock().isHeldByCurrentThread()) {
434                        throw new IllegalStateException("RainbowGum component tried to log too early. "
435                                        + "This is usually caused by dependencies calling logging.");
436                }
437                RainbowGum newlyStarted = null;
438                lock.writeLock().lock();
439                try {
440                        var r = rainbowGum;
441                        if (r != null) {
442                                return r;
443                        }
444                        r = supplier.get();
445                        start(r);
446                        rainbowGum = r;
447                        newlyStarted = r;
448                        return r;
449
450                }
451                finally {
452                        lock.writeLock().unlock();
453                        /*
454                         * Published outside of the write lock so a subscriber can never deadlock or
455                         * trip the "tried to log too early" reentrancy guard above by, say, trying to
456                         * read a volatile field it owns - it is not calling back into this class.
457                         */
458                        if (newlyStarted != null) {
459                                globalChangePublisher.publish(newlyStarted);
460                        }
461                }
462
463        }
464
465        static boolean remove(@Nullable RainbowGum gum) {
466                lock.writeLock().lock();
467                try {
468                        var original = rainbowGum;
469                        if (gum != null) {
470                                if (original != gum) {
471                                        return false;
472                                }
473                        }
474                        /*
475                         * Reset the global router
476                         */
477                        if (original != null) {
478                                LogRouter.global().close();
479                        }
480                        rainbowGum = null;
481                        supplier = RainbowGumServiceProvider::provide;
482                        return true;
483                }
484                finally {
485                        lock.writeLock().unlock();
486                }
487        }
488
489        static void set(Supplier<RainbowGum> rainbowGumSupplier) {
490                Objects.requireNonNull(rainbowGumSupplier);
491                if (lock.writeLock().isHeldByCurrentThread()) {
492                        throw new IllegalStateException("RainbowGum component tried to log too early. "
493                                        + "This is usually caused by dependencies calling logging.");
494                }
495                lock.writeLock().lock();
496                try {
497                        rainbowGum = null;
498                        supplier = rainbowGumSupplier;
499                }
500                finally {
501                        lock.writeLock().unlock();
502                }
503        }
504
505        private static void start(RainbowGum gum) {
506                Objects.requireNonNull(gum);
507                ShutdownManager.addShutdownHook(gum);
508                gum.start();
509                InternalRootRouter.setRouter(gum.router());
510        }
511
512}
513
514/*
515 * Unlike LogConfig.ChangePublisher/LogRouter.RouteChangePublisher (both scoped to a
516 * single RainbowGum/router instance and torn down with it) this is a JVM-lifetime static
517 * registry, so consumers are held only weakly - otherwise every SLF4J logger factory (or
518 * any other subscriber) ever created over the life of the JVM, e.g. across many
519 * short-lived RainbowGums in tests, would be pinned forever.
520 */
521final class GlobalChangePublisher {
522
523        private final Queue<WeakReference<Consumer<RainbowGum>>> consumers = new ConcurrentLinkedQueue<>();
524
525        void add(Consumer<RainbowGum> consumer) {
526                consumers.add(new WeakReference<>(consumer));
527        }
528
529        void publish(RainbowGum gum) {
530                for (var it = consumers.iterator(); it.hasNext();) {
531                        var consumer = it.next().get();
532                        if (consumer == null) {
533                                it.remove();
534                        }
535                        else {
536                                consumer.accept(gum);
537                        }
538                }
539        }
540
541}
542
543final class SimpleRainbowGum implements RainbowGum, Shutdownable {
544
545        private final LogConfig config;
546
547        private final RootRouter router;
548
549        private final AtomicInteger state = new AtomicInteger(0);
550
551        private final UUID instanceId;
552
553        private static final int INIT = 0;
554
555        private static final int STARTED = 1;
556
557        private static final int CLOSED = 2;
558
559        public SimpleRainbowGum(LogConfig config, RootRouter router, UUID instanceId) {
560                super();
561                this.config = config;
562                this.router = router;
563                this.instanceId = instanceId;
564        }
565
566        @Override
567        public LogConfig config() {
568                return this.config;
569        }
570
571        @Override
572        public RootRouter router() {
573                return this.router;
574        }
575
576        @Override
577        public RainbowGum start() {
578                int current;
579                if ((current = state.compareAndExchange(INIT, STARTED)) == INIT) {
580                        return RainbowGum.super.start();
581                }
582                throw new IllegalStateException("Cannot start. This rainbowgum is " + stateLabel(current));
583        }
584
585        @Override
586        public UUID instanceId() {
587                return this.instanceId;
588        }
589
590        @Override
591        public void close() {
592                if (state.compareAndSet(STARTED, CLOSED)) {
593                        RainbowGumHolder.remove(this);
594                        try {
595                                shutdown();
596                        }
597                        finally {
598                                ShutdownManager.removeShutdownHook(this);
599                        }
600                        return;
601                }
602        }
603
604        @Override
605        public void shutdown() {
606                router().close();
607                config().serviceRegistry().close();
608        }
609
610        @Override
611        public String toString() {
612                return "SimpleRainbowGum [instanceId=" + instanceId + ", config=" + config + ", router=" + router + ", state="
613                                + stateLabel(state.get()) + "]";
614        }
615
616        private static String stateLabel(int state) {
617                return switch (state) {
618                        case INIT -> "created";
619                        case STARTED -> "started";
620                        case CLOSED -> "closed";
621                        default -> {
622                                throw new IllegalArgumentException("" + state);
623                        }
624                };
625        }
626
627}