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}