001package io.jstach.rainbowgum; 002 003import java.lang.System.Logger.Level; 004import java.lang.ref.WeakReference; 005import java.util.ArrayDeque; 006import java.util.ArrayList; 007import java.util.List; 008import java.util.Objects; 009import java.util.Optional; 010import java.util.Queue; 011import java.util.ServiceLoader; 012import java.util.UUID; 013import java.util.concurrent.ConcurrentLinkedQueue; 014import java.util.concurrent.atomic.AtomicInteger; 015import java.util.concurrent.locks.ReentrantReadWriteLock; 016import java.util.function.Consumer; 017import java.util.function.Function; 018import java.util.function.Supplier; 019 020import org.jspecify.annotations.Nullable; 021 022import io.jstach.rainbowgum.LogRouter.RootRouter; 023import io.jstach.rainbowgum.LogRouter.Router; 024import io.jstach.rainbowgum.spi.RainbowGumServiceProvider; 025 026//@formatter:off 027/** 028 * The main entry point and configuration of RainbowGum logging. 029 * <p> 030 * RainbowGum logging loads configuration through the service loader. While you can 031 * manually set RainbowGum using {@link #set(Supplier)} it is better to register 032 * implementations through the ServiceLoader so that RainbowGum will load prior to any 033 * external logging. 034 * <p> 035 * To register a custom RainbowGum: 036 * 037 * 038{@snippet class="snippets.RainbowGumProviderExample" region="provider" : 039 040class RainbowGumProviderExample implements RainbowGumProvider { 041 042 @Override 043 public Optional<RainbowGum> provide(LogConfig config) { 044 045 Integer bufferSize = config.properties() // 046 .forKey("logging.custom.async.bufferSize") 047 .ofInt() 048 .or(1024) 049 .validateNow(RainbowGumProviderExample.class); 050 051 LogProvider<LogOutput> output = (name, cfg) -> cfg.properties() 052 .forKey("logging.custom.output") 053 .ofProvider(LogOutput::of) 054 .or(LogOutput.ofStandardOut()) 055 .validateNow(RainbowGumProviderExample.class) 056 .provide(name, cfg); 057 058 var gum = RainbowGum.builder() // 059 .route(r -> { 060 r.publisher(PublisherFactory // 061 .async() // 062 .bufferSize(bufferSize) // 063 .build()); 064 r.appender("console", a -> { 065 a.output(output); 066 }); 067 r.level(Level.INFO); 068 }) 069 .build(); 070 071 return Optional.of(gum); 072 } 073 074} 075} 076 * <p> 077 * <strong>Initialization order</strong> - most of the actual work happens in 078 * {@link LogConfig.Builder#build()}, {@link LogRouter.Router.Builder}, and 079 * {@code LogAppenderRegistry}; this is the exact sequence for anyone debugging a 080 * configuration or writing a {@link RainbowGumServiceProvider.RainbowGumProvider}: 081 * <ol> 082 * <li>{@link LogConfig.Builder#build()} builds the shared {@link LogConfig}: 083 * <ol> 084 * <li>Resolve the {@link ServiceRegistry} (default: a new empty one) and 085 * {@link LogProperties} - manually added 086 * {@link RainbowGumServiceProvider.PropertiesProvider}s, else (if a 087 * {@link java.util.ServiceLoader} was supplied) SPI-discovered ones, with system 088 * properties always layered in as a final fallback. Providers receive a temporary 089 * {@link LogAlerts} collector.</li> 090 * <li>Build {@link LogAlerts} and {@link LogMetrics} from properties.</li> 091 * <li>Build the global level resolver ({@code logging.level.*} and 092 * {@code logging.group.*}, plus anything added directly on the builder).</li> 093 * <li>Construct the {@link LogConfig} itself and replay provider alerts into its 094 * configured {@link LogAlerts}. This is also where the {@link LogConfig#globals() 095 * globals} are resolved and the default output/encoder/publisher registries are 096 * created.</li> 097 * <li>Run every {@link RainbowGumServiceProvider.Configurator} (manually added, 098 * then SPI-discovered), highest {@linkplain RainbowGumServiceProvider.Configurator#priority() 099 * priority} first. A configurator returning {@code false} is retried in a later 100 * pass (see {@link RainbowGumServiceProvider#PASSES}) - useful if it depends on 101 * another configurator running first.</li> 102 * <li>Start {@link LogAlerts} - deliberately last, so a configurator's own alert 103 * listener is already registered before alerts decides whether an unobserved 104 * error should refuse startup.</li> 105 * </ol> 106 * </li> 107 * <li>{@link Builder#build()} builds each named {@link LogRouter.Router} (a single 108 * route named {@value LogProperties#DEFAULT_NAME}, unless {@code logging.routes} 109 * names several) via {@link LogRouter.Router.Builder}'s own (package-private) 110 * {@code build()}: 111 * <ol> 112 * <li>Resolve route flags ({@code logging.route.<name>.flags}).</li> 113 * <li>Build a route-scoped level resolver: anything configured directly on the 114 * route builder, layered over {@code logging.route.<name>.level.*}, falling back 115 * to the global level resolver from step 1 (skipped entirely if the route flag 116 * {@code IGNORE_GLOBAL_LEVEL_RESOLVER} is set) - and finally to {@code INFO} if 117 * nothing at all resolves a level.</li> 118 * <li>Resolve appenders - only if none were added programmatically on the route 119 * builder (that check is all-or-nothing: adding even one appender in code skips 120 * property-based resolution entirely). See <strong>Appender resolution</strong> 121 * below.</li> 122 * <li>Resolve the publisher - {@code logging.route.<name>.publisher.uri}, falling 123 * back to a synchronous publisher if unset.</li> 124 * <li>Hand the resolved appenders to the publisher and register the publisher in 125 * the {@link ServiceRegistry}.</li> 126 * </ol> 127 * </li> 128 * <li>{@link #start()} starts the root router, which starts each route's 129 * publisher, which starts each of its appenders, which starts its output (e.g. 130 * opening a file).</li> 131 * </ol> 132 * <strong>Appender resolution</strong> - the part most often mistaken for a bug, 133 * since no appenders are ever added programmatically in the common case: 134 * <ol> 135 * <li>{@code logging.route.<name>.appenders} names the route's appenders; for the 136 * {@value LogProperties#DEFAULT_NAME} route only, a missing value falls back to 137 * the unprefixed {@code logging.appenders}.</li> 138 * <li>If that is <em>still</em> missing: for the default route only, the list 139 * defaults to {@code [file, console]} if {@code logging.file.name} (or Spring 140 * Boot's convention) is set, otherwise just {@code [console]}. A non-default 141 * route with no appenders configured anywhere is a startup failure, not a silent 142 * no-op.</li> 143 * <li>Each named appender then resolves independently. {@code file} and 144 * {@code console} are the two built-in special cases, each with its own output 145 * property/fallback ({@code logging.file.name} then 146 * {@code logging.appender.file.output.uri} for {@code file}; 147 * {@code logging.appender.console.output.uri} falling back to standard-out for 148 * {@code console}). Any other name is fully generic: 149 * {@code logging.appender.<name>.output.uri}/{@code .encoder.uri}/{@code .flags}/ 150 * {@code .type}, with the output's own URI scheme choosing which registered 151 * {@link LogOutput.OutputProvider} builds it.</li> 152 * <li>An output can supply its own encoder by implementing 153 * {@link LogOutput.ProvidesEncoder} (rare, e.g. some structured outputs): with 154 * {@link LogOutput.ProvidesEncoder.Policy#MANDATORY} that encoder is used as-is 155 * and the {@code .encoder.uri} property is never consulted (configuring one anyway 156 * is a startup failure); with {@link LogOutput.ProvidesEncoder.Policy#DEFAULT} 157 * it is only a fallback and the {@code .encoder.uri} property, if set, still wins. 158 * An output that does not implement {@link LogOutput.ProvidesEncoder} at all is 159 * unaffected: the {@code .encoder.uri} property (or the registry's default for the 160 * output's type) always applies.</li> 161 * </ol> 162 */ 163//@formatter:on 164@SuppressWarnings("InvalidInlineTag") 165public sealed interface RainbowGum extends AutoCloseable, LogEventLogger { 166 167 /** 168 * Gets the currently statically bound RainbowGum and will try to load and find one if 169 * there is none currently bound. <strong> This is a blocking operation as in locks 170 * are used and will block indefinitely till a gum has loaded. </strong> If that is 171 * not desired see {@link #getOrNull()}. 172 * @return current RainbowGum or new loaded one. 173 * @throws IllegalStateException if called on a thread that is starting the global 174 * RainbowGum, for example by logging while it loads. See {@link #ofOrNull()}. 175 */ 176 public static RainbowGum of() { 177 var gum = RainbowGumHolder.getOrNullIfStarting(); 178 if (gum == null) { 179 throw new IllegalStateException("RainbowGum component tried to log too early. " 180 + "This is usually caused by dependencies calling logging."); 181 } 182 return gum; 183 } 184 185 /** 186 * Same as {@link #of()} except that it returns <code>null</code> instead of throwing 187 * when called on a thread that is starting the global RainbowGum, for example by 188 * logging while it loads. A logging facade getting <code>null</code> can use 189 * {@link LogRouter#global()}, which holds events until a RainbowGum is bound. 190 * @return current RainbowGum or new loaded one, or <code>null</code> if this thread 191 * is starting one. 192 */ 193 public static @Nullable RainbowGum ofOrNull() { 194 return RainbowGumHolder.getOrNullIfStarting(); 195 } 196 197 /** 198 * Gets the currently statically bound RainbowGum or <code>null</code> if none are 199 * <strong>finished binding</strong>. Unlike {@link #of()} this will never load or 200 * start an instance but rather just gets the currently bound one. It will also never 201 * block and does not wait if one is currently being loaded. This is a snapshot: the 202 * instance can be replaced or closed after it is read. 203 * @return current RainbowGum or <code>null</code> 204 */ 205 public static @Nullable RainbowGum getOrNull() { 206 return RainbowGumHolder.current(); 207 } 208 209 /** 210 * Provides the service loader default based RainbowGum. 211 * @return RainbowGum. 212 */ 213 public static RainbowGum defaults() { 214 return RainbowGumServiceProvider.provide(); 215 } 216 217 /** 218 * Creates a RainbowGum that will <strong>ALWAYS</strong> use the global router. 219 * @param config config. 220 * @return rainbow gum that always uses global router. 221 */ 222 public static RainbowGum queued(LogConfig config) { 223 return new SimpleRainbowGum(config, LogRouter.global(), UUID.randomUUID()); 224 } 225 226 /** 227 * Sets the global default RainbowGum. The supplied {@link RainbowGum} must not 228 * already be started. 229 * @param supplier the supplier will be memoized when {@linkplain Supplier#get() 230 * accessed} and {@link RainbowGum#start()} will be called. 231 */ 232 public static void set(Supplier<RainbowGum> supplier) { 233 RainbowGumHolder.set(supplier); 234 } 235 236 /** 237 * Subscribes to changes of the globally bound RainbowGum: a new one becoming bound 238 * (see {@link #set(Supplier)}, {@link #set(RainbowGum)}, {@link Builder#set()}) or 239 * the bound one no longer being bound (closing it, {@link Builder#unset()}, or 240 * {@link #set(Supplier)} replacing it). This is primarily for components like SLF4J's 241 * logger factory that are themselves bootstrapped once (often before "the real" 242 * RainbowGum has loaded, e.g. a framework's own pre-boot sequence) and otherwise have 243 * no way of finding out that a different RainbowGum has since replaced the one they 244 * captured. 245 * <p> 246 * Only a {@linkplain java.lang.ref.WeakReference weak reference} to 247 * <code>consumer</code> is retained, so subscribing does not by itself keep the 248 * consumer (or whatever it is attached to) reachable - the caller is responsible for 249 * keeping a strong reference to <code>consumer</code> for as long as it needs to keep 250 * receiving notifications, otherwise it may silently stop being called once garbage 251 * collected. 252 * <p> 253 * The consumer receives what {@link #getOrNull()} returns after the change: the newly 254 * bound instance, or <code>null</code> when none is bound anymore. Removing when none 255 * is bound is not a change. Registering a supplier neither starts it nor closes the 256 * gum it replaces. Subscribing does not replay the currently bound instance. 257 * Notifications are delivered in the order of the changes. A concurrent or reentrant 258 * change queues its notification until the current delivery finishes, so its caller 259 * may return before subscribers have seen it. A subscriber throwing a runtime 260 * exception is reported through the internal error log and does not prevent later 261 * deliveries. A notification is not a guarantee that the supplied gum is still 262 * current or open when the consumer runs. 263 * @apiNote A consumer receiving <code>null</code> should drop what it holds rather 264 * than call {@link #of()}, which would start another gum while the application may be 265 * shutting down. 266 * @param consumer called with the newly bound global RainbowGum or <code>null</code> 267 * when none is bound. Never called while any RainbowGum internal lock is held. 268 */ 269 public static void onGlobalChange(Consumer<@Nullable RainbowGum> consumer) { 270 RainbowGumHolder.globalChangePublisher.add(consumer); 271 } 272 273 /** 274 * Sets a Rainbow Gum and provides a supplier that will start it globally. 275 * @param gum that will be set immediatly. 276 * @return supplier that will set, get and start the supplied gum and if it is not the 277 * same will throw {@link IllegalStateException}. 278 */ 279 public static Supplier<? extends RainbowGum> set(RainbowGum gum) { 280 UUID instanceId = gum.instanceId(); 281 RainbowGum.set(() -> gum); 282 return () -> { 283 var of = RainbowGum.of(); 284 /* 285 * TODO this is a hack. The holder lock should be used to make this not 286 * happen. 287 */ 288 if (!instanceId.equals(of.instanceId())) { 289 throw new IllegalStateException("Another rainbow gum registered itself as the global. " 290 + "This is rare reace condition and probably a bug"); 291 } 292 return of; 293 }; 294 } 295 296 /** 297 * The config associated with this instance. 298 * @return config. 299 */ 300 public LogConfig config(); 301 302 /** 303 * The router that will route log messages to publishers. 304 * @return router 305 */ 306 public RootRouter router(); 307 308 /** 309 * Starts the rainbow gum and returns it. It is returned for try-with usage 310 * convenience. 311 * @return the started RainbowGum. 312 */ 313 default RainbowGum start() { 314 router().start(config()); 315 var debugMode = config().debugMode(); 316 if (debugMode == LogConfig.DebugModeType.ALL || debugMode == LogConfig.DebugModeType.HELP) { 317 var report = config().reporter().report(); 318 MetaLog.error(LogEventFactory.of(RainbowGum.class.getName()) 319 .eventNoArg(Level.INFO, "Rainbow Gum started:\n" + report, null)); 320 } 321 if (config() instanceof DefaultLogConfig c) { 322 var check = c.unusedKeyCheck(); 323 if (check != null) { 324 reportUnusedKeys(check); 325 } 326 } 327 if (config().alerts() instanceof DefaultLogAlerts alerts) { 328 try { 329 alerts.finishStartup(); 330 } 331 catch (RuntimeException e) { 332 close(); 333 throw e; 334 } 335 } 336 return this; 337 } 338 339 /* 340 * By default an unused key with a suggestion is almost always a typo, so it is a 341 * warning, which logging.alerts.fail=warning (and logging.debug=help) turns into a 342 * failure. So is an unread URI query parameter, since its URI chose the component. 343 * Any other is only information: the key may be for a component type that was not 344 * chosen, such as encoder options left behind by a profile that switches encoder. 345 * logging.alerts.unusedKey raises both for strict configurations. 346 */ 347 private void reportUnusedKeys(UnusedKeyCheck check) { 348 var eventFactory = LogEventFactory.of(LogConfig.class.getName()); 349 var unusedKeyLevel = config().alerts() instanceof DefaultLogAlerts alerts ? alerts.unusedKeyLevel() 350 : LogAlerts.UnusedKeyLevel.INFO; 351 for (var u : check.unused()) { 352 var level = unusedKeyLevel.level(u.likelyMistake()); 353 var event = eventFactory.eventNoArg(level, u.message(), null); 354 config().alerts().alert(event); 355 if (level == Level.INFO && config().debugMode().checksUnusedKeys()) { 356 MetaLog.error(event); 357 } 358 } 359 } 360 361 /** 362 * Unique id of rainbow gum instance. 363 * @return random id created on creation. 364 */ 365 public UUID instanceId(); 366 367 /** 368 * Will close the RainbowGum and all registered components as well as removed from the 369 * shutdown hooks. If the rainbow gum is set as global it will no longer be global and 370 * replaced with the bootstrapping in memory queue. {@inheritDoc} 371 */ 372 @Override 373 public void close(); 374 375 /** 376 * This append call is mainly for testing as it does not avoid making events that do 377 * not need to be made if no logging needs to be done. {@inheritDoc} 378 */ 379 @Override 380 default void log(LogEvent event) { 381 var r = router().route(event.loggerName(), event.level()); 382 if (r.isEnabled()) { 383 r.log(event); 384 } 385 } 386 387 /** 388 * Use to build a custom {@link RainbowGum} which will use the {@link LogConfig} 389 * provided by the service loader, so installed modules are discovered. 390 * @return builder. 391 * @see #builder(Consumer) 392 */ 393 public static Builder builder() { 394 ServiceLoader<RainbowGumServiceProvider> loader = ServiceLoader.load(RainbowGumServiceProvider.class); 395 var config = RainbowGumServiceProvider.provideConfig(loader); 396 return builder(config); 397 } 398 399 /** 400 * Use to build a custom {@link RainbowGum} with supplied config. 401 * @param config the config 402 * @return builder. 403 * @see #builder() 404 */ 405 public static Builder builder(LogConfig config) { 406 return new Builder(config); 407 } 408 409 /** 410 * Use to build a custom {@link RainbowGum} whose config starts like 411 * {@link #builder()}: the config builder already uses the service loader, so 412 * installed modules are discovered, and the consumer changes only what it needs to. 413 * Calling {@link LogConfig.Builder#serviceLoader(ServiceLoader)} in the consumer 414 * replaces the service loader. To build without the service loader use 415 * {@link #builder(LogConfig)} with a config from {@link LogConfig#builder()}. 416 * @param config consumer that has first argument as config builder. 417 * @return builder. 418 * @see #builder() 419 */ 420 public static Builder builder(Consumer<? super LogConfig.Builder> config) { 421 var b = LogConfig.builder().serviceLoader(ServiceLoader.load(RainbowGumServiceProvider.class)); 422 config.accept(b); 423 return builder(b.build()); 424 } 425 426 /** 427 * RainbowGum Builder. 428 */ 429 public class Builder { 430 431 private final LogConfig config; 432 433 private final List<Router> routes = new ArrayList<>(); 434 435 private Builder(LogConfig config) { 436 this.config = config; 437 } 438 439 /** 440 * Adds a router. 441 * @param route a router. 442 * @return builder. 443 */ 444 public Builder route(Router route) { 445 bind(); 446 this.routes.add(route); 447 return this; 448 } 449 450 /* 451 * Routers provision outputs/appenders into the config as soon as they are built, 452 * so the config must be claimed before that, not just at build(). 453 */ 454 private void bind() { 455 ((DefaultLogConfig) config).bind(this); 456 } 457 458 /** 459 * Adds a route by using a consumer of the route builder. 460 * @param name name of router. 461 * @param consumer consumer is passed router builder. The consumer does not need 462 * to call {@link Router#builder(String,LogConfig)} 463 * @return builder. 464 * @see io.jstach.rainbowgum.LogRouter.Router.Builder 465 */ 466 public Builder route(String name, Consumer<Router.Builder> consumer) { 467 bind(); 468 var builder = Router.builder(name, config); 469 consumer.accept(builder); 470 return route(builder.build()); 471 } 472 473 /** 474 * Adds a route by using a consumer of the route builder. 475 * @param consumer consumer is passed router builder. The consumer does not need 476 * to call {@link Router#builder(String,LogConfig)} 477 * @return builder. 478 * @see io.jstach.rainbowgum.LogRouter.Router.Builder 479 */ 480 public Builder route(Consumer<Router.Builder> consumer) { 481 bind(); 482 var builder = Router.builder(Router.DEFAULT_ROUTER_NAME, config); 483 consumer.accept(builder); 484 return route(builder.build()); 485 } 486 487 /** 488 * Builds an un-started {@link RainbowGum}. 489 * @return an un-started {@link RainbowGum}. 490 */ 491 public RainbowGum build() { 492 return build(UUID.randomUUID()); 493 } 494 495 /** 496 * Builds an un-started {@link RainbowGum}. 497 * @param instanceId unique id for rainbow gum instance. 498 * @return an un-started {@link RainbowGum}. 499 */ 500 private RainbowGum build(UUID instanceId) { 501 ((DefaultLogConfig) config).markBuilt(this); 502 var routes = this.routes; 503 var config = this.config; 504 if (routes.isEmpty()) { 505 var routeNamesResult = config.properties() // 506 .forKey(LogProperties.ROUTES_PROPERTY) 507 .ofList() 508 .or(List.of()); 509 List<String> routeNames = routeNamesResult.validateNow(Builder.class); 510 /* 511 * Validated as a plain list of names, before any of them is used to build 512 * a Router, so a bad name is reported against this list property itself, 513 * not deferred until some unrelated {name}-keyed property (e.g. this 514 * route's own level) happens to interpolate it first. 515 */ 516 LogProperty.Validator.validateNames(LogRouter.class, "route", routeNamesResult, routeNames); 517 if (routeNames.isEmpty()) { 518 routes = List.of(Router.builder(Router.DEFAULT_ROUTER_NAME, config).build()); 519 } 520 else { 521 routes = routeNames.stream().map(n -> Router.builder(n, config).build()).toList(); 522 } 523 } 524 var root = InternalRootRouter.of(routes, config); 525 config.changePublisher().subscribe(c -> { 526 root.changePublisher().publish(root); 527 }); 528 ((DefaultLogConfig) config).reporter().bind(root); 529 return new SimpleRainbowGum(config, root, instanceId); 530 } 531 532 /** 533 * Builds, starts and sets the RainbowGum as the global one picked up by logging 534 * facades. 535 * @return started and set rainbow that can be used in a try-close. 536 */ 537 public RainbowGum set() { 538 UUID instanceId = UUID.randomUUID(); 539 RainbowGum.set(() -> build(instanceId)); 540 var gum = RainbowGum.of(); 541 /* 542 * TODO this is a hack. The holder lock should be used to make this not 543 * happen. 544 */ 545 if (!instanceId.equals(gum.instanceId())) { 546 throw new IllegalStateException("Another rainbow gum registered itself as the global. " 547 + "This is rare reace condition and probably a bug"); 548 } 549 return gum; 550 } 551 552 /** 553 * Removes the currently set Rainbow Gum which will close it if one exists and 554 * will be replaced by the default provision process if {@link RainbowGum#of()} is 555 * called before {@link #set()}. 556 * @apiNote This is largely an internal detail for unit testing the provision 557 * process. 558 */ 559 public void unset() { 560 RainbowGumHolder.remove(null); 561 } 562 563 /** 564 * For returning an optional for the LogProvider contract. 565 * @return optional that always has a rainbow gum. 566 * @apiNote this method is for ergonomics. 567 */ 568 public Optional<RainbowGum> optional() { 569 return Optional.of(this.build()); 570 } 571 572 /** 573 * For returning an optional for the LogProvider contract. 574 * @param condition condition to check if this rainbow gum should be used. 575 * @return optional rainbow gum. 576 * @apiNote this method is for ergonomics. 577 */ 578 public Optional<RainbowGum> optional(Function<? super LogConfig, Boolean> condition) { 579 var cond = condition.apply(config); 580 if (cond) { 581 return Optional.of(this.build()); 582 } 583 return Optional.empty(); 584 } 585 586 } 587 588} 589 590final class RainbowGumHolder { 591 592 /* 593 * Serializes provisioning and changes to the supplier/router. Snapshot reads only 594 * need the volatile field; neither a read lock nor a stamp can pin a returned gum's 595 * lifetime after the read completes. 596 */ 597 private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); 598 599 private static Supplier<RainbowGum> supplier = RainbowGumServiceProvider::provide; 600 601 private static volatile @Nullable RainbowGum rainbowGum = null; 602 603 static final GlobalChangePublisher globalChangePublisher = new GlobalChangePublisher(); 604 605 /* 606 * Lock free read for per event hot paths (see KeyValuesContributor.global()) where 607 * possibly seeing the previous gum while another one binds is harmless. 608 */ 609 static @Nullable RainbowGum peek() { 610 return rainbowGum; 611 } 612 613 static @Nullable RainbowGum current() { 614 return rainbowGum; 615 } 616 617 /* 618 * Null when this thread holds the write lock, so is starting or removing a gum. 619 */ 620 static @Nullable RainbowGum getOrNullIfStarting() { 621 lock.readLock().lock(); 622 try { 623 var r = rainbowGum; 624 if (r != null) { 625 return r; 626 } 627 } 628 finally { 629 lock.readLock().unlock(); 630 } 631 if (lock.writeLock().isHeldByCurrentThread()) { 632 return null; 633 } 634 lock.writeLock().lock(); 635 try { 636 var r = rainbowGum; 637 if (r != null) { 638 return r; 639 } 640 r = supplier.get(); 641 start(r); 642 rainbowGum = r; 643 globalChangePublisher.enqueue(Optional.of(r)); 644 return r; 645 646 } 647 finally { 648 lock.writeLock().unlock(); 649 /* 650 * Enqueue under the write lock to preserve binding order, but invoke user 651 * callbacks only after releasing it. The publisher never holds its monitor 652 * while calling a subscriber, including one that binds another gum. 653 */ 654 drainGlobalChanges(); 655 } 656 657 } 658 659 static boolean remove(@Nullable RainbowGum gum) { 660 lock.writeLock().lock(); 661 try { 662 var original = rainbowGum; 663 if (gum != null && original != gum) { 664 return false; 665 } 666 rainbowGum = null; 667 supplier = RainbowGumServiceProvider::provide; 668 if (original != null) { 669 /* 670 * Unbound and queued before the global router is reset, so a failing 671 * output close still publishes the removal. 672 */ 673 globalChangePublisher.enqueue(Optional.empty()); 674 LogRouter.global().close(); 675 } 676 return true; 677 } 678 finally { 679 lock.writeLock().unlock(); 680 drainGlobalChanges(); 681 } 682 } 683 684 static void set(Supplier<RainbowGum> rainbowGumSupplier) { 685 Objects.requireNonNull(rainbowGumSupplier); 686 if (lock.writeLock().isHeldByCurrentThread()) { 687 throw new IllegalStateException("RainbowGum component tried to log too early. " 688 + "This is usually caused by dependencies calling logging."); 689 } 690 lock.writeLock().lock(); 691 try { 692 var original = rainbowGum; 693 rainbowGum = null; 694 supplier = rainbowGumSupplier; 695 if (original != null) { 696 globalChangePublisher.enqueue(Optional.empty()); 697 } 698 } 699 finally { 700 lock.writeLock().unlock(); 701 drainGlobalChanges(); 702 } 703 } 704 705 /* 706 * Component code can reenter remove() during startup or shutdown, so only the 707 * outermost lifecycle operation calls subscribers. 708 */ 709 private static void drainGlobalChanges() { 710 if (!lock.writeLock().isHeldByCurrentThread()) { 711 globalChangePublisher.drain(); 712 } 713 } 714 715 private static void start(RainbowGum gum) { 716 Objects.requireNonNull(gum); 717 ShutdownManager.addShutdownHook(gum); 718 gum.start(); 719 InternalRootRouter.setRouter(gum.router()); 720 } 721 722} 723 724/* 725 * Unlike LogConfig.ChangePublisher/LogRouter.RouteChangePublisher (both scoped to a 726 * single RainbowGum/router instance and torn down with it) this is a JVM-lifetime static 727 * registry, so consumers are held only weakly - otherwise every SLF4J logger factory (or 728 * any other subscriber) ever created over the life of the JVM, e.g. across many 729 * short-lived RainbowGums in tests, would be pinned forever. 730 */ 731final class GlobalChangePublisher { 732 733 private final Queue<WeakReference<Consumer<@Nullable RainbowGum>>> consumers = new ConcurrentLinkedQueue<>(); 734 735 /* 736 * Both guarded by this. No callbacks may run while this monitor is held. Empty is a 737 * removal, as ArrayDeque does not allow null. 738 */ 739 private final Queue<Optional<RainbowGum>> pending = new ArrayDeque<>(); 740 741 private boolean draining; 742 743 void add(Consumer<@Nullable RainbowGum> consumer) { 744 Objects.requireNonNull(consumer); 745 consumers.add(new WeakReference<>(consumer)); 746 } 747 748 synchronized void enqueue(Optional<RainbowGum> state) { 749 pending.add(state); 750 } 751 752 void drain() { 753 synchronized (this) { 754 if (draining) { 755 return; 756 } 757 draining = true; 758 } 759 try { 760 while (true) { 761 Optional<RainbowGum> state; 762 synchronized (this) { 763 state = pending.poll(); 764 if (state == null) { 765 draining = false; 766 return; 767 } 768 } 769 publish(state.orElse(null)); 770 } 771 } 772 catch (RuntimeException | Error e) { 773 synchronized (this) { 774 draining = false; 775 } 776 throw e; 777 } 778 } 779 780 private void publish(@Nullable RainbowGum gum) { 781 for (var it = consumers.iterator(); it.hasNext();) { 782 var consumer = it.next().get(); 783 if (consumer == null) { 784 it.remove(); 785 } 786 else { 787 try { 788 consumer.accept(gum); 789 } 790 catch (RuntimeException e) { 791 MetaLog.error(RainbowGum.class, "Global change subscriber threw", e); 792 } 793 } 794 } 795 } 796 797} 798 799final class SimpleRainbowGum implements RainbowGum, Shutdownable { 800 801 private final LogConfig config; 802 803 private final RootRouter router; 804 805 private final AtomicInteger state = new AtomicInteger(0); 806 807 private final UUID instanceId; 808 809 private static final int INIT = 0; 810 811 private static final int STARTED = 1; 812 813 private static final int CLOSED = 2; 814 815 public SimpleRainbowGum(LogConfig config, RootRouter router, UUID instanceId) { 816 super(); 817 this.config = config; 818 this.router = router; 819 this.instanceId = instanceId; 820 } 821 822 @Override 823 public LogConfig config() { 824 return this.config; 825 } 826 827 @Override 828 public RootRouter router() { 829 return this.router; 830 } 831 832 @Override 833 public RainbowGum start() { 834 int current; 835 if ((current = state.compareAndExchange(INIT, STARTED)) == INIT) { 836 return RainbowGum.super.start(); 837 } 838 throw new IllegalStateException("Cannot start. This rainbowgum is " + stateLabel(current)); 839 } 840 841 @Override 842 public UUID instanceId() { 843 return this.instanceId; 844 } 845 846 @Override 847 public void close() { 848 if (state.compareAndSet(STARTED, CLOSED)) { 849 try { 850 RainbowGumHolder.remove(this); 851 } 852 finally { 853 try { 854 shutdown(); 855 } 856 finally { 857 ShutdownManager.removeShutdownHook(this); 858 } 859 } 860 return; 861 } 862 } 863 864 @Override 865 public void shutdown() { 866 /* 867 * queued(config) borrows the global router. Only remove() resets it, as closing 868 * it here could reset a replacement that is already bound. 869 */ 870 if (!(router instanceof GlobalLogRouter)) { 871 router.close(); 872 } 873 config().alerts().close(); 874 config().serviceRegistry().close(); 875 } 876 877 @Override 878 public String toString() { 879 return "SimpleRainbowGum [instanceId=" + instanceId + ", config=" + config + ", router=" + router + ", state=" 880 + stateLabel(state.get()) + "]"; 881 } 882 883 private static String stateLabel(int state) { 884 return switch (state) { 885 case INIT -> "created"; 886 case STARTED -> "started"; 887 case CLOSED -> "closed"; 888 default -> { 889 throw new IllegalArgumentException("" + state); 890 } 891 }; 892 } 893 894}