001package io.jstach.rainbowgum; 002 003import java.io.PrintStream; 004import java.lang.System.Logger.Level; 005import java.time.Instant; 006import java.util.ArrayList; 007import java.util.Arrays; 008import java.util.List; 009import java.util.Locale; 010import java.util.Objects; 011import java.util.concurrent.CopyOnWriteArrayList; 012import java.util.concurrent.atomic.AtomicLong; 013import java.util.concurrent.locks.ReentrantLock; 014import java.util.function.Supplier; 015 016import org.jspecify.annotations.Nullable; 017 018import io.jstach.rainbowgum.annotation.EnumAlias; 019 020/** 021 * Alerts (sometimes called errors or status events elsewhere) are for reporting problems 022 * with the logging system itself rather than application logging - for example an 023 * appender failing to write, or an async publisher's queue overflowing. Because logging 024 * itself may be broken these are not routed through the normal {@link LogRouter} but are 025 * instead kept in a small in memory ring buffer (see {@link #dump()} and 026 * {@link #stats()}). Warning and error alerts are also reported immediately (currently to 027 * stderr); informational alerts remain available for diagnostics without producing output 028 * by default. 029 * <p> 030 * An instance is available from every {@link LogConfig#alerts()}. Components that are 031 * {@linkplain LogProvider provided} config, or that are 032 * {@linkplain LogLifecycle#start( LogConfig) started} with config, should prefer 033 * capturing {@code config.alerts()} over reaching for global state. 034 * <p> 035 * {@link LogAlerts} is itself a {@link LogLifecycle}: {@link #start(LogConfig)} is called 036 * exactly once, after every 037 * {@link io.jstach.rainbowgum.spi.RainbowGumServiceProvider.Configurator} has run, and is 038 * where {@link UnobservedErrorsAction} (already resolved at construction, same as 039 * capacity) gets applied against whatever has been recorded by then - see that enum for 040 * what "nobody is listening yet" means and why it matters specifically at that moment 041 * rather than for the life of the process. 042 * 043 * @see LogConfig#alerts() 044 */ 045public sealed interface LogAlerts extends LogLifecycle permits DefaultLogAlerts, SwappableLogAlerts { 046 047 /** 048 * Default capacity of the alert ring buffer. 049 */ 050 static final int DEFAULT_CAPACITY = 512; 051 052 /** 053 * Records an alert. 054 * @param event event describing the alert. 055 */ 056 public void alert(LogEvent event); 057 058 /** 059 * Records an alert. 060 * @param loggerName usually the class where the alert originated. 061 * @param throwable cause of the alert. 062 */ 063 default void error(Class<?> loggerName, Throwable throwable) { 064 String m = Objects.requireNonNullElse(throwable.getMessage(), "exception"); 065 error(loggerName, m, throwable); 066 } 067 068 /** 069 * Records an alert. 070 * @param loggerName usually the class where the alert originated. 071 * @param message alert message. 072 * @param throwable cause of the alert. 073 */ 074 default void error(Class<?> loggerName, String message, Throwable throwable) { 075 alert(event(loggerName, Level.ERROR, message, throwable)); 076 } 077 078 /** 079 * Records a warning alert. 080 * @param loggerName usually the class where the alert originated. 081 * @param message alert message. 082 */ 083 default void warn(Class<?> loggerName, String message) { 084 alert(event(loggerName, Level.WARNING, message, null)); 085 } 086 087 /** 088 * Records an informational alert. 089 * @param loggerName usually the class where the alert originated. 090 * @param message alert message. 091 */ 092 default void info(Class<?> loggerName, String message) { 093 alert(event(loggerName, Level.INFO, message, null)); 094 } 095 096 private static LogEvent event(Class<?> loggerName, Level level, String message, @Nullable Throwable throwable) { 097 var currentThread = Thread.currentThread(); 098 return LogEvent.of(Instant.now(), currentThread.getName(), currentThread.threadId(), level, 099 loggerName.getName(), message, KeyValues.of(), throwable); 100 } 101 102 /** 103 * A snapshot of the alerts currently held in the ring buffer, oldest first. Alerts 104 * are evicted oldest first once the buffer is at {@link Stats#capacity()}. 105 * @return immutable snapshot. 106 */ 107 public List<LogEvent> dump(); 108 109 /** 110 * Clears the ring buffer. Does not reset {@link Stats#total()} - like a 111 * Prometheus/Micrometer counter, that is meant to be monotonically increasing for the 112 * life of the process; a downstream metrics system computes rate of change rather 113 * than relying on the counter itself being reset. Clearing diagnostics does not undo 114 * an alert that fails startup under {@link FailLevel}. 115 */ 116 public void clear(); 117 118 /** 119 * Stats about the alerts recorded so far. 120 * @return stats. 121 */ 122 public Stats stats(); 123 124 /** 125 * Registers a listener that is notified synchronously, in addition to the alert being 126 * recorded in the ring buffer, every time {@link #alert(LogEvent)} is called. 127 * <p> 128 * <strong>Listeners are held with a normal (strong) reference and are not 129 * automatically removed.</strong> This is deliberate: alert listeners are expected to 130 * be few and long lived - typically registered once (e.g. a metrics bridge or an 131 * ops/paging integration) for as long as the owning {@link LogConfig} is - rather 132 * than one per short lived object. A weak reference would risk silently dropping a 133 * listener (a lambda with no other strong reference could be collected almost 134 * immediately) which is the wrong failure mode for something whose entire job is not 135 * losing alerts. Call {@link AutoCloseable#close()} on the returned registration to 136 * unregister deterministically once the caller's own lifecycle ends. 137 * @param listener listener to register. 138 * @return registration; {@link AutoCloseable#close()} unregisters the listener. 139 */ 140 public AutoCloseable addListener(Listener listener); 141 142 /** 143 * Notified of alerts as they happen. See {@link #addListener(Listener)}. 144 */ 145 @FunctionalInterface 146 interface Listener { 147 148 /** 149 * Called synchronously every time an alert is recorded. Should not throw - 150 * exceptions are caught and reported separately so a broken listener cannot 151 * disrupt alert recording or other listeners. 152 * @param event the alert. 153 */ 154 void onAlert(LogEvent event); 155 156 } 157 158 /** 159 * Stats about alerts recorded. 160 * 161 * @param total total number of alerts ever recorded, including ones since evicted 162 * from the ring buffer. 163 * @param size number of alerts currently held in the ring buffer. 164 * @param capacity maximum number of alerts the ring buffer holds. 165 */ 166 record Stats(long total, int size, int capacity) { 167 } 168 169 /** 170 * What building LogConfig and starting Rainbow Gum do with alerts recorded while 171 * starting, set by {@value LogProperties#ALERTS_FAIL_PROPERTY}. Checked once when 172 * {@link LogConfig} is built and once right after Rainbow Gum starts; alerts after 173 * that never fail anything. Evicting or clearing an alert from the diagnostic queue 174 * does not prevent it from failing startup. 175 */ 176 enum FailLevel { 177 178 /** 179 * Default. Alerts never fail startup. 180 */ 181 @EnumAlias("false") 182 OFF, 183 /** 184 * Fail if an error alert is recorded while starting. 185 */ 186 ERROR, 187 /** 188 * Fail if a warning or error alert is recorded while starting. 189 */ 190 @EnumAlias({ "true", "warn" }) 191 WARNING; 192 193 static FailLevel parse(String value) { 194 return switch (value.toLowerCase(java.util.Locale.ROOT)) { 195 case "true", "warn" -> WARNING; 196 case "false" -> OFF; 197 default -> LogProperty.enumValue(FailLevel.class, value, "true", "false", "warn"); 198 }; 199 } 200 201 boolean fails(Level level) { 202 return switch (this) { 203 case OFF -> false; 204 case ERROR -> level == Level.ERROR; 205 case WARNING -> level == Level.ERROR || level == Level.WARNING; 206 }; 207 } 208 209 } 210 211 /** 212 * The alert level for a property key that was set but not read while starting, set by 213 * {@value LogProperties#ALERTS_UNUSED_KEY_PROPERTY}. Keys are only checked in the 214 * debug modes that report them and when {@value LogProperties#ALERTS_FAIL_PROPERTY} 215 * would fail on them. 216 */ 217 enum UnusedKeyLevel { 218 219 /** 220 * Default. A key close to a key that was read is a warning with a suggestion, as 221 * is a URI query parameter the component its URI chose does not read. Any other 222 * unread key is information, since it may be for a component type that was not 223 * chosen. 224 */ 225 INFO, 226 /** 227 * Every unread key is a warning, so {@code logging.alerts.fail=warning} fails on 228 * any of them. 229 */ 230 @EnumAlias("warn") 231 WARNING, 232 /** 233 * Every unread key is an error, so {@code logging.alerts.fail=error} fails on any 234 * of them. 235 */ 236 ERROR; 237 238 static UnusedKeyLevel parse(String value) { 239 return switch (value.toLowerCase(java.util.Locale.ROOT)) { 240 case "warn" -> WARNING; 241 default -> LogProperty.enumValue(UnusedKeyLevel.class, value, "warn"); 242 }; 243 } 244 245 Level level(boolean likelyMistake) { 246 return switch (this) { 247 case INFO -> likelyMistake ? Level.WARNING : Level.INFO; 248 case WARNING -> Level.WARNING; 249 case ERROR -> Level.ERROR; 250 }; 251 } 252 253 } 254 255 /** 256 * What {@link #start(LogConfig)} does if, at that moment, at least one error alert 257 * has been recorded and still zero {@link Listener}s are registered - the situation 258 * Logback's {@code StatusManager} calls an unobserved status: nothing is watching, so 259 * whatever went wrong during property loading or a 260 * {@link io.jstach.rainbowgum.spi.RainbowGumServiceProvider.Configurator} would 261 * otherwise only ever have reached the individual, easy to miss stderr lines each 262 * {@link #alert(LogEvent)} call already produces. 263 * <p> 264 * Deliberately keyed as 265 * {@value LogProperties#ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY} (an enum, not a 266 * boolean) since "fail" without "dump" first would throw away the only diagnostic 267 * evidence of why it failed. 268 */ 269 enum UnobservedErrorsAction { 270 271 /** 272 * Do nothing beyond the immediate reporting of warning and error events. 273 */ 274 NONE, 275 /** 276 * Default. Report the whole backlog to {@code MetaLog} as one clearly labeled 277 * block, the same way Logback auto-installs a console listener and prints its 278 * accumulated status if nothing else is watching by the end of configuration - 279 * RainbowGum still starts. 280 */ 281 DUMP, 282 /** 283 * Same reporting as {@link #DUMP}, but then throws, so 284 * {@link LogConfig.Builder#build()} (and therefore whatever is building a 285 * {@link RainbowGum} from it) never completes. Goes further than Logback ever 286 * does - an explicit, opt-in choice of integrity over resilience for deployments 287 * where a silently-broken bootstrap is worse than refusing to start. 288 */ 289 FAIL; 290 291 static UnobservedErrorsAction parse(String value) { 292 return LogProperty.enumValue(UnobservedErrorsAction.class, value); 293 } 294 295 } 296 297} 298 299/* 300 * Handed to PropertiesProviders. Buffers alerts until the configured LogAlerts exists, 301 * then drainTo replays the buffer into it and forwards every later call, so a provider 302 * may keep this instance and alert after provideProperties returns. Replay and swap 303 * happen under one lock so a concurrent alert is neither lost nor reordered. Listener 304 * registration is never supported: providers record alerts but cannot observe them. 305 */ 306final class SwappableLogAlerts implements LogAlerts { 307 308 private final List<LogEvent> events = new ArrayList<>(); 309 310 private long total; 311 312 private volatile @Nullable LogAlerts delegate; 313 314 @Override 315 public void alert(LogEvent event) { 316 var d = delegate; 317 if (d == null) { 318 synchronized (this) { 319 d = delegate; 320 if (d == null) { 321 events.add(event.freeze()); 322 total++; 323 return; 324 } 325 } 326 } 327 d.alert(event); 328 } 329 330 synchronized void drainTo(LogAlerts target) { 331 if (delegate != null) { 332 throw new IllegalStateException("Alerts have already been drained"); 333 } 334 for (var event : events) { 335 target.alert(event); 336 } 337 events.clear(); 338 delegate = target; 339 } 340 341 @Override 342 public synchronized List<LogEvent> dump() { 343 var d = delegate; 344 return d != null ? d.dump() : List.copyOf(events); 345 } 346 347 @Override 348 public synchronized void clear() { 349 var d = delegate; 350 if (d != null) { 351 d.clear(); 352 } 353 else { 354 events.clear(); 355 } 356 } 357 358 @Override 359 public synchronized Stats stats() { 360 var d = delegate; 361 return d != null ? d.stats() : new Stats(total, events.size(), Integer.MAX_VALUE); 362 } 363 364 @Override 365 public AutoCloseable addListener(Listener listener) { 366 throw new UnsupportedOperationException("Properties providers cannot register alert listeners"); 367 } 368 369 @Override 370 public void start(LogConfig config) { 371 throw new UnsupportedOperationException("Properties provider alerts cannot be started"); 372 } 373 374 @Override 375 public synchronized void close() { 376 // The drained delegate belongs to LogConfig, so only an undrained buffer is 377 // dropped. 378 if (delegate == null) { 379 events.clear(); 380 } 381 } 382 383} 384 385final class DefaultLogAlerts implements LogAlerts { 386 387 private final LogEvent[] ring; 388 389 private int start = 0; 390 391 private int size = 0; 392 393 private final AtomicLong total = new AtomicLong(); 394 395 private final AtomicLong errors = new AtomicLong(); 396 397 private final ReentrantLock lock = new ReentrantLock(); 398 399 private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>(); 400 401 /* 402 * Kept apart from listeners.isEmpty() deliberately: DefaultLogConfig's own 403 * metrics-bridge listener (see its constructor) is always registered before 404 * start(...) ever runs, and a passive in-process counter nobody has wired an exporter 405 * to is not "someone is watching" in the sense UnobservedErrorsAction cares about - 406 * only a real addListener(Listener) call (this class's public API) flips this. 407 */ 408 private volatile boolean hasExternalListener = false; 409 410 private final LogEventFactory eventFactory = LogEventFactory.of(DefaultLogAlerts.class.getName()); 411 412 /* 413 * Resolved once, at construction (same as capacity) rather than inside start(...): 414 * unlike capacity this does not shape any constructor-time state, but LogProperties 415 * is available just as early, and start(LogConfig) is really about announcing "I have 416 * started" (config is there because a few edge cases needed it, not because this 417 * needed to be a start-time lookup). 418 */ 419 private final UnobservedErrorsAction unobservedErrorsAction; 420 421 /* 422 * Private and deliberately not defensive - of(LogProperties) below is the only 423 * caller, and it has already validated both fields (capacity > 0, a resolvable 424 * UnobservedErrorsAction) before ever reaching this constructor. Re-checking here 425 * would be defensive programming duplicating what was already a real, reported 426 * validation failure one call up - see of(...)'s own comment. 427 */ 428 private final FailLevel failLevel; 429 430 /* 431 * Names what set the fail level, for the failure message: the property, or 432 * logging.debug=help raising it. 433 */ 434 private final String failLevelSource; 435 436 private final UnusedKeyLevel unusedKeyLevel; 437 438 /* 439 * Startup failure is independent of the bounded diagnostic queue. Keep the first 440 * failure and the count until startup ends, even if clear() or eviction removes it 441 * from the queue. All three fields are protected by lock. 442 */ 443 private long startupFailures; 444 445 private @Nullable LogEvent firstStartupFailure; 446 447 private boolean startupComplete; 448 449 private DefaultLogAlerts(int capacity, UnobservedErrorsAction unobservedErrorsAction, FailLevel failLevel, 450 String failLevelSource, UnusedKeyLevel unusedKeyLevel) { 451 this.ring = new LogEvent[capacity]; 452 this.unobservedErrorsAction = unobservedErrorsAction; 453 this.failLevel = failLevel; 454 this.failLevelSource = failLevelSource; 455 this.unusedKeyLevel = unusedKeyLevel; 456 } 457 458 FailLevel failLevel() { 459 return failLevel; 460 } 461 462 UnusedKeyLevel unusedKeyLevel() { 463 return unusedKeyLevel; 464 } 465 466 /* 467 * Checks the startup failure count and lists retained failing alerts, including the 468 * first failure even if it left the queue. Called when LogConfig is built and right 469 * after Rainbow Gum starts. 470 */ 471 void failIfAlerted(String phase) { 472 checkStartup(phase, false); 473 } 474 475 void finishStartup() { 476 checkStartup("starting", true); 477 } 478 479 private void checkStartup(String phase, boolean complete) { 480 lock.lock(); 481 try { 482 if (startupComplete) { 483 return; 484 } 485 startupComplete = complete; 486 var first = firstStartupFailure; 487 if (first == null) { 488 return; 489 } 490 var failing = new ArrayList<>(dump().stream().filter(e -> failLevel.fails(e.level())).toList()); 491 if (!failing.contains(first)) { 492 failing.addFirst(first); 493 } 494 var sb = new StringBuilder(); 495 sb.append(startupFailures) 496 .append(" alert(s) at ") 497 .append(failLevel.name().toLowerCase(Locale.ROOT)) 498 .append(" or above were recorded while ") 499 .append(phase) 500 .append(" and ") 501 .append(failLevelSource) 502 .append(":"); 503 for (var e : failing) { 504 sb.append("\n[").append(e.level()).append("] "); 505 e.formattedMessage(sb); 506 } 507 if (startupFailures > failing.size()) { 508 sb.append("\n") 509 .append(startupFailures - failing.size()) 510 .append(" additional failing alert(s) are no longer retained."); 511 } 512 throw new IllegalStateException(sb.toString()); 513 } 514 finally { 515 if (complete) { 516 firstStartupFailure = null; 517 } 518 lock.unlock(); 519 } 520 } 521 522 /* 523 * The fail level as resolved from properties, before LogAlerts exists, so LogConfig 524 * can decide whether to record property reads. Resolved again, and validated, by 525 * of(...). 526 */ 527 static FailLevel failLevelOr(LogProperties properties, FailLevel fallback) { 528 var value = properties.valueOrNull(LogProperties.ALERTS_FAIL_PROPERTY); 529 try { 530 return value == null ? fallback : FailLevel.parse(value); 531 } 532 catch (RuntimeException e) { 533 return fallback; 534 } 535 } 536 537 /* 538 * The unused key level as resolved from properties, before LogAlerts exists, like 539 * failLevelOr. An invalid value is reported by of(...). 540 */ 541 static UnusedKeyLevel unusedKeyLevelOr(LogProperties properties, UnusedKeyLevel fallback) { 542 var value = properties.valueOrNull(LogProperties.ALERTS_UNUSED_KEY_PROPERTY); 543 try { 544 return value == null ? fallback : UnusedKeyLevel.parse(value); 545 } 546 catch (RuntimeException e) { 547 return fallback; 548 } 549 } 550 551 /* 552 * Package-friend static factory: resolves and validates both 553 * LogProperties#ALERTS_CAPACITY_PROPERTY and 554 * LogProperties#ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY against one shared Validator 555 * - a mistake in both at once is reported together instead of only the first one 556 * found - then constructs. requirePositiveCapacity is a plain int -> int validating 557 * transform (not an object construction) specifically so it can sit inside map(...) 558 * without tripping CheckerFramework's Initialization Checker, which does not refine a 559 * `new Foo(...)` expression back to @Initialized when it appears directly inside a 560 * lambda body passed through a generic method like map(...) - the one and only `new 561 * DefaultLogAlerts(...)` call stays a plain, unconditional statement here instead. 562 */ 563 static DefaultLogAlerts of(LogProperties properties, FailLevel failLevelFallback, 564 UnusedKeyLevel unusedKeyLevelFallback, boolean help) { 565 var validator = LogProperty.Validator.of(LogAlerts.class); 566 var failLevelResult = properties.forKey(LogProperties.ALERTS_FAIL_PROPERTY) 567 .ofString() 568 .map(FailLevel::parse) 569 .or(failLevelFallback) 570 .validateIfError(validator); 571 var unobservedErrorsActionResult = properties.forKey(LogProperties.ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY) 572 .ofString() 573 .map(UnobservedErrorsAction::parse) 574 .or(UnobservedErrorsAction.DUMP) 575 .validateIfError(validator); 576 var unusedKeyLevelResult = properties.forKey(LogProperties.ALERTS_UNUSED_KEY_PROPERTY) 577 .ofString() 578 .map(UnusedKeyLevel::parse) 579 .or(unusedKeyLevelFallback) 580 .validateIfError(validator); 581 var capacityResult = properties.forKey(LogProperties.ALERTS_CAPACITY_PROPERTY) 582 .ofInt() 583 .or(LogAlerts.DEFAULT_CAPACITY) 584 .map(DefaultLogAlerts::requirePositiveCapacity) 585 .validateIfError(validator); 586 validator.validate(); 587 var failLevel = failLevelResult.value(); 588 String failLevelSource = LogProperties.ALERTS_FAIL_PROPERTY + "=" + failLevel.name().toLowerCase(Locale.ROOT); 589 if (help && failLevel != FailLevel.WARNING) { 590 failLevel = FailLevel.WARNING; 591 failLevelSource = "logging.debug=help"; 592 } 593 return new DefaultLogAlerts(capacityResult.value(), unobservedErrorsActionResult.value(), failLevel, 594 failLevelSource, unusedKeyLevelResult.value()); 595 } 596 597 private static int requirePositiveCapacity(int capacity) { 598 if (capacity <= 0) { 599 throw new IllegalArgumentException("capacity should be greater than 0"); 600 } 601 return capacity; 602 } 603 604 @Override 605 public void alert(LogEvent event) { 606 var frozen = event.freeze(); 607 total.incrementAndGet(); 608 if (frozen.level() == Level.ERROR) { 609 errors.incrementAndGet(); 610 } 611 lock.lock(); 612 try { 613 if (!startupComplete && failLevel.fails(frozen.level())) { 614 startupFailures++; 615 if (firstStartupFailure == null) { 616 firstStartupFailure = frozen; 617 } 618 } 619 if (size < ring.length) { 620 ring[(start + size) % ring.length] = frozen; 621 size++; 622 } 623 else { 624 ring[start] = frozen; 625 start = (start + 1) % ring.length; 626 } 627 } 628 finally { 629 lock.unlock(); 630 } 631 for (var listener : listeners) { 632 try { 633 listener.onAlert(frozen); 634 } 635 catch (Exception e) { 636 MetaLog.INSTANCE.log(eventFactory.eventNoArg(Level.ERROR, "LogAlerts.Listener threw", e)); 637 } 638 } 639 if (frozen.level() == Level.WARNING || frozen.level() == Level.ERROR) { 640 MetaLog.INSTANCE.log(frozen); 641 } 642 } 643 644 @Override 645 public AutoCloseable addListener(Listener listener) { 646 listeners.add(listener); 647 hasExternalListener = true; 648 return () -> listeners.remove(listener); 649 } 650 651 /* 652 * DefaultLogConfig's own metrics-bridge listener goes through this instead of 653 * addListener(Listener) - see hasExternalListener's own comment for why. 654 */ 655 void addInternalListener(Listener listener) { 656 listeners.add(listener); 657 } 658 659 @Override 660 public void start(LogConfig config) { 661 if (unobservedErrorsAction == UnobservedErrorsAction.NONE || hasExternalListener || errors.get() == 0) { 662 return; 663 } 664 var backlog = dump(); 665 MetaLog.error(eventFactory.eventNoArg(Level.ERROR, 666 backlog.size() + " alert(s) were recorded before any LogAlerts.Listener was registered - " 667 + "dumping the backlog now since nothing else will see it:", 668 null)); 669 for (var event : backlog) { 670 MetaLog.error(event); 671 } 672 if (unobservedErrorsAction == UnobservedErrorsAction.FAIL) { 673 throw new IllegalStateException( 674 backlog.size() + " alert(s) were recorded before any LogAlerts.Listener was registered and " 675 + LogProperties.ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY + "=FAIL - refusing to start."); 676 } 677 } 678 679 @Override 680 public void close() { 681 listeners.clear(); 682 lock.lock(); 683 try { 684 startupComplete = true; 685 firstStartupFailure = null; 686 } 687 finally { 688 lock.unlock(); 689 } 690 } 691 692 @Override 693 public List<LogEvent> dump() { 694 lock.lock(); 695 try { 696 List<LogEvent> list = new ArrayList<>(size); 697 for (int i = 0; i < size; i++) { 698 var e = ring[(start + i) % ring.length]; 699 if (e != null) { 700 list.add(e); 701 } 702 } 703 return List.copyOf(list); 704 } 705 finally { 706 lock.unlock(); 707 } 708 } 709 710 @Override 711 public void clear() { 712 lock.lock(); 713 try { 714 Arrays.fill(ring, null); 715 start = 0; 716 size = 0; 717 } 718 finally { 719 lock.unlock(); 720 } 721 } 722 723 @Override 724 public Stats stats() { 725 lock.lock(); 726 try { 727 return new Stats(total.get(), size, ring.length); 728 } 729 finally { 730 lock.unlock(); 731 } 732 } 733 734} 735 736/** 737 * Logging about logging. This is the static, always-available entry point used by code 738 * that cannot easily reach a {@link LogConfig} (e.g. before a {@link RainbowGum} is 739 * bound), and always reports directly (currently to stderr) rather than forwarding to a 740 * bound {@link RainbowGum}'s {@link LogConfig#alerts()} - alerts is a small bounded ring 741 * buffer meant to hold a curated, recent view of noteworthy logging-system problems, and 742 * {@link MetaLog} is used from enough different low level failure paths that routing it 743 * there risked flooding/evicting genuine alerts with whatever volume of things end up 744 * calling this class. 745 * <p> 746 * Components that already have (or can easily capture) a {@link LogConfig} - for example 747 * via {@link LogProvider} or {@link LogLifecycle#start(LogConfig)} - should prefer 748 * {@link LogConfig#alerts()} directly instead of this class. 749 * 750 * @author agentgt 751 */ 752@SuppressWarnings("ImmutableEnumChecker") 753enum MetaLog implements LogEventLogger { 754 755 INSTANCE; 756 757 static Supplier<? extends @Nullable PrintStream> output = () -> System.err; 758 759 private final LogFormatter formatter = LogFormatter.builder() // 760 .text("[") 761 .level() 762 .text("] - RAINBOW_GUM") 763 .text(" - ") 764 .add(LogFormatter.of((b, e) -> { 765 b.append(gumLoggerName(e.loggerName())); 766 })) 767 .text(" - ") 768 .message() 769 .textIfThrowable(" ") 770 .throwable() 771 .newline() 772 .build(); 773 774 /** 775 * Reports a logging system event at its original level. 776 * @param event event to log. 777 */ 778 static void error(LogEvent event) { 779 INSTANCE.log(event); 780 } 781 782 /** 783 * Logs an error in the logging system. 784 * @param loggerName derived from class. 785 * @param throwable error to log. 786 */ 787 static void error(Class<?> loggerName, Throwable throwable) { 788 String m = Objects.requireNonNullElse(throwable.getMessage(), "exception"); 789 error(loggerName, m, throwable); 790 } 791 792 /** 793 * Logs an error in the logging system. 794 * @param loggerName derived from class. 795 * @param message error message. 796 * @param throwable error to log. 797 */ 798 static void error(Class<?> loggerName, String message, Throwable throwable) { 799 var currentThread = Thread.currentThread(); 800 var event = LogEvent.of(Instant.now(), currentThread.getName(), currentThread.threadId(), Level.ERROR, 801 loggerName.getName(), message, KeyValues.of(), throwable); 802 error(event); 803 } 804 805 private static String gumLoggerName(String value) { 806 String prefix = "io.jstach.rainbowgum."; 807 if (value.startsWith(prefix)) { 808 value = value.substring(prefix.length()); 809 } 810 return value; 811 } 812 813 @Override 814 public void log(LogEvent event) { 815 var err = output.get(); 816 if (err != null) { 817 var sb = new StringBuilder(); 818 formatter.format(sb, event); 819 err.append(sb); 820 } 821 } 822 823}