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, and 221 * any other unread key is information, since it may be for a component type that 222 * was not chosen. 223 */ 224 INFO, 225 /** 226 * Every unread key is a warning, so {@code logging.alerts.fail=warning} fails on 227 * any of them. 228 */ 229 @EnumAlias("warn") 230 WARNING, 231 /** 232 * Every unread key is an error, so {@code logging.alerts.fail=error} fails on any 233 * of them. 234 */ 235 ERROR; 236 237 static UnusedKeyLevel parse(String value) { 238 return switch (value.toLowerCase(java.util.Locale.ROOT)) { 239 case "warn" -> WARNING; 240 default -> LogProperty.enumValue(UnusedKeyLevel.class, value, "warn"); 241 }; 242 } 243 244 Level level(boolean suggested) { 245 return switch (this) { 246 case INFO -> suggested ? Level.WARNING : Level.INFO; 247 case WARNING -> Level.WARNING; 248 case ERROR -> Level.ERROR; 249 }; 250 } 251 252 } 253 254 /** 255 * What {@link #start(LogConfig)} does if, at that moment, at least one error alert 256 * has been recorded and still zero {@link Listener}s are registered - the situation 257 * Logback's {@code StatusManager} calls an unobserved status: nothing is watching, so 258 * whatever went wrong during property loading or a 259 * {@link io.jstach.rainbowgum.spi.RainbowGumServiceProvider.Configurator} would 260 * otherwise only ever have reached the individual, easy to miss stderr lines each 261 * {@link #alert(LogEvent)} call already produces. 262 * <p> 263 * Deliberately keyed as 264 * {@value LogProperties#ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY} (an enum, not a 265 * boolean) since "fail" without "dump" first would throw away the only diagnostic 266 * evidence of why it failed. 267 */ 268 enum UnobservedErrorsAction { 269 270 /** 271 * Do nothing beyond the immediate reporting of warning and error events. 272 */ 273 NONE, 274 /** 275 * Default. Report the whole backlog to {@code MetaLog} as one clearly labeled 276 * block, the same way Logback auto-installs a console listener and prints its 277 * accumulated status if nothing else is watching by the end of configuration - 278 * RainbowGum still starts. 279 */ 280 DUMP, 281 /** 282 * Same reporting as {@link #DUMP}, but then throws, so 283 * {@link LogConfig.Builder#build()} (and therefore whatever is building a 284 * {@link RainbowGum} from it) never completes. Goes further than Logback ever 285 * does - an explicit, opt-in choice of integrity over resilience for deployments 286 * where a silently-broken bootstrap is worse than refusing to start. 287 */ 288 FAIL; 289 290 static UnobservedErrorsAction parse(String value) { 291 return LogProperty.enumValue(UnobservedErrorsAction.class, value); 292 } 293 294 } 295 296} 297 298/* 299 * Handed to PropertiesProviders. Buffers alerts until the configured LogAlerts exists, 300 * then drainTo replays the buffer into it and forwards every later call, so a provider 301 * may keep this instance and alert after provideProperties returns. Replay and swap 302 * happen under one lock so a concurrent alert is neither lost nor reordered. Listener 303 * registration is never supported: providers record alerts but cannot observe them. 304 */ 305final class SwappableLogAlerts implements LogAlerts { 306 307 private final List<LogEvent> events = new ArrayList<>(); 308 309 private long total; 310 311 private volatile @Nullable LogAlerts delegate; 312 313 @Override 314 public void alert(LogEvent event) { 315 var d = delegate; 316 if (d == null) { 317 synchronized (this) { 318 d = delegate; 319 if (d == null) { 320 events.add(event.freeze()); 321 total++; 322 return; 323 } 324 } 325 } 326 d.alert(event); 327 } 328 329 synchronized void drainTo(LogAlerts target) { 330 if (delegate != null) { 331 throw new IllegalStateException("Alerts have already been drained"); 332 } 333 for (var event : events) { 334 target.alert(event); 335 } 336 events.clear(); 337 delegate = target; 338 } 339 340 @Override 341 public synchronized List<LogEvent> dump() { 342 var d = delegate; 343 return d != null ? d.dump() : List.copyOf(events); 344 } 345 346 @Override 347 public synchronized void clear() { 348 var d = delegate; 349 if (d != null) { 350 d.clear(); 351 } 352 else { 353 events.clear(); 354 } 355 } 356 357 @Override 358 public synchronized Stats stats() { 359 var d = delegate; 360 return d != null ? d.stats() : new Stats(total, events.size(), Integer.MAX_VALUE); 361 } 362 363 @Override 364 public AutoCloseable addListener(Listener listener) { 365 throw new UnsupportedOperationException("Properties providers cannot register alert listeners"); 366 } 367 368 @Override 369 public void start(LogConfig config) { 370 throw new UnsupportedOperationException("Properties provider alerts cannot be started"); 371 } 372 373 @Override 374 public synchronized void close() { 375 // The drained delegate belongs to LogConfig, so only an undrained buffer is 376 // dropped. 377 if (delegate == null) { 378 events.clear(); 379 } 380 } 381 382} 383 384final class DefaultLogAlerts implements LogAlerts { 385 386 private final LogEvent[] ring; 387 388 private int start = 0; 389 390 private int size = 0; 391 392 private final AtomicLong total = new AtomicLong(); 393 394 private final AtomicLong errors = new AtomicLong(); 395 396 private final ReentrantLock lock = new ReentrantLock(); 397 398 private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>(); 399 400 /* 401 * Kept apart from listeners.isEmpty() deliberately: DefaultLogConfig's own 402 * metrics-bridge listener (see its constructor) is always registered before 403 * start(...) ever runs, and a passive in-process counter nobody has wired an exporter 404 * to is not "someone is watching" in the sense UnobservedErrorsAction cares about - 405 * only a real addListener(Listener) call (this class's public API) flips this. 406 */ 407 private volatile boolean hasExternalListener = false; 408 409 private final LogEventFactory eventFactory = LogEventFactory.of(DefaultLogAlerts.class.getName()); 410 411 /* 412 * Resolved once, at construction (same as capacity) rather than inside start(...): 413 * unlike capacity this does not shape any constructor-time state, but LogProperties 414 * is available just as early, and start(LogConfig) is really about announcing "I have 415 * started" (config is there because a few edge cases needed it, not because this 416 * needed to be a start-time lookup). 417 */ 418 private final UnobservedErrorsAction unobservedErrorsAction; 419 420 /* 421 * Private and deliberately not defensive - of(LogProperties) below is the only 422 * caller, and it has already validated both fields (capacity > 0, a resolvable 423 * UnobservedErrorsAction) before ever reaching this constructor. Re-checking here 424 * would be defensive programming duplicating what was already a real, reported 425 * validation failure one call up - see of(...)'s own comment. 426 */ 427 private final FailLevel failLevel; 428 429 /* 430 * Names what set the fail level, for the failure message: the property, or 431 * logging.debug=help raising it. 432 */ 433 private final String failLevelSource; 434 435 private final UnusedKeyLevel unusedKeyLevel; 436 437 /* 438 * Startup failure is independent of the bounded diagnostic queue. Keep the first 439 * failure and the count until startup ends, even if clear() or eviction removes it 440 * from the queue. All three fields are protected by lock. 441 */ 442 private long startupFailures; 443 444 private @Nullable LogEvent firstStartupFailure; 445 446 private boolean startupComplete; 447 448 private DefaultLogAlerts(int capacity, UnobservedErrorsAction unobservedErrorsAction, FailLevel failLevel, 449 String failLevelSource, UnusedKeyLevel unusedKeyLevel) { 450 this.ring = new LogEvent[capacity]; 451 this.unobservedErrorsAction = unobservedErrorsAction; 452 this.failLevel = failLevel; 453 this.failLevelSource = failLevelSource; 454 this.unusedKeyLevel = unusedKeyLevel; 455 } 456 457 FailLevel failLevel() { 458 return failLevel; 459 } 460 461 UnusedKeyLevel unusedKeyLevel() { 462 return unusedKeyLevel; 463 } 464 465 /* 466 * Checks the startup failure count and lists retained failing alerts, including the 467 * first failure even if it left the queue. Called when LogConfig is built and right 468 * after Rainbow Gum starts. 469 */ 470 void failIfAlerted(String phase) { 471 checkStartup(phase, false); 472 } 473 474 void finishStartup() { 475 checkStartup("starting", true); 476 } 477 478 private void checkStartup(String phase, boolean complete) { 479 lock.lock(); 480 try { 481 if (startupComplete) { 482 return; 483 } 484 startupComplete = complete; 485 var first = firstStartupFailure; 486 if (first == null) { 487 return; 488 } 489 var failing = new ArrayList<>(dump().stream().filter(e -> failLevel.fails(e.level())).toList()); 490 if (!failing.contains(first)) { 491 failing.addFirst(first); 492 } 493 var sb = new StringBuilder(); 494 sb.append(startupFailures) 495 .append(" alert(s) at ") 496 .append(failLevel.name().toLowerCase(Locale.ROOT)) 497 .append(" or above were recorded while ") 498 .append(phase) 499 .append(" and ") 500 .append(failLevelSource) 501 .append(":"); 502 for (var e : failing) { 503 sb.append("\n[").append(e.level()).append("] ").append(e.message()); 504 } 505 if (startupFailures > failing.size()) { 506 sb.append("\n") 507 .append(startupFailures - failing.size()) 508 .append(" additional failing alert(s) are no longer retained."); 509 } 510 throw new IllegalStateException(sb.toString()); 511 } 512 finally { 513 if (complete) { 514 firstStartupFailure = null; 515 } 516 lock.unlock(); 517 } 518 } 519 520 /* 521 * The fail level as resolved from properties, before LogAlerts exists, so LogConfig 522 * can decide whether to record property reads. Resolved again, and validated, by 523 * of(...). 524 */ 525 static FailLevel failLevelOr(LogProperties properties, FailLevel fallback) { 526 var value = properties.valueOrNull(LogProperties.ALERTS_FAIL_PROPERTY); 527 try { 528 return value == null ? fallback : FailLevel.parse(value); 529 } 530 catch (RuntimeException e) { 531 return fallback; 532 } 533 } 534 535 /* 536 * The unused key level as resolved from properties, before LogAlerts exists, like 537 * failLevelOr. An invalid value is reported by of(...). 538 */ 539 static UnusedKeyLevel unusedKeyLevelOr(LogProperties properties, UnusedKeyLevel fallback) { 540 var value = properties.valueOrNull(LogProperties.ALERTS_UNUSED_KEY_PROPERTY); 541 try { 542 return value == null ? fallback : UnusedKeyLevel.parse(value); 543 } 544 catch (RuntimeException e) { 545 return fallback; 546 } 547 } 548 549 /* 550 * Package-friend static factory: resolves and validates both 551 * LogProperties#ALERTS_CAPACITY_PROPERTY and 552 * LogProperties#ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY against one shared Validator 553 * - a mistake in both at once is reported together instead of only the first one 554 * found - then constructs. requirePositiveCapacity is a plain int -> int validating 555 * transform (not an object construction) specifically so it can sit inside map(...) 556 * without tripping CheckerFramework's Initialization Checker, which does not refine a 557 * `new Foo(...)` expression back to @Initialized when it appears directly inside a 558 * lambda body passed through a generic method like map(...) - the one and only `new 559 * DefaultLogAlerts(...)` call stays a plain, unconditional statement here instead. 560 */ 561 static DefaultLogAlerts of(LogProperties properties, FailLevel failLevelFallback, 562 UnusedKeyLevel unusedKeyLevelFallback, boolean help) { 563 var validator = LogProperty.Validator.of(LogAlerts.class); 564 var failLevelResult = properties.forKey(LogProperties.ALERTS_FAIL_PROPERTY) 565 .ofString() 566 .map(FailLevel::parse) 567 .or(failLevelFallback) 568 .validateIfError(validator); 569 var unobservedErrorsActionResult = properties.forKey(LogProperties.ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY) 570 .ofString() 571 .map(UnobservedErrorsAction::parse) 572 .or(UnobservedErrorsAction.DUMP) 573 .validateIfError(validator); 574 var unusedKeyLevelResult = properties.forKey(LogProperties.ALERTS_UNUSED_KEY_PROPERTY) 575 .ofString() 576 .map(UnusedKeyLevel::parse) 577 .or(unusedKeyLevelFallback) 578 .validateIfError(validator); 579 var capacityResult = properties.forKey(LogProperties.ALERTS_CAPACITY_PROPERTY) 580 .ofInt() 581 .or(LogAlerts.DEFAULT_CAPACITY) 582 .map(DefaultLogAlerts::requirePositiveCapacity) 583 .validateIfError(validator); 584 validator.validate(); 585 var failLevel = failLevelResult.value(); 586 String failLevelSource = LogProperties.ALERTS_FAIL_PROPERTY + "=" + failLevel.name().toLowerCase(Locale.ROOT); 587 if (help && failLevel != FailLevel.WARNING) { 588 failLevel = FailLevel.WARNING; 589 failLevelSource = "logging.debug=help"; 590 } 591 return new DefaultLogAlerts(capacityResult.value(), unobservedErrorsActionResult.value(), failLevel, 592 failLevelSource, unusedKeyLevelResult.value()); 593 } 594 595 private static int requirePositiveCapacity(int capacity) { 596 if (capacity <= 0) { 597 throw new IllegalArgumentException("capacity should be greater than 0"); 598 } 599 return capacity; 600 } 601 602 @Override 603 public void alert(LogEvent event) { 604 var frozen = event.freeze(); 605 total.incrementAndGet(); 606 if (frozen.level() == Level.ERROR) { 607 errors.incrementAndGet(); 608 } 609 lock.lock(); 610 try { 611 if (!startupComplete && failLevel.fails(frozen.level())) { 612 startupFailures++; 613 if (firstStartupFailure == null) { 614 firstStartupFailure = frozen; 615 } 616 } 617 if (size < ring.length) { 618 ring[(start + size) % ring.length] = frozen; 619 size++; 620 } 621 else { 622 ring[start] = frozen; 623 start = (start + 1) % ring.length; 624 } 625 } 626 finally { 627 lock.unlock(); 628 } 629 for (var listener : listeners) { 630 try { 631 listener.onAlert(frozen); 632 } 633 catch (Exception e) { 634 MetaLog.INSTANCE.log(eventFactory.eventNoArg(Level.ERROR, "LogAlerts.Listener threw", e)); 635 } 636 } 637 if (frozen.level() == Level.WARNING || frozen.level() == Level.ERROR) { 638 MetaLog.INSTANCE.log(frozen); 639 } 640 } 641 642 @Override 643 public AutoCloseable addListener(Listener listener) { 644 listeners.add(listener); 645 hasExternalListener = true; 646 return () -> listeners.remove(listener); 647 } 648 649 /* 650 * DefaultLogConfig's own metrics-bridge listener goes through this instead of 651 * addListener(Listener) - see hasExternalListener's own comment for why. 652 */ 653 void addInternalListener(Listener listener) { 654 listeners.add(listener); 655 } 656 657 @Override 658 public void start(LogConfig config) { 659 if (unobservedErrorsAction == UnobservedErrorsAction.NONE || hasExternalListener || errors.get() == 0) { 660 return; 661 } 662 var backlog = dump(); 663 MetaLog.error(eventFactory.eventNoArg(Level.ERROR, 664 backlog.size() + " alert(s) were recorded before any LogAlerts.Listener was registered - " 665 + "dumping the backlog now since nothing else will see it:", 666 null)); 667 for (var event : backlog) { 668 MetaLog.error(event); 669 } 670 if (unobservedErrorsAction == UnobservedErrorsAction.FAIL) { 671 throw new IllegalStateException( 672 backlog.size() + " alert(s) were recorded before any LogAlerts.Listener was registered and " 673 + LogProperties.ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY + "=FAIL - refusing to start."); 674 } 675 } 676 677 @Override 678 public void close() { 679 listeners.clear(); 680 lock.lock(); 681 try { 682 startupComplete = true; 683 firstStartupFailure = null; 684 } 685 finally { 686 lock.unlock(); 687 } 688 } 689 690 @Override 691 public List<LogEvent> dump() { 692 lock.lock(); 693 try { 694 List<LogEvent> list = new ArrayList<>(size); 695 for (int i = 0; i < size; i++) { 696 var e = ring[(start + i) % ring.length]; 697 if (e != null) { 698 list.add(e); 699 } 700 } 701 return List.copyOf(list); 702 } 703 finally { 704 lock.unlock(); 705 } 706 } 707 708 @Override 709 public void clear() { 710 lock.lock(); 711 try { 712 Arrays.fill(ring, null); 713 start = 0; 714 size = 0; 715 } 716 finally { 717 lock.unlock(); 718 } 719 } 720 721 @Override 722 public Stats stats() { 723 lock.lock(); 724 try { 725 return new Stats(total.get(), size, ring.length); 726 } 727 finally { 728 lock.unlock(); 729 } 730 } 731 732} 733 734/** 735 * Logging about logging. This is the static, always-available entry point used by code 736 * that cannot easily reach a {@link LogConfig} (e.g. before a {@link RainbowGum} is 737 * bound), and always reports directly (currently to stderr) rather than forwarding to a 738 * bound {@link RainbowGum}'s {@link LogConfig#alerts()} - alerts is a small bounded ring 739 * buffer meant to hold a curated, recent view of noteworthy logging-system problems, and 740 * {@link MetaLog} is used from enough different low level failure paths that routing it 741 * there risked flooding/evicting genuine alerts with whatever volume of things end up 742 * calling this class. 743 * <p> 744 * Components that already have (or can easily capture) a {@link LogConfig} - for example 745 * via {@link LogProvider} or {@link LogLifecycle#start(LogConfig)} - should prefer 746 * {@link LogConfig#alerts()} directly instead of this class. 747 * 748 * @author agentgt 749 */ 750@SuppressWarnings("ImmutableEnumChecker") 751enum MetaLog implements LogEventLogger { 752 753 INSTANCE; 754 755 static Supplier<? extends @Nullable PrintStream> output = () -> System.err; 756 757 private final LogFormatter formatter = LogFormatter.builder() // 758 .text("[") 759 .level() 760 .text("] - RAINBOW_GUM") 761 .text(" - ") 762 .add(LogFormatter.of((b, e) -> { 763 b.append(gumLoggerName(e.loggerName())); 764 })) 765 .text(" - ") 766 .message() 767 .textIfThrowable(" ") 768 .throwable() 769 .newline() 770 .build(); 771 772 /** 773 * Reports a logging system event at its original level. 774 * @param event event to log. 775 */ 776 static void error(LogEvent event) { 777 INSTANCE.log(event); 778 } 779 780 /** 781 * Logs an error in the logging system. 782 * @param loggerName derived from class. 783 * @param throwable error to log. 784 */ 785 static void error(Class<?> loggerName, Throwable throwable) { 786 String m = Objects.requireNonNullElse(throwable.getMessage(), "exception"); 787 error(loggerName, m, throwable); 788 } 789 790 /** 791 * Logs an error in the logging system. 792 * @param loggerName derived from class. 793 * @param message error message. 794 * @param throwable error to log. 795 */ 796 static void error(Class<?> loggerName, String message, Throwable throwable) { 797 var currentThread = Thread.currentThread(); 798 var event = LogEvent.of(Instant.now(), currentThread.getName(), currentThread.threadId(), Level.ERROR, 799 loggerName.getName(), message, KeyValues.of(), throwable); 800 error(event); 801 } 802 803 private static String gumLoggerName(String value) { 804 String prefix = "io.jstach.rainbowgum."; 805 if (value.startsWith(prefix)) { 806 value = value.substring(prefix.length()); 807 } 808 return value; 809 } 810 811 @Override 812 public void log(LogEvent event) { 813 var err = output.get(); 814 if (err != null) { 815 var sb = new StringBuilder(); 816 formatter.format(sb, event); 817 err.append(sb); 818 } 819 } 820 821}