001package io.jstach.rainbowgum; 002 003import java.lang.System.Logger.Level; 004import java.util.ArrayList; 005import java.util.Collection; 006import java.util.Collections; 007import java.util.EnumSet; 008import java.util.IdentityHashMap; 009import java.util.List; 010import java.util.Objects; 011import java.util.Set; 012import java.util.concurrent.ConcurrentLinkedQueue; 013import java.util.concurrent.CopyOnWriteArrayList; 014import java.util.concurrent.locks.ReentrantLock; 015import java.util.function.Consumer; 016import java.util.function.Function; 017 018import org.eclipse.jdt.annotation.Nullable; 019 020import io.jstach.rainbowgum.LogProperty.Property; 021import io.jstach.rainbowgum.LogPublisher.PublisherFactory; 022import io.jstach.rainbowgum.LogRouter.RootRouter; 023import io.jstach.rainbowgum.LogRouter.Route; 024import io.jstach.rainbowgum.LogRouter.Router; 025import io.jstach.rainbowgum.annotation.CaseChanging; 026 027/** 028 * Routes messages to a publisher by providing a {@link Route} from a logger name and 029 * level. 030 */ 031public sealed interface LogRouter extends LogLifecycle { 032 033 /** 034 * Finds a route. 035 * @param loggerName topic. 036 * @param level level. 037 * @return route never <code>null</code>. 038 */ 039 public Route route(String loggerName, java.lang.System.Logger.Level level); 040 041 /** 042 * Creates (or reuses in the case of logging off) an event builder. 043 * @param loggerName logger name of the event. 044 * @param level level that the event should be set to. 045 * @return builder. 046 * @apiNote using the builder is slightly slower and possibly more garbage (if the 047 * builder is not marked for escape analysis) than just manually checking 048 * {@link Route#isEnabled()} and constructing the event using the LogEvent static 049 * "<code>of</code>" factory methods. 050 */ 051 default LogEvent.Builder eventBuilder(String loggerName, Level level) { 052 var route = route(loggerName, level); 053 if (route.isEnabled()) { 054 return new LogEventBuilder(route, level, loggerName); 055 } 056 return NoOpLogEventBuilder.NOOP; 057 } 058 059 /** 060 * Global router which is always available. 061 * @return global root router. 062 */ 063 public static RootRouter global() { 064 return GlobalLogRouter.INSTANCE; 065 } 066 067 /** 068 * Creates a <strong>static</strong> router based on the level. Use a 069 * {@link LevelResolver} to resolve the level first. 070 * @param logger to publish events that are equal or above level. 071 * @param level threshold of the router. 072 * @return immutable static router. 073 * @apiNote This method is for facades that do not have named level methods but are 074 * passed the level on every log method like the System Logger. 075 */ 076 public static LogRouter ofLevel(LogEventLogger logger, Level level) { 077 return new LevelRouter(level, logger); 078 } 079 080 /** 081 * A route is similar to a SLF4J Logger or System Logger but has a much simpler 082 * contract. 083 * 084 * The proper usage of Route in most cases is to call {@link #isEnabled()} before 085 * calling {@link #log(LogEvent)}. 086 * 087 * That is {@link #log(LogEvent)} does not do any checking if the event is allowed 088 * furthermore by first checking if {@link #isEnabled()} is true one can decide 089 * whether or not to create a {@link LogEvent}. 090 */ 091 public sealed interface Route extends LogEventLogger { 092 093 /** 094 * Determines if {@link #log(LogEvent)} maybe called. 095 * @return true if log can be called. 096 */ 097 public boolean isEnabled(); 098 099 /** 100 * Route singletons and utilities. 101 */ 102 @CaseChanging 103 public enum Routes implements Route { 104 105 /** 106 * A route that is a NOOP and is always disabled. 107 */ 108 NotFound { 109 @Override 110 public void log(LogEvent event) { 111 } 112 113 @Override 114 public boolean isEnabled() { 115 return false; 116 } 117 118 }; 119 120 } 121 122 } 123 124 /** 125 * Router flags for adhoc router customization. 126 */ 127 public enum RouteFlag { 128 129 /** 130 * The route will not use the global level resolver and will only use the level 131 * config directly on the router. 132 */ 133 IGNORE_GLOBAL_LEVEL_RESOLVER; 134 135 } 136 137 /** 138 * Root router is a router that has child routers. 139 */ 140 sealed interface RootRouter extends LogRouter permits InternalRootRouter { 141 142 /** 143 * Level resolver to find levels for a log name. 144 * @return level resolver. 145 */ 146 public LevelResolver levelResolver(); 147 148 /** 149 * Register for a router changes usually involving the level resolver being 150 * updated by config. 151 * @param router consumer that will be called with the updated router. 152 * @apiNote its best to use the router in the consumer accept argument and not 153 * this router even if they are usually the same. 154 */ 155 public void onChange(Consumer<? super RootRouter> router); 156 157 } 158 159 /** 160 * Router routes messages to a publisher. 161 */ 162 sealed interface Router extends LogRouter, LogEventLogger { 163 164 /** 165 * The router name given if no router is explicitly declared. 166 */ 167 public static final String DEFAULT_ROUTER_NAME = LogProperties.DEFAULT_NAME; 168 169 /** 170 * Level resolver. 171 * @return level resolver. 172 */ 173 LevelResolver levelResolver(); 174 175 /** 176 * Log publisher. 177 * @return publisher. 178 */ 179 LogPublisher publisher(); 180 181 /** 182 * Whether or not the publisher is synchronous. A synchronous publisher generally 183 * blocks. 184 * @return true if the publisher is synchronous. 185 */ 186 default boolean synchronous() { 187 return publisher().synchronous(); 188 } 189 190 @Override 191 default void log(LogEvent event) { 192 publisher().log(event); 193 } 194 195 /** 196 * Creates a builder from config. 197 * @param name name of the route which the publisher will inherit. 198 * @param config config. 199 * @return builder. 200 */ 201 static Builder builder(String name, LogConfig config) { 202 return new Builder(name, config); 203 } 204 205 /** 206 * Creates a router. 207 * 208 * @see AbstractRouter 209 */ 210 @FunctionalInterface 211 public interface RouterFactory { 212 213 /** 214 * Builder will call this factory if passed in. 215 * @param publisher publisher from builder. 216 * @param levelResolver level resolver from builder. 217 * @param name name from builder. 218 * @param config config from builder. 219 * @return router. 220 */ 221 public Router create(LogPublisher publisher, LevelResolver levelResolver, String name, LogConfig config); 222 223 /** 224 * Creates a router from a function. 225 * @param function <strong>the function may return <code>null</code></strong> 226 * which indicates to ignore. 227 * @return factory. 228 */ 229 static RouterFactory of(@SuppressWarnings("exports") Function<LogEvent, @Nullable LogEvent> function) { 230 return (pub, lr, n, c) -> new AbstractRouter(n, pub, lr) { 231 @Override 232 protected @Nullable LogEvent transformOrNull(LogEvent event) { 233 return function.apply(event); 234 } 235 }; 236 } 237 238 } 239 240 /** 241 * Router builder. 242 */ 243 public final class Builder extends LevelResolver.AbstractBuilder<Builder> implements LogConfig.ConfigSupport { 244 245 private final LogConfig config; 246 247 private final String name; 248 249 private final EnumSet<RouteFlag> flags = EnumSet.noneOf(RouteFlag.class); 250 251 private RouterFactory factory = (publisher, levelResolver, n, c) -> new SimpleRouter(n, publisher, 252 levelResolver); 253 254 private List<LogProvider<LogAppender>> appenders = new ArrayList<>(); 255 256 private Builder(String name, LogConfig config) { 257 this.name = Objects.requireNonNull(name); 258 this.config = config; 259 } 260 261 private @Nullable PublisherFactory publisher; 262 263 @Override 264 protected Builder self() { 265 return this; 266 } 267 268 /** 269 * Gets the currently bound config. 270 * @return config. 271 */ 272 @Override 273 public LogConfig config() { 274 return this.config; 275 } 276 277 /** 278 * Adds a flag. 279 * @param flag flag added if not already added. 280 * @return this. 281 */ 282 public Builder flag(RouteFlag flag) { 283 flags.add(flag); 284 return this; 285 } 286 287 /** 288 * Adds an appender by giving an appender builder to a consumer. 289 * @param name appender name. 290 * @param consumer consumer. 291 * @return this builder. 292 */ 293 public Builder appender(String name, Consumer<LogAppender.Builder> consumer) { 294 var builder = LogAppender.builder(name); 295 consumer.accept(builder); 296 this.appenders.add(builder.build()); 297 return this; 298 } 299 300 /** 301 * Adds an appender. 302 * @param appender appender provider. 303 * @return this builder. 304 */ 305 public Builder appender(LogProvider<LogAppender> appender) { 306 this.appenders.add(Objects.requireNonNull(appender)); 307 return self(); 308 } 309 310 /** 311 * Sets the publisher. Only one publisher to router. 312 * @param publisher publisher. 313 * @return this builder. 314 */ 315 public Builder publisher(PublisherFactory publisher) { 316 this.publisher = publisher; 317 return self(); 318 } 319 320 /** 321 * Factory to use for creating the router. 322 * @param factory router factory. 323 * @return this. 324 */ 325 public Builder factory(RouterFactory factory) { 326 this.factory = Objects.requireNonNull(factory); 327 return this; 328 } 329 330 Router build() { 331 return build(this.factory); 332 } 333 334 /** 335 * Builds the router. 336 * @param factory to create the router. 337 * @return router. 338 */ 339 Router build(RouterFactory factory) { 340 String name = this.name; 341 String routerLevelPrefix = LogProperties.interpolateNamedKey(LogProperties.ROUTE_LEVEL_PREFIX, name); 342 var levelResolverBuilder = LevelResolver.builder(); 343 var currentConfig = buildLevelConfigOrNull(); 344 if (currentConfig != null) { 345 levelResolverBuilder.config(currentConfig); 346 } 347 levelResolverBuilder.config(config.properties(), routerLevelPrefix); 348 if (!flags.contains(RouteFlag.IGNORE_GLOBAL_LEVEL_RESOLVER)) { 349 levelResolverBuilder.config(config.levelResolver()); 350 } 351 var levelResolver = levelResolverBuilder.build(); 352 if (currentConfig == null) { 353 /* 354 * If no config is provided in this route the global level might not 355 * be set. 356 */ 357 var level = levelResolver.resolveLevel(""); 358 Objects.requireNonNull(level); 359 if (level == System.Logger.Level.ALL) { 360 /* 361 * The global root level was not set or set to ALL. This is a bug 362 * if this happens as the builders and other places will turn ALL 363 * -> TRACE. 364 */ 365 levelResolverBuilder.config(StaticLevelResolver.INFO); 366 levelResolver = levelResolverBuilder.build(); 367 // throw new IllegalStateException("Global Level Resolver should 368 // not resolve to Level.ALL"); 369 } 370 } 371 372 var _levelResolver = levelResolver; 373 /* 374 * This routers level resolver needs to be notified if config changes. 375 */ 376 config.changePublisher().subscribe(c -> { 377 _levelResolver.clear(); 378 }); 379 var publisher = this.publisher; 380 381 List<LogProvider<LogAppender>> appenders = new ArrayList<>(this.appenders); 382 if (appenders.isEmpty()) { 383 DefaultAppenderRegistry.appenders(config, name) // 384 .stream() // 385 .forEach(appenders::add); 386 } 387 388 if (publisher == null) { 389 publisher = Property.builder() // 390 .ofProviderRef() // 391 .map(r -> config.publisherRegistry().provide(r)) // 392 .buildWithName(LogProperties.ROUTE_PUBLISHER_PROPERTY, name) 393 .get(config.properties()) 394 .value(() -> LogPublisher.SyncLogPublisher.builder().build()); 395 } 396 397 var apps = new LogAppender.Appenders(name, config, appenders); 398 var pub = publisher.create(name, config, apps); 399 /* 400 * Register the publisher for lookup like status checks. 401 */ 402 config.serviceRegistry().put(LogPublisher.class, name, pub); 403 return factory.create(pub, levelResolver, name, config); 404 } 405 406 } 407 408 } 409 410 /** 411 * A user supplied router usually for filtering or transforming purposes. The method 412 * required to implement is {@link #transformOrNull(LogEvent)}. 413 * 414 * @see RouterFactory 415 */ 416 @SuppressWarnings("javadoc") // TODO Eclipse bug. 417 public non-sealed abstract class AbstractRouter implements Router, Route { 418 419 private final String name; 420 421 private final LogPublisher publisher; 422 423 private final LevelResolver levelResolver; 424 425 /** 426 * Router tuple of publisher and resolver. 427 * @param name router name not <code>null</code>. 428 * @param publisher not <code>null</code>. 429 * @param levelResolver not <code>null</code>. 430 */ 431 protected AbstractRouter(String name, LogPublisher publisher, LevelResolver levelResolver) { 432 super(); 433 this.name = name; 434 this.publisher = publisher; 435 this.levelResolver = levelResolver; 436 } 437 438 @Override 439 public final boolean synchronous() { 440 return Router.super.synchronous(); 441 } 442 443 @Override 444 public final void log(LogEvent event) { 445 var e = transformOrNull(event); 446 if (e != null) { 447 Router.super.log(e); 448 } 449 } 450 451 /** 452 * Implement for custom filtering or transformation. 453 * @param event usually from logging facade. 454 * @return event to be pushed to logger or not if <code>null</code>. 455 */ 456 protected abstract @Nullable LogEvent transformOrNull(LogEvent event); 457 458 @Override 459 public final Route route(String loggerName, Level level) { 460 if (levelResolver().isEnabled(loggerName, level)) { 461 return this; 462 } 463 return Route.Routes.NotFound; 464 } 465 466 @Override 467 public final void start(LogConfig config) { 468 publisher.start(config); 469 } 470 471 @Override 472 public final void close() { 473 publisher.close(); 474 475 } 476 477 @Override 478 public final boolean isEnabled() { 479 return true; 480 } 481 482 @Override 483 public final LevelResolver levelResolver() { 484 return this.levelResolver; 485 } 486 487 @Override 488 public final LogPublisher publisher() { 489 return this.publisher; 490 } 491 492 @Override 493 public String toString() { 494 return getClass() + "[name=" + name + ", publisher=" + publisher + ", levelResolver=" + levelResolver + "]"; 495 } 496 497 } 498 499} 500 501record LevelRouter(Level loggerLevel, LogEventLogger logger) implements LogRouter, Route { 502 503 @Override 504 public void start(LogConfig config) { 505 } 506 507 @Override 508 public void close() { 509 } 510 511 @Override 512 public void log(LogEvent event) { 513 logger.log(event); 514 } 515 516 @Override 517 public boolean isEnabled() { 518 return true; 519 } 520 521 @Override 522 public Route route(String loggerName, Level level) { 523 if (LevelResolver.checkEnabled(level, loggerLevel)) { 524 return this; 525 } 526 return Routes.NotFound; 527 } 528} 529 530record SimpleRouter(String name, LogPublisher publisher, LevelResolver levelResolver) implements Router, Route { 531 532 @Override 533 public void close() { 534 publisher.close(); 535 } 536 537 @Override 538 public Route route(String loggerName, java.lang.System.Logger.Level level) { 539 if (levelResolver().isEnabled(loggerName, level)) { 540 return this; 541 } 542 return Route.Routes.NotFound; 543 } 544 545 @Override 546 public boolean isEnabled() { 547 return true; 548 } 549 550 @Override 551 public void start(LogConfig config) { 552 publisher.start(config); 553 } 554 555} 556 557sealed interface InternalRootRouter extends RootRouter { 558 559 /** 560 * Sets the root router. 561 * @param router root router. 562 */ 563 static void setRouter(RootRouter router) { 564 if (GlobalLogRouter.INSTANCE == router) { 565 return; 566 } 567 GlobalLogRouter.INSTANCE.drain((InternalRootRouter) router); 568 } 569 570 static InternalRootRouter of(List<? extends Router> routes) { 571 572 if (routes.isEmpty()) { 573 throw new IllegalArgumentException("atleast one route is required"); 574 } 575 List<Router> sorted = new ArrayList<>(); 576 /* 577 * We add the async routers first 578 */ 579 Set<Router> matched = Collections.newSetFromMap(new IdentityHashMap<Router, Boolean>()); 580 for (var r : routes) { 581 if (!r.synchronous()) { 582 if (matched.add(r)) { 583 sorted.add(r); 584 } 585 } 586 } 587 for (var r : routes) { 588 if (r.synchronous()) { 589 if (matched.add(r)) { 590 sorted.add(r); 591 } 592 } 593 } 594 routes = sorted; 595 LevelResolver.Builder resolverBuilder = LevelResolver.builder(); 596 routes.stream().map(Router::levelResolver).forEach(resolverBuilder::resolver); 597 var globalLevelResolver = resolverBuilder.build(); 598 599 Router[] array = routes.toArray(new Router[] {}); 600 601 if (array.length == 1) { 602 var r = array[0]; 603 return r.synchronous() ? new SingleSyncRootRouter(r) : new SingleAsyncRootRouter(r); 604 } 605 return new CompositeLogRouter(array, globalLevelResolver); 606 } 607 608 default boolean isEnabled(String loggerName, java.lang.System.Logger.Level level) { 609 return route(loggerName, level).isEnabled(); 610 611 } 612 613 default void drain(InternalRootRouter delegate) { 614 615 } 616 617 @Override 618 default void onChange(Consumer<? super RootRouter> router) { 619 changePublisher().add(router); 620 } 621 622 public RouteChangePublisher changePublisher(); 623 624 final class RouteChangePublisher { 625 626 private final Collection<Consumer<? super RootRouter>> consumers = new CopyOnWriteArrayList<Consumer<? super RootRouter>>(); 627 628 void add(Consumer<? super RootRouter> consumer) { 629 consumers.add(consumer); 630 } 631 632 void publish(RootRouter router) { 633 for (var c : consumers) { 634 c.accept(router); 635 } 636 } 637 638 void transferTo(RouteChangePublisher changePublisher) { 639 changePublisher.consumers.addAll(consumers); 640 this.consumers.clear(); 641 } 642 643 } 644 645} 646 647record SingleSyncRootRouter(Router router, RouteChangePublisher changePublisher) implements InternalRootRouter { 648 649 public SingleSyncRootRouter(Router router) { 650 this(router, new RouteChangePublisher()); 651 } 652 653 @Override 654 public void start(LogConfig config) { 655 router.start(config); 656 } 657 658 @Override 659 public void close() { 660 router.close(); 661 } 662 663 @Override 664 public LevelResolver levelResolver() { 665 return router.levelResolver(); 666 } 667 668 @Override 669 public Route route(String loggerName, Level level) { 670 return router.route(loggerName, level); 671 } 672 673} 674 675record SingleAsyncRootRouter(Router router, RouteChangePublisher changePublisher) implements InternalRootRouter { 676 677 public SingleAsyncRootRouter(Router router) { 678 this(router, new RouteChangePublisher()); 679 } 680 681 @Override 682 public void start(LogConfig config) { 683 router.start(config); 684 } 685 686 @Override 687 public void close() { 688 router.close(); 689 } 690 691 @Override 692 public LevelResolver levelResolver() { 693 return router.levelResolver(); 694 } 695 696 @Override 697 public Route route(String loggerName, Level level) { 698 return router.route(loggerName, level); 699 } 700 701} 702 703record CompositeLogRouter(Router[] routers, LevelResolver levelResolver, 704 RouteChangePublisher changePublisher) implements InternalRootRouter, Route { 705 706 public CompositeLogRouter(Router[] routers, LevelResolver levelResolver) { 707 this(routers, levelResolver, new RouteChangePublisher()); 708 } 709 710 @Override 711 public Route route(String loggerName, Level level) { 712 /* 713 * The assumption is the provided level resolver is a cached composite level 714 * resolver of all the routers. 715 */ 716 if (levelResolver.isEnabled(loggerName, level)) { 717 return this; 718 } 719 return Routes.NotFound; 720 } 721 722 @Override 723 public void log(LogEvent event) { 724 for (var router : routers) { 725 var route = router.route(event.loggerName(), event.level()); 726 if (route.isEnabled()) { 727 /* 728 * We assume async routers are earlier in the array. 729 */ 730 if (!router.synchronous()) { 731 /* 732 * Now all events are frozen from here onward to guarantee that the 733 * synchronous routers see the same thing as the async routers. 734 * 735 * Freeze is a noop if it already frozen. 736 */ 737 event = event.freeze(); 738 } 739 route.log(event); 740 } 741 } 742 } 743 744 @Override 745 public boolean isEnabled() { 746 return true; 747 } 748 749 @Override 750 public void start(LogConfig config) { 751 for (var r : routers) { 752 r.start(config); 753 } 754 } 755 756 @Override 757 public void close() { 758 for (var r : routers) { 759 r.close(); 760 } 761 } 762} 763 764final class QueueEventsRouter implements InternalRootRouter, Route { 765 766 private final ConcurrentLinkedQueue<LogEvent> events = new ConcurrentLinkedQueue<>(); 767 768 private static final LevelResolver INFO_RESOLVER = StaticLevelResolver.of(Level.INFO); 769 770 private final RouteChangePublisher changePublisher = new RouteChangePublisher(); 771 772 @Override 773 public LevelResolver levelResolver() { 774 return INFO_RESOLVER; 775 } 776 777 @Override 778 public Route route(String loggerName, Level level) { 779 if (INFO_RESOLVER.isEnabled(loggerName, level)) { 780 return this; 781 } 782 return Routes.NotFound; 783 } 784 785 @Override 786 public void start(LogConfig config) { 787 } 788 789 @Override 790 public void close() { 791 events.clear(); 792 } 793 794 @Override 795 public void log(LogEvent event) { 796 events.add(event); 797 if (event.level() == Level.ERROR) { 798 MetaLog.error(event); 799 } 800 801 } 802 803 @Override 804 public void drain(InternalRootRouter delegate) { 805 LogEvent e; 806 while ((e = this.events.poll()) != null) { 807 delegate.route(e.loggerName(), e.level()).log(e); 808 } 809 } 810 811 @Override 812 public boolean isEnabled() { 813 return true; 814 } 815 816 @Override 817 public RouteChangePublisher changePublisher() { 818 return this.changePublisher; 819 } 820 821} 822 823@SuppressWarnings("ImmutableEnumChecker") 824enum GlobalLogRouter implements InternalRootRouter, Route { 825 826 INSTANCE; 827 828 private volatile InternalRootRouter delegate = new QueueEventsRouter(); 829 830 private final ReentrantLock drainLock = new ReentrantLock(); 831 832 static boolean isShutdownEvent(String loggerName, java.lang.System.Logger.Level level) { 833 return loggerName.equals(LogLifecycle.SHUTDOWN) 834 && Boolean.parseBoolean(System.getProperty(LogLifecycle.SHUTDOWN)); 835 } 836 837 @Override 838 public LevelResolver levelResolver() { 839 return delegate.levelResolver(); 840 } 841 842 @Override 843 public boolean isEnabled() { 844 return true; 845 } 846 847 @Override 848 public void log(LogEvent event) { 849 InternalRootRouter d = delegate; 850 if (d instanceof QueueEventsRouter q) { 851 q.log(event); 852 } 853 else { 854 String loggerName = event.loggerName(); 855 var level = event.level(); 856 var route = d.route(event.loggerName(), event.level()); 857 if (route.isEnabled()) { 858 route.log(event); 859 } 860 if (isShutdownEvent(loggerName, level)) { 861 d.close(); 862 } 863 } 864 } 865 866 @Override 867 public Route route(String loggerName, Level level) { 868 return this.delegate.route(loggerName, level); 869 } 870 871 @Override 872 public boolean isEnabled(String loggerName, Level level) { 873 return this.delegate.isEnabled(loggerName, level); 874 } 875 876 @Override 877 public RouteChangePublisher changePublisher() { 878 return this.delegate.changePublisher(); 879 } 880 881 @Override 882 public void drain(InternalRootRouter delegate) { 883 _drain(delegate); 884 } 885 886 private InternalRootRouter _drain(InternalRootRouter delegate) { 887 if (delegate == this) { 888 throw new IllegalStateException("bug"); 889 } 890 drainLock.lock(); 891 try { 892 var original = this.delegate; 893 if (original instanceof QueueEventsRouter q) { 894 q.changePublisher().transferTo(delegate.changePublisher()); 895 delegate.changePublisher().publish(delegate); 896 } 897 this.delegate = Objects.requireNonNull(delegate); 898 original.drain(delegate); 899 return original; 900 } 901 finally { 902 drainLock.unlock(); 903 } 904 } 905 906 @Override 907 public void start(LogConfig config) { 908 } 909 910 @Override 911 public void close() { 912 var d = _drain(new QueueEventsRouter()); 913 d.close(); 914 } 915 916}