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}