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.jspecify.annotations.Nullable; 019 020import io.jstach.rainbowgum.LogRouter.RootRouter; 021import io.jstach.rainbowgum.LogRouter.Router; 022import io.jstach.rainbowgum.spi.RainbowGumServiceProvider; 023 024//@formatter:off 025/** 026 * The main entry point and configuration of RainbowGum logging. 027 * <p> 028 * RainbowGum logging loads configuration through the service loader. While you can 029 * manually set RainbowGum using {@link #set(Supplier)} it is better to register 030 * implementations through the ServiceLoader so that RainbowGum will load prior to any 031 * external logging. 032 * <p> 033 * To register a custom RainbowGum: 034 * 035 * 036{@snippet class="snippets.RainbowGumProviderExample" region="provider" : 037 038class RainbowGumProviderExample implements RainbowGumProvider { 039 040 @Override 041 public Optional<RainbowGum> provide(LogConfig config) { 042 043 Integer bufferSize = config.properties() // 044 .forKey("logging.custom.async.bufferSize") 045 .ofInt() 046 .or(1024) 047 .validateNow(RainbowGumProviderExample.class); 048 049 LogProvider<LogOutput> output = (name, cfg) -> cfg.properties() 050 .forKey("logging.custom.output") 051 .ofProvider(LogOutput::of) 052 .or(LogOutput.ofStandardOut()) 053 .validateNow(RainbowGumProviderExample.class) 054 .provide(name, cfg); 055 056 var gum = RainbowGum.builder() // 057 .route(r -> { 058 r.publisher(PublisherFactory // 059 .async() // 060 .bufferSize(bufferSize) // 061 .build()); 062 r.appender("console", a -> { 063 a.output(output); 064 }); 065 r.level(Level.INFO); 066 }) 067 .build(); 068 069 return Optional.of(gum); 070 } 071 072} 073} 074 * <p> 075 * <strong>Initialization order</strong> - most of the actual work happens in 076 * {@link LogConfig.Builder#build()}, {@link LogRouter.Router.Builder}, and 077 * {@code LogAppenderRegistry}; this is the exact sequence for anyone debugging a 078 * configuration or writing a {@link RainbowGumServiceProvider.RainbowGumProvider}: 079 * <ol> 080 * <li>{@link LogConfig.Builder#build()} builds the shared {@link LogConfig}: 081 * <ol> 082 * <li>Resolve the {@link ServiceRegistry} (default: a new empty one) and 083 * {@link LogProperties} - manually added 084 * {@link RainbowGumServiceProvider.PropertiesProvider}s, else (if a 085 * {@link java.util.ServiceLoader} was supplied) SPI-discovered ones, with system 086 * properties always layered in as a final fallback.</li> 087 * <li>Build {@link LogAlerts} and {@link LogMetrics} from properties.</li> 088 * <li>Build the global level resolver ({@code logging.level.*} and 089 * {@code logging.group.*}, plus anything added directly on the builder).</li> 090 * <li>Construct the {@link LogConfig} itself - this is also where a few 091 * process-wide flags get latched (config change support, the appender lock, and 092 * thread-local-disabled globals) and the default output/encoder/publisher 093 * registries are created.</li> 094 * <li>Run every {@link RainbowGumServiceProvider.Configurator} (manually added, 095 * then SPI-discovered), highest {@linkplain RainbowGumServiceProvider.Configurator#priority() 096 * priority} first. A configurator returning {@code false} is retried in a later 097 * pass (see {@link RainbowGumServiceProvider#PASSES}) - useful if it depends on 098 * another configurator running first.</li> 099 * <li>Start {@link LogAlerts} - deliberately last, so a configurator's own alert 100 * listener is already registered before alerts decides whether an unobserved 101 * error should refuse startup.</li> 102 * </ol> 103 * </li> 104 * <li>{@link Builder#build()} builds each named {@link LogRouter.Router} (a single 105 * route named {@value LogProperties#DEFAULT_NAME}, unless {@code logging.routes} 106 * names several) via {@link LogRouter.Router.Builder}'s own (package-private) 107 * {@code build()}: 108 * <ol> 109 * <li>Resolve route flags ({@code logging.route.<name>.flags}).</li> 110 * <li>Build a route-scoped level resolver: anything configured directly on the 111 * route builder, layered over {@code logging.route.<name>.level.*}, falling back 112 * to the global level resolver from step 1 (skipped entirely if the route flag 113 * {@code IGNORE_GLOBAL_LEVEL_RESOLVER} is set) - and finally to {@code INFO} if 114 * nothing at all resolves a level.</li> 115 * <li>Resolve appenders - only if none were added programmatically on the route 116 * builder (that check is all-or-nothing: adding even one appender in code skips 117 * property-based resolution entirely). See <strong>Appender resolution</strong> 118 * below.</li> 119 * <li>Resolve the publisher - {@code logging.route.<name>.publisher}, falling 120 * back to a synchronous publisher if unset.</li> 121 * <li>Hand the resolved appenders to the publisher and register the publisher in 122 * the {@link ServiceRegistry}.</li> 123 * </ol> 124 * </li> 125 * <li>{@link #start()} starts the root router, which starts each route's 126 * publisher, which starts each of its appenders, which starts its output (e.g. 127 * opening a file).</li> 128 * </ol> 129 * <strong>Appender resolution</strong> - the part most often mistaken for a bug, 130 * since no appenders are ever added programmatically in the common case: 131 * <ol> 132 * <li>{@code logging.route.<name>.appenders} names the route's appenders; for the 133 * {@value LogProperties#DEFAULT_NAME} route only, a missing value falls back to 134 * the unprefixed {@code logging.appenders}.</li> 135 * <li>If that is <em>still</em> missing: for the default route only, the list 136 * defaults to {@code [file, console]} if {@code logging.file.name} (or Spring 137 * Boot's convention) is set, otherwise just {@code [console]}. A non-default 138 * route with no appenders configured anywhere is a startup failure, not a silent 139 * no-op.</li> 140 * <li>Each named appender then resolves independently. {@code file} and 141 * {@code console} are the two built-in special cases, each with its own output 142 * property/fallback ({@code logging.file.name} then 143 * {@code logging.appender.file.output} for {@code file}; 144 * {@code logging.appender.console.output} falling back to standard-out for 145 * {@code console}). Any other name is fully generic: 146 * {@code logging.appender.<name>.output}/{@code .encoder}/{@code .flags}/ 147 * {@code .type}, with the output's own URI scheme choosing which registered 148 * {@link LogOutput.OutputProvider} builds it.</li> 149 * <li>An encoder is only looked up if the resolved output does not already 150 * implement {@link LogEncoder} itself (rare, e.g. some structured outputs); 151 * otherwise it is used as-is and the {@code .encoder} property is never 152 * consulted.</li> 153 * </ol> 154 */ 155//@formatter:on 156@SuppressWarnings("InvalidInlineTag") 157public sealed interface RainbowGum extends AutoCloseable, LogEventLogger { 158 159 /** 160 * Gets the currently statically bound RainbowGum and will try to load and find one if 161 * there is none currently bound. <strong> This is a blocking operation as in locks 162 * are used and will block indefinitely till a gum has loaded. </strong> If that is 163 * not desired see {@link #getOrNull()}. 164 * @return current RainbowGum or new loaded one. 165 */ 166 public static RainbowGum of() { 167 return RainbowGumHolder.get(); 168 } 169 170 /** 171 * Gets the currently statically bound RainbowGum or <code>null</code> if none are 172 * <strong>finished binding</strong>. Unlike {@link #of()} this will never load or 173 * start an instance but rather just gets the currently bound one. It will also never 174 * block and does not wait if one is currently being loaded. 175 * @return current RainbowGum or <code>null</code> 176 */ 177 public static @Nullable RainbowGum getOrNull() { 178 return RainbowGumHolder.current(); 179 } 180 181 /** 182 * Provides the service loader default based RainbowGum. 183 * @return RainbowGum. 184 */ 185 public static RainbowGum defaults() { 186 return RainbowGumServiceProvider.provide(); 187 } 188 189 /** 190 * Creates a RainbowGum that will <strong>ALWAYS</strong> use the global router. 191 * @param config config. 192 * @return rainbow gum that always uses global router. 193 */ 194 public static RainbowGum queued(LogConfig config) { 195 return new SimpleRainbowGum(config, LogRouter.global(), UUID.randomUUID()); 196 } 197 198 /** 199 * Sets the global default RainbowGum. The supplied {@link RainbowGum} must not 200 * already be started. 201 * @param supplier the supplier will be memoized when {@linkplain Supplier#get() 202 * accessed} and {@link RainbowGum#start()} will be called. 203 */ 204 public static void set(Supplier<RainbowGum> supplier) { 205 RainbowGumHolder.set(supplier); 206 } 207 208 /** 209 * Subscribes to notification of a new RainbowGum becoming the globally bound one (see 210 * {@link #set(Supplier)}, {@link #set(RainbowGum)}, {@link Builder#set()}). This is 211 * primarily for components like SLF4J's logger factory that are themselves 212 * bootstrapped once (often before "the real" RainbowGum has loaded, e.g. a 213 * framework's own pre-boot sequence) and otherwise have no way of finding out that a 214 * different RainbowGum has since replaced the one they captured. 215 * <p> 216 * Only a {@linkplain java.lang.ref.WeakReference weak reference} to 217 * <code>consumer</code> is retained, so subscribing does not by itself keep the 218 * consumer (or whatever it is attached to) reachable - the caller is responsible for 219 * keeping a strong reference to <code>consumer</code> for as long as it needs to keep 220 * receiving notifications, otherwise it may silently stop being called once garbage 221 * collected. 222 * @param consumer called with the newly bound global RainbowGum. Never called while 223 * any RainbowGum internal lock is held. 224 */ 225 public static void onGlobalChange(Consumer<RainbowGum> consumer) { 226 RainbowGumHolder.globalChangePublisher.add(consumer); 227 } 228 229 /** 230 * Sets a Rainbow Gum and provides a supplier that will start it globally. 231 * @param gum that will be set immediatly. 232 * @return supplier that will set, get and start the supplied gum and if it is not the 233 * same will throw {@link IllegalStateException}. 234 */ 235 public static Supplier<? extends RainbowGum> set(RainbowGum gum) { 236 UUID instanceId = gum.instanceId(); 237 RainbowGum.set(() -> gum); 238 return () -> { 239 var of = RainbowGum.of(); 240 /* 241 * TODO this is a hack. The holder lock should be used to make this not 242 * happen. 243 */ 244 if (!instanceId.equals(of.instanceId())) { 245 throw new IllegalStateException("Another rainbow gum registered itself as the global. " 246 + "This is rare reace condition and probably a bug"); 247 } 248 return of; 249 }; 250 } 251 252 /** 253 * The config associated with this instance. 254 * @return config. 255 */ 256 public LogConfig config(); 257 258 /** 259 * The router that will route log messages to publishers. 260 * @return router 261 */ 262 public RootRouter router(); 263 264 /** 265 * Starts the rainbow gum and returns it. It is returned for try-with usage 266 * convenience. 267 * @return the started RainbowGum. 268 */ 269 default RainbowGum start() { 270 router().start(config()); 271 return this; 272 } 273 274 /** 275 * Unique id of rainbow gum instance. 276 * @return random id created on creation. 277 */ 278 public UUID instanceId(); 279 280 /** 281 * Will close the RainbowGum and all registered components as well as removed from the 282 * shutdown hooks. If the rainbow gum is set as global it will no longer be global and 283 * replaced with the bootstrapping in memory queue. {@inheritDoc} 284 */ 285 @Override 286 public void close(); 287 288 /** 289 * This append call is mainly for testing as it does not avoid making events that do 290 * not need to be made if no logging needs to be done. {@inheritDoc} 291 */ 292 @Override 293 default void log(LogEvent event) { 294 var r = router().route(event.loggerName(), event.level()); 295 if (r.isEnabled()) { 296 r.log(event); 297 } 298 } 299 300 /** 301 * Use to build a custom {@link RainbowGum} which will use the {@link LogConfig} 302 * provided by the service loader. 303 * @return builder. 304 */ 305 public static Builder builder() { 306 ServiceLoader<RainbowGumServiceProvider> loader = ServiceLoader.load(RainbowGumServiceProvider.class); 307 var config = RainbowGumServiceProvider.provideConfig(loader); 308 return builder(config); 309 } 310 311 /** 312 * Use to build a custom {@link RainbowGum} with supplied config. 313 * @param config the config 314 * @return builder. 315 * @see #builder() 316 */ 317 public static Builder builder(LogConfig config) { 318 return new Builder(config); 319 } 320 321 /** 322 * Use to build a custom {@link RainbowGum} with supplied config. 323 * @param config consumer that has first argument as config builder. 324 * @return builder. 325 * @see #builder() 326 * @apiNote this method is for ergonomic fluent reasons. 327 */ 328 public static Builder builder(Consumer<? super LogConfig.Builder> config) { 329 var b = LogConfig.builder(); 330 config.accept(b); 331 return builder(b.build()); 332 } 333 334 /** 335 * RainbowGum Builder. 336 */ 337 public class Builder { 338 339 private final LogConfig config; 340 341 private final List<Router> routes = new ArrayList<>(); 342 343 private Builder(LogConfig config) { 344 this.config = config; 345 } 346 347 /** 348 * Adds a router. 349 * @param route a router. 350 * @return builder. 351 */ 352 public Builder route(Router route) { 353 this.routes.add(route); 354 return this; 355 } 356 357 /** 358 * Adds a route by using a consumer of the route builder. 359 * @param name name of router. 360 * @param consumer consumer is passed router builder. The consumer does not need 361 * to call {@link Router#builder(String,LogConfig)} 362 * @return builder. 363 * @see io.jstach.rainbowgum.LogRouter.Router.Builder 364 */ 365 public Builder route(String name, Consumer<Router.Builder> consumer) { 366 var builder = Router.builder(name, config); 367 consumer.accept(builder); 368 return route(builder.build()); 369 } 370 371 /** 372 * Adds a route by using a consumer of the route builder. 373 * @param consumer consumer is passed router builder. The consumer does not need 374 * to call {@link Router#builder(String,LogConfig)} 375 * @return builder. 376 * @see io.jstach.rainbowgum.LogRouter.Router.Builder 377 */ 378 public Builder route(Consumer<Router.Builder> consumer) { 379 var builder = Router.builder(Router.DEFAULT_ROUTER_NAME, config); 380 consumer.accept(builder); 381 return route(builder.build()); 382 } 383 384 /** 385 * Builds an un-started {@link RainbowGum}. 386 * @return an un-started {@link RainbowGum}. 387 */ 388 public RainbowGum build() { 389 return build(UUID.randomUUID()); 390 } 391 392 /** 393 * Builds an un-started {@link RainbowGum}. 394 * @param instanceId unique id for rainbow gum instance. 395 * @return an un-started {@link RainbowGum}. 396 */ 397 private RainbowGum build(UUID instanceId) { 398 var routes = this.routes; 399 var config = this.config; 400 if (routes.isEmpty()) { 401 var routeNamesResult = config.properties() // 402 .forKey(LogProperties.ROUTES_PROPERTY) 403 .ofList() 404 .or(List.of()); 405 List<String> routeNames = routeNamesResult.validateNow(Builder.class); 406 /* 407 * Validated as a plain list of names, before any of them is used to build 408 * a Router, so a bad name is reported against this list property itself, 409 * not deferred until some unrelated {name}-keyed property (e.g. this 410 * route's own level) happens to interpolate it first. 411 */ 412 LogProperty.Validator.validateNames(LogRouter.class, "route", routeNamesResult, routeNames); 413 if (routeNames.isEmpty()) { 414 routes = List.of(Router.builder(Router.DEFAULT_ROUTER_NAME, config).build()); 415 } 416 else { 417 routes = routeNames.stream().map(n -> Router.builder(n, config).build()).toList(); 418 } 419 } 420 var root = InternalRootRouter.of(routes, config); 421 config.changePublisher().subscribe(c -> { 422 root.changePublisher().publish(root); 423 }); 424 return new SimpleRainbowGum(config, root, instanceId); 425 } 426 427 /** 428 * Builds, starts and sets the RainbowGum as the global one picked up by logging 429 * facades. 430 * @return started and set rainbow that can be used in a try-close. 431 */ 432 public RainbowGum set() { 433 UUID instanceId = UUID.randomUUID(); 434 RainbowGum.set(() -> build(instanceId)); 435 var gum = RainbowGum.of(); 436 /* 437 * TODO this is a hack. The holder lock should be used to make this not 438 * happen. 439 */ 440 if (!instanceId.equals(gum.instanceId())) { 441 throw new IllegalStateException("Another rainbow gum registered itself as the global. " 442 + "This is rare reace condition and probably a bug"); 443 } 444 return gum; 445 } 446 447 /** 448 * Removes the currently set Rainbow Gum which will close it if one exists and 449 * will be replaced by the default provision process if {@link RainbowGum#of()} is 450 * called before {@link #set()}. 451 * @apiNote This is largely an internal detail for unit testing the provision 452 * process. 453 */ 454 public void unset() { 455 RainbowGumHolder.remove(null); 456 } 457 458 /** 459 * For returning an optional for the LogProvider contract. 460 * @return optional that always has a rainbow gum. 461 * @apiNote this method is for ergonomics. 462 */ 463 public Optional<RainbowGum> optional() { 464 return Optional.of(this.build()); 465 } 466 467 /** 468 * For returning an optional for the LogProvider contract. 469 * @param condition condition to check if this rainbow gum should be used. 470 * @return optional rainbow gum. 471 * @apiNote this method is for ergonomics. 472 */ 473 public Optional<RainbowGum> optional(Function<? super LogConfig, Boolean> condition) { 474 var cond = condition.apply(config); 475 if (cond) { 476 return Optional.of(this.build()); 477 } 478 return Optional.empty(); 479 } 480 481 } 482 483} 484 485final class RainbowGumHolder { 486 487 /* 488 * TODO perhaps a StampedLock would be better performance wise. 489 */ 490 private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); 491 492 private static Supplier<RainbowGum> supplier = RainbowGumServiceProvider::provide; 493 494 private static volatile @Nullable RainbowGum rainbowGum = null; 495 496 static final GlobalChangePublisher globalChangePublisher = new GlobalChangePublisher(); 497 498 static @Nullable RainbowGum current() { 499 if (lock.readLock().tryLock()) { 500 try { 501 return rainbowGum; 502 } 503 finally { 504 lock.readLock().unlock(); 505 } 506 } 507 return null; 508 } 509 510 static RainbowGum get() { 511 lock.readLock().lock(); 512 try { 513 var r = rainbowGum; 514 if (r != null) { 515 return r; 516 } 517 } 518 finally { 519 lock.readLock().unlock(); 520 } 521 if (lock.writeLock().isHeldByCurrentThread()) { 522 throw new IllegalStateException("RainbowGum component tried to log too early. " 523 + "This is usually caused by dependencies calling logging."); 524 } 525 RainbowGum newlyStarted = null; 526 lock.writeLock().lock(); 527 try { 528 var r = rainbowGum; 529 if (r != null) { 530 return r; 531 } 532 r = supplier.get(); 533 start(r); 534 rainbowGum = r; 535 newlyStarted = r; 536 return r; 537 538 } 539 finally { 540 lock.writeLock().unlock(); 541 /* 542 * Published outside of the write lock so a subscriber can never deadlock or 543 * trip the "tried to log too early" reentrancy guard above by, say, trying to 544 * read a volatile field it owns - it is not calling back into this class. 545 */ 546 if (newlyStarted != null) { 547 globalChangePublisher.publish(newlyStarted); 548 } 549 } 550 551 } 552 553 static boolean remove(@Nullable RainbowGum gum) { 554 lock.writeLock().lock(); 555 try { 556 var original = rainbowGum; 557 if (gum != null) { 558 if (original != gum) { 559 return false; 560 } 561 } 562 /* 563 * Reset the global router 564 */ 565 if (original != null) { 566 LogRouter.global().close(); 567 } 568 rainbowGum = null; 569 supplier = RainbowGumServiceProvider::provide; 570 return true; 571 } 572 finally { 573 lock.writeLock().unlock(); 574 } 575 } 576 577 static void set(Supplier<RainbowGum> rainbowGumSupplier) { 578 Objects.requireNonNull(rainbowGumSupplier); 579 if (lock.writeLock().isHeldByCurrentThread()) { 580 throw new IllegalStateException("RainbowGum component tried to log too early. " 581 + "This is usually caused by dependencies calling logging."); 582 } 583 lock.writeLock().lock(); 584 try { 585 rainbowGum = null; 586 supplier = rainbowGumSupplier; 587 } 588 finally { 589 lock.writeLock().unlock(); 590 } 591 } 592 593 private static void start(RainbowGum gum) { 594 Objects.requireNonNull(gum); 595 ShutdownManager.addShutdownHook(gum); 596 gum.start(); 597 InternalRootRouter.setRouter(gum.router()); 598 } 599 600} 601 602/* 603 * Unlike LogConfig.ChangePublisher/LogRouter.RouteChangePublisher (both scoped to a 604 * single RainbowGum/router instance and torn down with it) this is a JVM-lifetime static 605 * registry, so consumers are held only weakly - otherwise every SLF4J logger factory (or 606 * any other subscriber) ever created over the life of the JVM, e.g. across many 607 * short-lived RainbowGums in tests, would be pinned forever. 608 */ 609final class GlobalChangePublisher { 610 611 private final Queue<WeakReference<Consumer<RainbowGum>>> consumers = new ConcurrentLinkedQueue<>(); 612 613 void add(Consumer<RainbowGum> consumer) { 614 consumers.add(new WeakReference<>(consumer)); 615 } 616 617 void publish(RainbowGum gum) { 618 for (var it = consumers.iterator(); it.hasNext();) { 619 var consumer = it.next().get(); 620 if (consumer == null) { 621 it.remove(); 622 } 623 else { 624 consumer.accept(gum); 625 } 626 } 627 } 628 629} 630 631final class SimpleRainbowGum implements RainbowGum, Shutdownable { 632 633 private final LogConfig config; 634 635 private final RootRouter router; 636 637 private final AtomicInteger state = new AtomicInteger(0); 638 639 private final UUID instanceId; 640 641 private static final int INIT = 0; 642 643 private static final int STARTED = 1; 644 645 private static final int CLOSED = 2; 646 647 public SimpleRainbowGum(LogConfig config, RootRouter router, UUID instanceId) { 648 super(); 649 this.config = config; 650 this.router = router; 651 this.instanceId = instanceId; 652 } 653 654 @Override 655 public LogConfig config() { 656 return this.config; 657 } 658 659 @Override 660 public RootRouter router() { 661 return this.router; 662 } 663 664 @Override 665 public RainbowGum start() { 666 int current; 667 if ((current = state.compareAndExchange(INIT, STARTED)) == INIT) { 668 return RainbowGum.super.start(); 669 } 670 throw new IllegalStateException("Cannot start. This rainbowgum is " + stateLabel(current)); 671 } 672 673 @Override 674 public UUID instanceId() { 675 return this.instanceId; 676 } 677 678 @Override 679 public void close() { 680 if (state.compareAndSet(STARTED, CLOSED)) { 681 RainbowGumHolder.remove(this); 682 try { 683 shutdown(); 684 } 685 finally { 686 ShutdownManager.removeShutdownHook(this); 687 } 688 return; 689 } 690 } 691 692 @Override 693 public void shutdown() { 694 router().close(); 695 config().alerts().close(); 696 config().serviceRegistry().close(); 697 } 698 699 @Override 700 public String toString() { 701 return "SimpleRainbowGum [instanceId=" + instanceId + ", config=" + config + ", router=" + router + ", state=" 702 + stateLabel(state.get()) + "]"; 703 } 704 705 private static String stateLabel(int state) { 706 return switch (state) { 707 case INIT -> "created"; 708 case STARTED -> "started"; 709 case CLOSED -> "closed"; 710 default -> { 711 throw new IllegalArgumentException("" + state); 712 } 713 }; 714 } 715 716}