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