001package io.jstach.rainbowgum; 002 003import java.lang.System.Logger.Level; 004import java.time.Instant; 005import java.util.ArrayList; 006import java.util.Arrays; 007import java.util.Collection; 008import java.util.EnumSet; 009import java.util.List; 010import java.util.Locale; 011import java.util.Objects; 012import java.util.Set; 013import java.util.concurrent.atomic.AtomicBoolean; 014import java.util.concurrent.locks.ReentrantLock; 015 016import org.jspecify.annotations.NonNull; 017import org.jspecify.annotations.Nullable; 018 019import io.jstach.rainbowgum.annotation.CaseChanging; 020 021/** 022 * Appenders are guaranteed to be written synchronously much like an actor in actor 023 * concurrency. They safely hold onto and communicate with the encoder and output. 024 * Appenders largely deal with correct locking, buffer reuse and flushing. 025 * {@linkplain LogAppender.AppenderFlag Flags } can be set to control the behavior the 026 * appenders and publishers can request different appender behavior through the flags. 027 * 028 * @see LogAppender.AppenderFlag 029 * @apiNote because appenders require complicated implementation and to guarantee 030 * integrity the implementations are encapsulated (sealed). 031 */ 032public sealed interface LogAppender extends LogLifecycle { 033 034 /** 035 * Default Console appender name. 036 */ 037 static final String CONSOLE_APPENDER_NAME = "console"; 038 039 /** 040 * Default output file appender name. 041 */ 042 static final String FILE_APPENDER_NAME = "file"; 043 044 /** 045 * Output appender property. 046 */ 047 static final String APPENDER_OUTPUT_PROPERTY = LogProperties.APPENDER_OUTPUT_PROPERTY; 048 049 /** 050 * Encoder appender property. 051 */ 052 static final String APPENDER_ENCODER_PROPERTY = LogProperties.APPENDER_ENCODER_PROPERTY; 053 054 /** 055 * Appender flags. A list of flags (usually comma separated). 056 * @see AppenderFlag 057 */ 058 static final String APPENDER_FLAGS_PROPERTY = LogProperties.APPENDER_FLAGS_PROPERTY; 059 060 /** 061 * Appender type. 062 * @see AppenderType 063 */ 064 static final String APPENDER_TYPE_PROPERTY = LogProperties.APPENDER_TYPE_PROPERTY; 065 066 /** 067 * Batch of events. <strong>DO NOT MODIFY THE ARRAY</strong>. Do not use the 068 * <code>length</code> of the passed in array but instead use <code>count</code> 069 * parameter. 070 * @param events an array guaranteed to be smaller than count. 071 * @param count the number of items. 072 */ 073 public void append(LogEvent[] events, int count); 074 075 /** 076 * Appends a single event. 077 * @param event event. 078 */ 079 public void append(LogEvent event); 080 081 /** 082 * Boolean like flags for appender that can be set with 083 * {@link LogAppender#APPENDER_FLAGS_PROPERTY}. Unlike {@link AppenderType} more than 084 * one flag can be set at once. 085 */ 086 @CaseChanging 087 public enum AppenderFlag { 088 089 /** 090 * By default the appender will call flush on each item appended or if in async 091 * batch mode for each batch. This flag disables that behavior so that flushing is 092 * left up to the output (or an external mechanism) instead. 093 */ 094 DISABLE_IMMEDIATE_FLUSH, 095 /** 096 * The appender will drop events on reentry which happens if an appender during 097 * its append causes recursive appending in the same thread. This is an analog to 098 * what 099 * <a href="https://logback.qos.ch/manual/appenders.html#AppenderBase">Logback 100 * does by default</a>. Note that this is done using {@link ReentrantLock} and not 101 * ThreadLocal like logback <strong>and is not done by default hence the 102 * flag!</strong> 103 * <p> 104 * This flag is to allow outputs that do logging themselves. For performance 105 * reasons and to allow async publishers it is recommended that you fix the output 106 * code such that it does not do logging. This flag is ignored if 107 * {@link #REENTRY_LOG} is set. 108 * <p> 109 * <strong>This flag will not fix outputs causing lool like logging if an async 110 * publisher is used!</strong> That is why it is recommended you fix the output by 111 * dropping events that would cause infinite loop like logging. 112 * @see #REENTRY_LOG 113 */ 114 REENTRY_DROP, 115 /** 116 * The appender will log events as errors to std error on reentry which happens if 117 * an appender during its append causes recursive appending in the same thread. 118 * This is an analog to what 119 * <a href="https://logback.qos.ch/manual/appenders.html#AppenderBase">Logback 120 * does by default</a>. Note that this is done using {@link ReentrantLock} and not 121 * ThreadLocal like logback <strong>and is not done by default hence the 122 * flag!</strong> This flag is to resolve failures of outputs that then do 123 * logging. 124 * <p> 125 * This flag takes precedence over {@link #REENTRY_DROP}. 126 */ 127 REENTRY_LOG; 128 129 static Set<AppenderFlag> parse(Collection<String> value) { 130 if (value.isEmpty()) { 131 return EnumSet.noneOf(AppenderFlag.class); 132 } 133 var s = EnumSet.noneOf(AppenderFlag.class); 134 for (var v : value) { 135 s.add(parse(v)); 136 } 137 return s; 138 } 139 140 static AppenderFlag parse(String value) { 141 String v = value.toUpperCase(Locale.ROOT); 142 return AppenderFlag.valueOf(v); 143 } 144 145 } 146 147 /** 148 * The buffer/locking strategy an appender uses. Unlike {@link AppenderFlag} exactly 149 * one is in effect for a given appender - set on the {@link LogAppender.Builder} (or 150 * via {@link LogAppender#APPENDER_TYPE_PROPERTY}) at construction time and fixed for 151 * the appender's lifetime; a publisher cannot change it afterward. 152 */ 153 @CaseChanging 154 public enum AppenderType { 155 156 /** 157 * The appender will create a single buffer that will be reused and will be 158 * protected by the appenders locking. 159 */ 160 REUSE_BUFFER, 161 /** 162 * The appender will give each thread its own reusable buffer (a 163 * {@link ThreadLocal}) instead of allocating a new buffer per event. Encoding is 164 * done <strong>outside</strong> the appender's lock (the thread's buffer is only 165 * visited by that thread so no protection is needed while encoding) with the lock 166 * only held for the final write to the output. 167 * <p> 168 * The same {@link ThreadLocal} buffer is used regardless of whether the calling 169 * thread is a platform or virtual thread. A virtual thread's entry becomes 170 * collectible once the thread itself terminates, and a typical unit of work (e.g. 171 * one HTTP request) logs several times on the same thread, so reusing the buffer 172 * across those calls still pays off even for short-lived virtual threads. 173 * <p> 174 * This is the default type - see {@link #SYNCHRONIZED_THREAD_LOCAL_BUFFER} for 175 * the alternative lock-kind opt-in. 176 */ 177 LOCK_THREAD_LOCAL_BUFFER, 178 /** 179 * Like {@link #LOCK_THREAD_LOCAL_BUFFER} (a reused per-thread buffer, encoding 180 * done outside any lock) except the final write to the output is protected by a 181 * plain {@code synchronized} block (the JVM's intrinsic monitor) instead of a 182 * {@link ReentrantLock}. 183 * <p> 184 * The Java language has no way to acquire a monitor in one method call and 185 * release it in another, so this appender's critical sections are written as 186 * literal {@code synchronized} blocks rather than going through a shared lock 187 * abstraction the way {@link #LOCK_THREAD_LOCAL_BUFFER} does. 188 * {@link AppenderFlag#REENTRY_DROP} and {@link AppenderFlag#REENTRY_LOG} are 189 * still honored though - {@link Thread#holdsLock(Object)} is the 190 * {@code synchronized} equivalent of {@code ReentrantLock}'s 191 * {@code isHeldByCurrentThread()}, so reentrancy is detected the same way. 192 * <p> 193 * Motivated by Log4j2's own garbage-free appenders using {@code synchronized} 194 * rather than a {@code java.util.concurrent} lock around their buffer-transfer 195 * step, and confirmed by real-workload benchmarking to outperform 196 * {@link ReentrantLock} under platform-thread contention - this was the default 197 * for a time. Real-workload benchmarking under virtual threads found the 198 * opposite, a large and reproducible loss versus {@link ReentrantLock} for 199 * reasons not fully understood (classic JEP 491 pinning was checked and ruled 200 * out), so {@link #LOCK_THREAD_LOCAL_BUFFER} is the default now and this type is 201 * an opt-in for platform-thread-heavy deployments that want the edge. 202 * <p> 203 * <strong>Explicitly setting this type honors it</strong> even on a JDK where 204 * {@code synchronized} still pins the carrier platform thread when called from a 205 * virtual thread (before <a href="https://openjdk.org/jeps/491">JEP 491</a>, 206 * finalized in JDK 24) - the one exception is 207 * {@code LogProperties#GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY}: when that global 208 * property is active it downgrades even an explicit request for this type to 209 * {@link #LOCK_THREAD_LOCAL_BUFFER}, since its whole point is a hard guarantee 210 * independent of anything else in the configuration. 211 */ 212 SYNCHRONIZED_THREAD_LOCAL_BUFFER, 213 /** 214 * Like {@link #LOCK_THREAD_LOCAL_BUFFER} (encoding done outside the lock, the 215 * lock only guards the final write to the output) except the buffer is allocated 216 * fresh for every single event instead of being cached in a {@link ThreadLocal}. 217 * <p> 218 * For deployments that want a hard guarantee of never using {@link ThreadLocal} 219 * anywhere in the logging path - {@link #REUSE_BUFFER} is the existing 220 * no-{@code ThreadLocal} option, but it holds its lock across the entire 221 * encode-then-write critical section (the single shared buffer must stay 222 * protected for as long as anything is writing into it), so it pays for that 223 * guarantee with lock contention proportional to encoding cost, not just I/O 224 * cost. This type keeps {@link #LOCK_THREAD_LOCAL_BUFFER}'s low-contention shape 225 * (encode into a buffer nothing else can see, lock only for the write) while 226 * dropping the {@link ThreadLocal} - the buffer is simply a local variable, not 227 * cached anywhere, so nothing needs to be evicted or leaked-if-forgotten either. 228 * The tradeoff is a fresh buffer allocation (and whatever it grows to internally) 229 * on every single event instead of amortizing that allocation across a thread's 230 * whole lifetime. 231 * @apiNote not the default - {@link #LOCK_THREAD_LOCAL_BUFFER}'s reused buffer is 232 * the better choice unless avoiding {@link ThreadLocal} entirely is a hard 233 * requirement, not just a preference. 234 */ 235 LOCK_NEW_BUFFER, 236 /* 237 * Real-workload benchmarking under virtual threads found 238 * SYNCHRONIZED_THREAD_LOCAL_BUFFER and LOCK_THREAD_LOCAL_BUFFER each winning by a 239 * real, repeatable, double-digit-percentage margin on their own platform (native 240 * image, HotSpot respectively) and losing by a comparable margin on the other - a 241 * genuine cross-over, not just a shrinking gap, so no single fixed choice serves 242 * both well. Matters most for something distributed as more than one build of the 243 * same artifact for the same deployment - a native executable and a plain jar, 244 * say - where picking correctly per build would otherwise mean either shipping 245 * different configuration for each or sniffing the platform in application code. 246 * Detection is the same org.graalvm.nativeimage.imagecode system property check 247 * every native-image-aware framework uses to avoid a hard dependency on GraalVM's 248 * own SDK. 249 */ 250 /** 251 * Picks whichever concrete type actually performs best for the platform this 252 * process is currently running on, decided once, at appender construction time, 253 * not re-checked afterward. <strong>Currently</strong> that means 254 * {@link #SYNCHRONIZED_THREAD_LOCAL_BUFFER} when running as a GraalVM native 255 * image, or {@link #LOCK_THREAD_LOCAL_BUFFER} otherwise - but that specific 256 * mapping is an implementation detail of this heuristic, not a contract. The 257 * whole point of this type is to track whichever choice is actually best, so 258 * which concrete type a given platform resolves to today may change in a future 259 * release without notice, as benchmarking improves or platforms evolve. 260 * <strong>If your deployment needs a specific type to always be used, pick that 261 * type explicitly instead of relying on what {@code AUTO_DETECT} happens to 262 * resolve to right now.</strong> 263 * <p> 264 * <strong>Not the default</strong> - requires explicitly requesting this type, 265 * programmatically or via {@link LogAppender#APPENDER_TYPE_PROPERTY}. Downgraded 266 * the same as an explicit request for whichever type it currently resolves to 267 * would be: by {@code LogProperties#GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY} if 268 * it resolves to {@link #SYNCHRONIZED_THREAD_LOCAL_BUFFER}, and by 269 * {@code LogProperties#GLOBAL_THREADLOCAL_DISABLED_PROPERTY} either way. 270 */ 271 AUTO_DETECT; 272 273 static AppenderType parse(String value) { 274 String v = value.toUpperCase(Locale.ROOT); 275 return AppenderType.valueOf(v); 276 } 277 278 } 279 280 /** 281 * Creates a builder. 282 * @param name appender name. 283 * @return builder. 284 */ 285 public static Builder builder(String name) { 286 /* 287 * Eagerly triggers LogProperties' own key-parameter-value validation now, 288 * matching every annotation-processor-generated Builder's own constructor, which 289 * does the same via its own eager interpolateKey call: needed because this 290 * hand-written Builder can otherwise skip every keyed property lookup entirely 291 * when every field is set explicitly, never validating the name at all. Wrapped 292 * into a ValidationException the same way a generated builder's constructor is, 293 * so a bad name reports the same exception type regardless of whether it came 294 * from logging.appenders (LogAppenderRegistry's own validateNames call) or a 295 * direct programmatic builder(name) call like this one. 296 */ 297 try { 298 LogProperties.interpolateNamedKey(LogProperties.APPENDER_PREFIX, name); 299 } 300 catch (IllegalArgumentException e) { 301 throw LogProperty.ValidationException.of(LogAppender.class, e); 302 } 303 return new Builder(name); 304 } 305 306 /** 307 * Builder for creating standard appenders. Whatever is not set explicitly is resolved 308 * from properties keyed under {@link LogProperties#APPENDER_PREFIX} for this 309 * builder's name (see {@link #fromProperties(LogProperties)}), and failing that from 310 * a small set of defaults: no encoder resolves one from the output's own type, no 311 * flags means none are set, and no appender type means 312 * {@link AppenderType#LOCK_THREAD_LOCAL_BUFFER} - or {@link AppenderType#AUTO_DETECT} 313 * instead, if {@link LogProperties#GLOBAL_OPTIMIZE_PROPERTY} is enabled. There is 314 * deliberately no such default for output, since a required source is not something a 315 * generic named appender can safely guess, except for the well known 316 * {@code "console"}/ {@code "file"} names, which register their own fallback via 317 * {@link #outputDefault(LogProvider)}. 318 */ 319 public static final class Builder implements LogBuilder<Builder, LogAppender> { 320 321 private @Nullable LogProvider<? extends LogOutput> output = null; 322 323 /* 324 * A weaker fallback than an explicit output(...) call: consulted only if neither 325 * that nor APPENDER_OUTPUT_PROPERTY resolves anything. Package-private, not part 326 * of the public Builder API: DefaultAppenderRegistry is the only caller, for 327 * "console"'s stdout default, where the precedence is deliberately the opposite 328 * of an ordinary explicit value (the property is meant to override this default, 329 * not the other way around). 330 */ 331 private @Nullable LogProvider<? extends LogOutput> outputDefault = null; 332 333 private @Nullable LogProvider<? extends LogEncoder> encoder = null; 334 335 private @Nullable EnumSet<AppenderFlag> flags = null; 336 337 private @Nullable AppenderType appenderType = null; 338 339 private final String name; 340 341 private Builder(String name) { 342 this.name = name; 343 } 344 345 Builder outputDefault(LogProvider<? extends LogOutput> outputDefault) { 346 this.outputDefault = outputDefault; 347 return this; 348 } 349 350 /** 351 * Name of the appender. 352 * @return name. 353 */ 354 public String name() { 355 return this.name; 356 } 357 358 /** 359 * Sets output. 360 * @param output output. 361 * @return builder. 362 */ 363 public Builder output(LogProvider<? extends LogOutput> output) { 364 this.output = output; 365 return this; 366 } 367 368 /** 369 * Sets output. 370 * @param output output. 371 * @return builder. 372 */ 373 public Builder output(LogOutput output) { 374 this.output = LogProvider.of(output); 375 return this; 376 } 377 378 /** 379 * Sets formatter as encoder. 380 * @param formatter formatter to be converted to encoder. 381 * @return builder. 382 * @see LogEncoder#of(LogFormatter) 383 */ 384 public Builder formatter(LogFormatter formatter) { 385 this.encoder = LogEncoder.of(formatter); 386 return this; 387 } 388 389 /** 390 * Sets formatter as encoder. 391 * @param formatter formatter to be converted to encoder. 392 * @return builder. 393 * @see LogEncoder#of(LogFormatter) 394 */ 395 public Builder formatter(LogFormatter.EventFormatter formatter) { 396 this.encoder = LogEncoder.of(formatter); 397 return this; 398 } 399 400 /** 401 * Sets encoder. 402 * @param encoder encoder not <code>null</code>. 403 * @return builder. 404 */ 405 public Builder encoder(LogProvider<? extends LogEncoder> encoder) { 406 this.encoder = encoder; 407 return this; 408 } 409 410 /** 411 * Sets encoder. 412 * @param encoder encoder not <code>null</code>. 413 * @return builder. 414 */ 415 public Builder encoder(LogEncoder encoder) { 416 this.encoder = LogProvider.of(encoder); 417 return this; 418 } 419 420 /** 421 * Sets appender flags. 422 * @param flags flags will replace all flags currently set. 423 * @return this. 424 */ 425 public Builder flags(Collection<AppenderFlag> flags) { 426 _flags().addAll(flags); 427 return this; 428 } 429 430 private EnumSet<AppenderFlag> _flags() { 431 EnumSet<AppenderFlag> flags = this.flags; 432 if (flags == null) { 433 this.flags = flags = EnumSet.noneOf(AppenderFlag.class); 434 } 435 return flags; 436 } 437 438 /** 439 * Adds a flag. 440 * @param flag flag. 441 * @return this. 442 */ 443 public Builder flag(AppenderFlag flag) { 444 _flags().add(flag); 445 return this; 446 } 447 448 /** 449 * Sets the appender type (buffer/locking strategy). If not set it is resolved 450 * from {@link LogAppender#APPENDER_TYPE_PROPERTY} or otherwise defaults to 451 * {@link AppenderType#LOCK_THREAD_LOCAL_BUFFER} ({@link AppenderType#AUTO_DETECT} 452 * instead if {@link LogProperties#GLOBAL_OPTIMIZE_PROPERTY} is enabled). Calling 453 * this method always wins over both. 454 * @param appenderType appender type. 455 * @return this. 456 */ 457 public Builder appenderType(AppenderType appenderType) { 458 this.appenderType = appenderType; 459 return this; 460 } 461 462 @Override 463 public String propertyPrefix() { 464 return LogProperties.APPENDER_PREFIX; 465 } 466 467 /** 468 * Fills in whatever of flags/appender type is not already explicitly set on this 469 * builder from properties keyed under {@link #propertyPrefix()} for this 470 * builder's name; an already-set field always wins over the property, matching 471 * every other generated builder's {@code fromProperties} in this project. Both 472 * are resolved and validated together against one {@link LogProperty.Validator}, 473 * so a builder with both malformed reports both in a single exception rather than 474 * just the first one reached. 475 * <p> 476 * Output/encoder are deliberately not handled here even though they too are keyed 477 * under {@link #propertyPrefix()}: turning either into a concrete, registered 478 * {@link LogOutput}/{@link LogEncoder} needs a {@link LogConfig} (to actually 479 * look up the URI scheme), which this method does not have. {@link #build()} 480 * resolves those two once a real {@link LogConfig} is available, using the same 481 * property keys. 482 * @param properties properties to resolve unset fields from. 483 * @return this. 484 */ 485 @Override 486 public Builder fromProperties(LogProperties properties) { 487 /* 488 * Register every result with the validator first, and only call validate(), 489 * which throws one aggregate exception up front if anything registered is 490 * Missing (when added via add(...)) or an Error (either method), before 491 * extracting any individual value below. Extracting a value via 492 * value()/valueOrNull() throws immediately for a still-unhandled Error (see 493 * LogProperty.Result.Error's own valueOrNull()), which would otherwise let 494 * the first bad property escape unwrapped before the Validator ever got a 495 * chance to collect the rest: the same ordering the annotation processor 496 * generates for every other builder in this project. 497 */ 498 var validator = LogProperty.Validator.of(LogAppender.class); 499 var flagsResult = flags == null ? properties.forKey(APPENDER_FLAGS_PROPERTY, name) 500 .ofList() 501 .map(AppenderFlag::parse) 502 .validateIfError(validator) : null; 503 var appenderTypeResult = appenderType == null ? properties.forKey(APPENDER_TYPE_PROPERTY, name) 504 .ofString() 505 .map(AppenderType::parse) 506 .validateIfError(validator) : null; 507 validator.validate(); 508 if (flagsResult != null) { 509 var resolved = flagsResult.valueOrNull(); 510 // AppenderFlag.parse always returns an EnumSet (empty or not), so 511 // EnumSet.copyOf is always safe here, never the "non-EnumSet and empty" 512 // case it rejects. 513 if (resolved != null) { 514 flags = EnumSet.copyOf(resolved); 515 } 516 } 517 if (appenderTypeResult != null) { 518 appenderType = appenderTypeResult.valueOrNull(); 519 } 520 return this; 521 } 522 523 /** 524 * Builds. 525 * @return an appender factory. 526 */ 527 public LogProvider<LogAppender> build() { 528 /* 529 * We need to capture parameters since appender creation needs to be lazy, and 530 * a copy is made below (rather than mutating this builder's own fields) so a 531 * shared/reused Builder instance is never mutated by a later 532 * fromProperties(...) call the lazy lambda makes once a real LogConfig is 533 * available. 534 */ 535 var _name = name; 536 var _output = output; 537 var _outputDefault = outputDefault; 538 var _encoder = encoder; 539 var _flags = flags; 540 var _appenderType = appenderType; 541 /* 542 * TODO should we use the parent name for resolution? 543 */ 544 return (n, config) -> { 545 var b = new Builder(_name); 546 b.flags = _flags; 547 b.appenderType = _appenderType; 548 b.fromProperties(config.properties()); 549 550 LogOutput output = LogProvider.provideOrNull(_output, _name, config); 551 if (output == null) { 552 /* 553 * A malformed (not just missing) property must throw here, 554 * immediately, regardless of whether a default exists below: e.g. 555 * "console" with a genuinely bad logging.appender.console.output must 556 * fail loudly, not silently fall back to stdout. Only the Missing 557 * case falls through to outputDefault. 558 */ 559 output = switch (outputProperty(_name, config)) { 560 case LogProperty.Result.Success<LogOutput> s -> s.value(); 561 case LogProperty.Result.Missing<LogOutput> m -> null; 562 case LogProperty.Result.Error<LogOutput> e -> e.value(); 563 }; 564 } 565 if (output == null) { 566 output = LogProvider.provideOrNull(_outputDefault, _name, config); 567 } 568 if (output == null) { 569 /* 570 * No explicit value, no property, no default: the same missing 571 * -property failure a required property with no fallback produces 572 * anywhere else. Re-deriving the same (definitely still Missing) 573 * result and calling value() throws it unwrapped, no Validator 574 * involved, same as before this refactor. 575 */ 576 output = DefaultAppenderRegistry.rawValue(outputProperty(_name, config)).value(); 577 } 578 579 final LogOutput finalOutput = output; 580 LogEncoder explicitEncoder = _encoder != null ? LogProvider.provideOrNull(_encoder, _name, config) 581 : null; 582 LogEncoder encoder; 583 if (finalOutput instanceof LogOutput.ProvidesEncoder pe 584 && pe.policy() == LogOutput.ProvidesEncoder.Policy.MANDATORY) { 585 boolean propertyWired = !(encoderProperty(_name, config) instanceof LogProperty.Result.Missing); 586 if (explicitEncoder != null || propertyWired) { 587 throw LogProperty.ValidationException 588 .of(LogAppender.class, new IllegalArgumentException("Appender '" + _name + "' output " 589 + finalOutput.getClass().getName() 590 + " provides a mandatory encoder and does not allow another encoder to be configured.")); 591 } 592 encoder = pe.encoder(_name, config); 593 } 594 else { 595 encoder = explicitEncoder; 596 if (encoder == null) { 597 var propertyResult = encoderProperty(_name, config); 598 if (finalOutput instanceof LogOutput.ProvidesEncoder pe 599 && propertyResult instanceof LogProperty.Result.Missing) { 600 encoder = pe.encoder(_name, config); 601 } 602 else { 603 encoder = DefaultAppenderRegistry.rawValue(propertyResult.or(() -> config.encoderRegistry() 604 .encoderForOutputType(finalOutput.type()) 605 .provide(_name, config))).value(); 606 } 607 } 608 } 609 610 Set<AppenderFlag> flags = b.flags != null ? b.flags : EnumSet.noneOf(AppenderFlag.class); 611 AppenderType appenderType = b.appenderType != null ? b.appenderType 612 : AbstractLogAppender.globalOptimizeEnabled ? AppenderType.AUTO_DETECT 613 : AppenderType.LOCK_THREAD_LOCAL_BUFFER; 614 615 return DirectLogAppender.of(_name, output, encoder, appenderType, flags, config.alerts(), 616 config.metrics()); 617 }; 618 } 619 620 /* 621 * Resolves APPENDER_OUTPUT_PROPERTY all the way to a concrete LogOutput (not just 622 * a LogProvider), via LogProperty.provideValue so a failure inside the provided 623 * LogOutput's own construction (its own separate property tree, e.g. 624 * logging.output.file.bufferSize) is not relabeled "Error converting property" as 625 * if APPENDER_OUTPUT_PROPERTY's own value had failed to convert - see 626 * LogProperty#providingError. Moved here since output resolution is now entirely 627 * this Builder's concern. 628 */ 629 private static LogProperty.Result<LogOutput> outputProperty(String name, LogConfig config) { 630 return LogProperty.provideValue( 631 config.properties().forKey(APPENDER_OUTPUT_PROPERTY, name).ofProvider(LogOutput::of), name, config); 632 } 633 634 private static LogProperty.Result<LogEncoder> encoderProperty(String name, LogConfig config) { 635 return LogProperty.provideValue( 636 config.properties().forKey(APPENDER_ENCODER_PROPERTY, name).ofProvider(LogEncoder::of), name, 637 config); 638 } 639 640 } 641 642 /** 643 * Provides appenders safely to the publisher. The providing calls of 644 * <code>asXXX</code> can only be called once as they register the appenders. 645 */ 646 final class Appenders { 647 648 private final AtomicBoolean created = new AtomicBoolean(); 649 650 private final String name; 651 652 private final LogConfig config; 653 654 private final List<LogProvider<LogAppender>> appenders; 655 656 /* 657 * Set once, inside asList()/asSingle(), so LogReporter can find out what a 658 * publisher was actually given without asking the publisher itself (which would 659 * mean every current and future publisher implementation cooperating): see 660 * resolvedOrNull(). Not re-derivable by calling asList()/asSingle() again: both 661 * are one-shot (see the created guard above). 662 */ 663 private @Nullable List<? extends LogAppender> resolved; 664 665 Appenders(String name, LogConfig config, List<LogProvider<LogAppender>> appenders) { 666 super(); 667 this.name = name; 668 this.config = config; 669 this.appenders = appenders; 670 } 671 672 /** 673 * Return the appenders as a list. 674 * @return list of appenders. 675 * @throws IllegalStateException if appenders are already registered. 676 */ 677 public List<? extends LogAppender> asList() throws IllegalStateException { 678 if (created.compareAndSet(false, true)) { 679 var apps = appenders(); 680 List<LogAppender> appenders = new ArrayList<>(); 681 for (var a : apps) { 682 appenders.add(register(a)); 683 } 684 resolved = appenders; 685 return appenders; 686 } 687 else { 688 throw new IllegalStateException("Appenders already provided."); 689 } 690 691 } 692 693 /** 694 * Consolidate the appenders as a single appender, appended synchronously. If more 695 * than one appender is combined, each keeps its own independent lock and is 696 * appended to directly - see {@link CompositeLogAppender}. 697 * @return single appender. 698 * @throws IllegalStateException if appenders are already registered. 699 */ 700 public LogAppender asSingle() throws IllegalStateException { 701 if (created.compareAndSet(false, true)) { 702 var apps = appenders(); 703 var appender = composite(apps); 704 resolved = List.of(appender); 705 return register(appender); 706 } 707 else { 708 throw new IllegalStateException("Appenders already provided."); 709 } 710 } 711 712 /** 713 * The appenders actually resolved by whichever of {@link #asList()}/ 714 * {@link #asSingle()} a publisher factory called, or <code>null</code> if neither 715 * has been called yet (a publisher factory that never calls either one has no 716 * real appenders to report on regardless). 717 * @return resolved appenders, or <code>null</code>. 718 */ 719 @Nullable List<? extends LogAppender> resolvedOrNull() { 720 return resolved; 721 } 722 723 private LogAppender register(LogAppender appender) { 724 return switch (appender) { 725 case DirectLogAppender ia -> { 726 config.serviceRegistry().put(LogAppender.class, name + "." + ia.name(), ia); 727 yield ia; 728 } 729 case CompositeLogAppender ca -> { 730 config.serviceRegistry().put(LogAppender.class, name, ca); 731 yield ca; 732 } 733 default -> { 734 throw new IllegalStateException(); 735 } 736 }; 737 } 738 739 private List<LogAppender> appenders() { 740 return LogProvider.flatten(appenders) 741 .describe(n -> "Appenders for route: '" + n + "'") 742 .provide(name, config); 743 } 744 745 /** 746 * Creates a composite log appender from many, each keeping its own independent 747 * lock. 748 * @param appenders appenders. 749 * @return appender. 750 */ 751 private static LogAppender composite(List<? extends LogAppender> appenders) { 752 if (appenders.isEmpty()) { 753 throw new IllegalArgumentException("A single appender is required"); 754 } 755 if (appenders.size() == 1) { 756 return Objects.requireNonNull(appenders.get(0)); 757 } 758 return CompositeLogAppender.of(appenders); 759 } 760 761 } 762 763 @Override 764 public void close(); 765 766} 767 768interface AppenderVisitor { 769 770 boolean consume(DirectLogAppender appender); 771 772} 773 774/** 775 * This is a JAVADOC BUG 776 */ 777sealed interface InternalLogAppender extends LogAppender, Actor { 778 779 static InternalLogAppender of(LogAppender appender) { 780 return Objects.requireNonNull((InternalLogAppender) appender); // TODO eclipse 781 // bug. 782 } 783 784 /** 785 * An appender can act on actions. One of the key actions is reopening files. 786 * @param action action to run. 787 * @return alert events for anything that failed, empty if the action fully succeeded. 788 */ 789 @Override 790 public List<LogEvent> act(LogAction action); 791 792} 793 794sealed interface DirectLogAppender extends InternalLogAppender { 795 796 String name(); 797 798 LogOutput output(); 799 800 LogEncoder encoder(); 801 802 default List<LogEvent> _request(LogAction action) { 803 return switch (action) { 804 case LogAction.StandardAction a -> switch (a) { 805 case LogAction.StandardAction.REOPEN -> reopen(); 806 case LogAction.StandardAction.FLUSH -> flush(); 807 }; 808 }; 809 } 810 811 /** 812 * Reopens {@link #output()}. Any failure is reported to the alert system rather than 813 * thrown, at {@link Level#ERROR}. 814 * @return the alert event if reopening failed, empty if it succeeded. 815 */ 816 List<LogEvent> reopen(); 817 818 /** 819 * Flushes {@link #output()}. Any failure is reported to the alert system rather than 820 * thrown, at {@link Level#ERROR}. 821 * @return the alert event if flushing failed, empty if it succeeded. 822 */ 823 List<LogEvent> flush(); 824 825 static DirectLogAppender of(String name, LogOutput output, LogEncoder encoder, AppenderType type, 826 Set<LogAppender.AppenderFlag> flags, LogAlerts alerts, LogMetrics metrics) { 827 type = AbstractLogAppender.resolveAutoDetectAppenderType(type); 828 type = AbstractLogAppender.guardSynchronizedAppenderType(type); 829 type = AbstractLogAppender.guardThreadLocalAppenderType(type); 830 return switch (type) { 831 case REUSE_BUFFER -> 832 new ReuseBufferLogAppender(name, output, encoder, flags, new ReentrantLock(), alerts, metrics); 833 case SYNCHRONIZED_THREAD_LOCAL_BUFFER -> 834 new SynchronizedThreadLocalBufferLogAppender(name, output, encoder, flags, alerts, metrics); 835 case LOCK_THREAD_LOCAL_BUFFER -> new LockThreadLocalBufferLogAppender(name, output, encoder, flags, 836 new ReentrantLock(), alerts, metrics); 837 case LOCK_NEW_BUFFER -> 838 new LockNewBufferLogAppender(name, output, encoder, flags, new ReentrantLock(), alerts, metrics); 839 case AUTO_DETECT -> 840 throw new IllegalStateException("AUTO_DETECT should have already been resolved to a concrete type"); 841 }; 842 } 843 844} 845 846/** 847 * An abstract appender to help create custom appenders. 848 */ 849sealed abstract class AbstractLogAppender implements DirectLogAppender { 850 851 /* 852 * Set once from LogProperties#GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY during 853 * LogConfig construction (see DefaultLogConfig) - a global, process-wide guarantee 854 * that no appender will ever use `synchronized`, for deployments that want that 855 * guaranteed even when something explicitly requests 856 * SYNCHRONIZED_THREAD_LOCAL_BUFFER. Global (not per-route/per-appender) by design, 857 * matching the property's own scope. 858 */ 859 static volatile boolean forceReentrantLockAppenders = false; 860 861 /** 862 * Downgrades an explicit 863 * {@link LogAppender.AppenderType#SYNCHRONIZED_THREAD_LOCAL_BUFFER} to 864 * {@link LogAppender.AppenderType#LOCK_THREAD_LOCAL_BUFFER} if 865 * {@link #forceReentrantLockAppenders} is active - the enforcement point that makes 866 * the global no-synchronized guarantee a real guarantee rather than just a changed 867 * default. 868 * @param type type as given to an appender factory method. 869 * @return {@code type} unchanged, unless the guarantee is active and 870 * {@code SYNCHRONIZED_THREAD_LOCAL_BUFFER} was requested, in which case 871 * {@code LOCK_THREAD_LOCAL_BUFFER} instead. 872 */ 873 static LogAppender.AppenderType guardSynchronizedAppenderType(LogAppender.AppenderType type) { 874 if (!forceReentrantLockAppenders || type != LogAppender.AppenderType.SYNCHRONIZED_THREAD_LOCAL_BUFFER) { 875 return type; 876 } 877 return LogAppender.AppenderType.LOCK_THREAD_LOCAL_BUFFER; 878 } 879 880 /* 881 * Set once from LogProperties#GLOBAL_THREADLOCAL_DISABLED_PROPERTY during LogConfig 882 * construction (see DefaultLogConfig) - a global, process-wide guarantee that no 883 * appender will ever use ThreadLocal, for deployments that want that guaranteed even 884 * when something explicitly requests LOCK_THREAD_LOCAL_BUFFER (the default) or 885 * SYNCHRONIZED_THREAD_LOCAL_BUFFER. Global (not per-route/per-appender) by design, 886 * matching the property's own scope - rainbowgum-slf4j's MDC support independently 887 * reads the same property key to decide whether to disable itself too, see 888 * LogProperties#GLOBAL_THREADLOCAL_DISABLED_PROPERTY's javadoc. 889 */ 890 static volatile boolean forceNoThreadLocalAppenders = false; 891 892 /* 893 * Set once from LogProperties#GLOBAL_OPTIMIZE_PROPERTY during LogConfig construction 894 * (see DefaultLogConfig) - unlike 895 * forceReentrantLockAppenders/forceNoThreadLocalAppenders above, this is not a 896 * downgrade applied to an already-resolved type; it only changes what 897 * LogAppender.Builder#build() picks when appenderType was never set at all 898 * (explicitly, either way, always wins). See LogProperties#GLOBAL_OPTIMIZE_PROPERTY's 899 * javadoc for what it currently resolves to and why that is not a contract. 900 */ 901 static volatile boolean globalOptimizeEnabled = false; 902 903 /** 904 * Downgrades either {@link ThreadLocal}-backed type ( 905 * {@link LogAppender.AppenderType#LOCK_THREAD_LOCAL_BUFFER} or 906 * {@link LogAppender.AppenderType#SYNCHRONIZED_THREAD_LOCAL_BUFFER}) to 907 * {@link LogAppender.AppenderType#LOCK_NEW_BUFFER} if 908 * {@link #forceNoThreadLocalAppenders} is active - the enforcement point that makes 909 * the global no-{@link ThreadLocal} guarantee a real guarantee rather than just a 910 * changed default. An explicit {@link LogAppender.AppenderType#REUSE_BUFFER} request 911 * is left as-is - it is already {@link ThreadLocal}-free, so there is nothing to 912 * downgrade. 913 * @param type type as given to an appender factory method. 914 * @return {@code type} unchanged, unless the guarantee is active and a 915 * {@link ThreadLocal}-backed type was requested, in which case 916 * {@code LOCK_NEW_BUFFER} instead. 917 */ 918 static LogAppender.AppenderType guardThreadLocalAppenderType(LogAppender.AppenderType type) { 919 if (!forceNoThreadLocalAppenders) { 920 return type; 921 } 922 return switch (type) { 923 case LOCK_THREAD_LOCAL_BUFFER, SYNCHRONIZED_THREAD_LOCAL_BUFFER -> LogAppender.AppenderType.LOCK_NEW_BUFFER; 924 case REUSE_BUFFER, LOCK_NEW_BUFFER -> type; 925 case AUTO_DETECT -> 926 throw new IllegalStateException("AUTO_DETECT should have already been resolved to a concrete type"); 927 }; 928 } 929 930 /** 931 * Resolves {@link LogAppender.AppenderType#AUTO_DETECT} to 932 * {@link LogAppender.AppenderType#SYNCHRONIZED_THREAD_LOCAL_BUFFER} or 933 * {@link LogAppender.AppenderType#LOCK_THREAD_LOCAL_BUFFER} depending on 934 * {@link #isNativeImageRuntime()}; any other type is returned unchanged. Called 935 * before {@link #guardSynchronizedAppenderType(LogAppender.AppenderType)} and 936 * {@link #guardThreadLocalAppenderType(LogAppender.AppenderType)} so a resolved 937 * {@code SYNCHRONIZED_THREAD_LOCAL_BUFFER} is still subject to both of those global 938 * guarantees the same as if it had been requested directly. 939 * @param type type as given to an appender factory method. 940 * @return {@code type} unchanged unless it was {@code AUTO_DETECT}. 941 */ 942 static LogAppender.AppenderType resolveAutoDetectAppenderType(LogAppender.AppenderType type) { 943 if (type != LogAppender.AppenderType.AUTO_DETECT) { 944 return type; 945 } 946 return isNativeImageRuntime() ? LogAppender.AppenderType.SYNCHRONIZED_THREAD_LOCAL_BUFFER 947 : LogAppender.AppenderType.LOCK_THREAD_LOCAL_BUFFER; 948 } 949 950 /** 951 * The system property GraalVM native-image itself sets to {@code buildtime} during 952 * the image build and {@code runtime} when the built image actually executes - absent 953 * entirely on a plain JVM. The standard, dependency-free way every native-image-aware 954 * framework checks "am I native" without a hard compile-time dependency on GraalVM's 955 * own SDK just to ask this one question. 956 */ 957 static final String NATIVE_IMAGE_CODE_PROPERTY = "org.graalvm.nativeimage.imagecode"; 958 959 /** 960 * Whether this process is currently running as an executing (not building) GraalVM 961 * native image - see {@link #NATIVE_IMAGE_CODE_PROPERTY}. 962 * @return true if running as a native image at runtime. 963 */ 964 static boolean isNativeImageRuntime() { 965 return "runtime".equals(System.getProperty(NATIVE_IMAGE_CODE_PROPERTY)); 966 } 967 968 /** 969 * Whether an appender should drop (or drop-and-log) an append call because it is 970 * reentrant - i.e. the current thread is already inside a previous call to the same 971 * appender's write path, which happens if an output does logging itself during its 972 * own write. Shared by every appender that can detect reentrancy, regardless of 973 * whether it does so via a {@link ReentrantLock} ( 974 * {@code lock.isHeldByCurrentThread()}) or a {@code synchronized} block ( 975 * {@link Thread#holdsLock(Object)}) - callers pass in whichever check applies to 976 * them. Whenever this returns {@code true}, {@link LogMetrics#EVENTS_DROPPED_METRIC} 977 * is incremented by {@code count} regardless of whether 978 * {@link AppenderFlag#REENTRY_LOG} is set - counting and logging/alerting are 979 * separate concerns, so the count still happens even when the drop itself is silent. 980 * @param reentrant whether the current thread already holds this appender's 981 * lock/monitor. 982 * @param flags the appender's flags. 983 * @param metrics where to record {@link LogMetrics#EVENTS_DROPPED_METRIC} if events 984 * are dropped. 985 * @param count number of events that would be dropped - {@code 1} for a single event 986 * append, or the batch size for a batch append. 987 * @return {@code true} if the caller should drop the event(s) without appending. 988 */ 989 static boolean shouldDropForReentry(boolean reentrant, Set<LogAppender.AppenderFlag> flags, LogMetrics metrics, 990 int count) { 991 if (!reentrant) { 992 return false; 993 } 994 if (flags.contains(LogAppender.AppenderFlag.REENTRY_LOG)) { 995 Exception exception = new Exception("reentrant appender"); 996 MetaLog.error(LogAppender.class, exception); 997 metrics.errorCounter(LogMetrics.EVENTS_DROPPED_METRIC, count); 998 return true; 999 } 1000 if (flags.contains(LogAppender.AppenderFlag.REENTRY_DROP)) { 1001 metrics.errorCounter(LogMetrics.EVENTS_DROPPED_METRIC, count); 1002 return true; 1003 } 1004 return false; 1005 } 1006 1007 /** 1008 * name. 1009 */ 1010 protected final String name; 1011 1012 /** 1013 * output 1014 */ 1015 protected final LogOutput output; 1016 1017 /** 1018 * encoder 1019 */ 1020 protected final LogEncoder encoder; 1021 1022 protected final Set<LogAppender.AppenderFlag> flags; 1023 1024 protected final boolean immediateFlush; 1025 1026 /** 1027 * alerts for reporting encode/write failures that this appender catches so they never 1028 * propagate back to whatever application thread called logger.info(...). 1029 */ 1030 protected final LogAlerts alerts; 1031 1032 /** 1033 * metrics for recording counters like {@link LogMetrics#EVENTS_DROPPED_METRIC}. 1034 */ 1035 protected final LogMetrics metrics; 1036 1037 /** 1038 * Creates an appender from an output and encoder. 1039 * @param output set the output field and will be started and closed with the 1040 * appender. 1041 * @param encoder set the encoder field. 1042 * @param alerts alerts for reporting encode/write failures. 1043 * @param metrics metrics for recording counters. 1044 */ 1045 protected AbstractLogAppender(String name, LogOutput output, LogEncoder encoder, 1046 Set<LogAppender.AppenderFlag> flags, LogAlerts alerts, LogMetrics metrics) { 1047 super(); 1048 this.name = name; 1049 this.output = output; 1050 this.encoder = encoder; 1051 this.flags = flags; 1052 this.immediateFlush = !flags.contains(LogAppender.AppenderFlag.DISABLE_IMMEDIATE_FLUSH); 1053 this.alerts = alerts; 1054 this.metrics = metrics; 1055 } 1056 1057 @Override 1058 public void start(LogConfig config) { 1059 output.start(config); 1060 } 1061 1062 @Override 1063 public void close() { 1064 output.close(); 1065 } 1066 1067 @Override 1068 public String toString() { 1069 return getClass().getName() + "[name=" + name + " encoder=" + encoder + ", " + "output=" + output + ", flags=" 1070 + flags + "]"; 1071 } 1072 1073 @Override 1074 public String name() { 1075 return this.name; 1076 } 1077 1078 @Override 1079 public LogOutput output() { 1080 return this.output; 1081 } 1082 1083 @Override 1084 public LogEncoder encoder() { 1085 return this.encoder; 1086 } 1087 1088 @Override 1089 public List<LogEvent> reopen() { 1090 try { 1091 output.reopen(); 1092 return List.of(); 1093 } 1094 catch (Exception e) { 1095 var event = errorEvent(getClass(), "appender '" + name + "' failed to reopen output", e); 1096 alerts.error(event); 1097 return List.of(event); 1098 } 1099 } 1100 1101 @Override 1102 public List<LogEvent> flush() { 1103 try { 1104 output.flush(); 1105 return List.of(); 1106 } 1107 catch (Exception e) { 1108 var event = errorEvent(getClass(), "appender '" + name + "' failed to flush output", e); 1109 alerts.error(event); 1110 return List.of(event); 1111 } 1112 } 1113 1114 private static LogEvent errorEvent(Class<?> loggerName, String message, Throwable throwable) { 1115 var currentThread = Thread.currentThread(); 1116 return LogEvent.of(Instant.now(), currentThread.getName(), currentThread.threadId(), Level.ERROR, 1117 loggerName.getName(), message, KeyValues.of(), throwable); 1118 } 1119 1120} 1121 1122/** 1123 * Combines more than one appender on a route into one {@link LogAppender}. Each appender 1124 * keeps the independent lock it was already constructed with, and 1125 * {@link #append(LogEvent)}/{@link #append(LogEvent[], int)} skip locking at the 1126 * composite level entirely and append to every component directly - so e.g. a console 1127 * appender and a file appender under the same route never contend on the same lock for 1128 * unrelated I/O. {@link #start(LogConfig)}/{@link #close()}/{@link #act(LogAction)} do 1129 * the same - there is no composite-owned mutable state to protect, only a loop over 1130 * components that already handle their own synchronization where it matters. 1131 */ 1132@SuppressWarnings("ArrayRecordComponent") 1133record CompositeLogAppender(DirectLogAppender[] appenders) implements InternalLogAppender { 1134 1135 public static CompositeLogAppender of(List<? extends LogAppender> appenders) { 1136 @SuppressWarnings("null") // TODO Eclipse issue here 1137 DirectLogAppender @NonNull [] array = appenders.stream() 1138 .map(CompositeLogAppender::cast) 1139 .toArray(i -> new DirectLogAppender[i]); 1140 return new CompositeLogAppender(array); 1141 } 1142 1143 private static DirectLogAppender cast(LogAppender appender) { 1144 return (DirectLogAppender) appender; 1145 } 1146 1147 @Override 1148 public void append(LogEvent event) { 1149 for (var appender : appenders) { 1150 appender.append(event); 1151 } 1152 } 1153 1154 @Override 1155 public void append(LogEvent[] event, int count) { 1156 for (var appender : appenders) { 1157 appender.append(event, count); 1158 } 1159 } 1160 1161 @Override 1162 public void close() { 1163 for (var appender : appenders) { 1164 appender.close(); 1165 } 1166 } 1167 1168 @Override 1169 public void start(LogConfig config) { 1170 for (var appender : appenders) { 1171 appender.start(config); 1172 } 1173 } 1174 1175 @Override 1176 public List<LogEvent> act(LogAction action) { 1177 return Actor.act(appenders, action); 1178 } 1179 1180 @Override 1181 public String toString() { 1182 return getClass().getName() + "[appenders=" + Arrays.toString(appenders) + "]"; 1183 } 1184 1185} 1186 1187sealed abstract class LockLogAppender extends AbstractLogAppender implements InternalLogAppender { 1188 1189 protected final ReentrantLock lock; 1190 1191 public LockLogAppender(String name, LogOutput output, LogEncoder encoder, Set<LogAppender.AppenderFlag> flags, 1192 ReentrantLock lock, LogAlerts alerts, LogMetrics metrics) { 1193 super(name, output, encoder, flags, alerts, metrics); 1194 this.lock = lock; 1195 } 1196 1197 @Override 1198 public List<LogEvent> act(LogAction action) { 1199 lock.lock(); 1200 try { 1201 return _request(action); 1202 } 1203 finally { 1204 lock.unlock(); 1205 } 1206 } 1207 1208 @Override 1209 public void close() { 1210 lock.lock(); 1211 try { 1212 super.close(); 1213 } 1214 finally { 1215 lock.unlock(); 1216 } 1217 } 1218 1219} 1220 1221/* 1222 * The idea here is to reuse the buffer trading lock contention for less GC. 1223 */ 1224final class ReuseBufferLogAppender extends LockLogAppender implements InternalLogAppender { 1225 1226 private final LogEncoder.Buffer buffer; 1227 1228 ReuseBufferLogAppender(String name, LogOutput output, LogEncoder encoder, Set<LogAppender.AppenderFlag> flags, 1229 ReentrantLock lock, LogAlerts alerts, LogMetrics metrics) { 1230 super(name, output, encoder, flags, lock, alerts, metrics); 1231 this.buffer = encoder.buffer(output.bufferHints()); 1232 } 1233 1234 @Override 1235 public final void append(LogEvent event) { 1236 if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, 1)) { 1237 return; 1238 } 1239 try { 1240 lock.lock(); 1241 try { 1242 buffer.clear(); 1243 encoder.encode(event, buffer); 1244 output.write(event, buffer); 1245 if (immediateFlush) { 1246 output.flush(); 1247 } 1248 } 1249 finally { 1250 lock.unlock(); 1251 } 1252 } 1253 catch (Exception e) { 1254 alerts.error(getClass(), "appender '" + name + "' failed to append event", e); 1255 metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, 1); 1256 } 1257 } 1258 1259 @Override 1260 public void append(LogEvent[] events, int count) { 1261 if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, count)) { 1262 return; 1263 } 1264 try { 1265 lock.lock(); 1266 try { 1267 output.write(events, count, encoder, buffer); 1268 if (immediateFlush) { 1269 output.flush(); 1270 } 1271 } 1272 finally { 1273 lock.unlock(); 1274 } 1275 } 1276 catch (Exception e) { 1277 alerts.error(getClass(), "appender '" + name + "' failed to append batch of " + count + " event(s)", e); 1278 metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, count); 1279 } 1280 } 1281 1282 @Override 1283 public void close() { 1284 lock.lock(); 1285 try { 1286 super.close(); 1287 buffer.close(); 1288 } 1289 finally { 1290 lock.unlock(); 1291 } 1292 } 1293 1294} 1295 1296/* 1297 * The idea here is to encode outside the lock. Instead of allocating a fresh buffer per 1298 * event or sharing (and thus serializing access to) a single buffer 1299 * (ReuseBufferLogAppender), each thread gets its own buffer that only it will ever touch, 1300 * so encoding never needs to be guarded by the lock at all - only the final write to the 1301 * output does. 1302 */ 1303final class LockThreadLocalBufferLogAppender extends LockLogAppender implements InternalLogAppender { 1304 1305 /* 1306 * There is no way to enumerate every thread's buffer to close it on appender close so 1307 * we rely on Buffer implementations not holding onto real resources (today they are 1308 * all just wrapped in-memory builders) and let the ThreadLocal itself (and 1309 * consequently the per-thread entries) become collectible once this appender is 1310 * discarded. 1311 */ 1312 // CheckerFramework's ThreadLocal stub declares T as inherently @Nullable since get() 1313 // can return null before initialValue() runs, but withInitial(...) below guarantees 1314 // it never does here. 1315 @SuppressWarnings("nullness:type.argument") 1316 private final ThreadLocal<LogEncoder.Buffer> bufferThreadLocal; 1317 1318 LockThreadLocalBufferLogAppender(String name, LogOutput output, LogEncoder encoder, 1319 Set<LogAppender.AppenderFlag> flags, ReentrantLock lock, LogAlerts alerts, LogMetrics metrics) { 1320 super(name, output, encoder, flags, lock, alerts, metrics); 1321 this.bufferThreadLocal = ThreadLocal.withInitial(() -> encoder.buffer(output.bufferHints())); 1322 } 1323 1324 @Override 1325 public final void append(LogEvent event) { 1326 try { 1327 var buffer = bufferThreadLocal.get(); 1328 buffer.clear(); 1329 encoder.encode(event, buffer); 1330 writeLocked(event, buffer); 1331 } 1332 catch (Exception e) { 1333 alerts.error(getClass(), "appender '" + name + "' failed to append event", e); 1334 metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, 1); 1335 } 1336 } 1337 1338 private void writeLocked(LogEvent event, LogEncoder.Buffer buffer) { 1339 if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, 1)) { 1340 return; 1341 } 1342 lock.lock(); 1343 try { 1344 output.write(event, buffer); 1345 if (immediateFlush) { 1346 output.flush(); 1347 } 1348 } 1349 finally { 1350 lock.unlock(); 1351 } 1352 } 1353 1354 @Override 1355 public void append(LogEvent[] events, int count) { 1356 if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, count)) { 1357 return; 1358 } 1359 try { 1360 lock.lock(); 1361 try { 1362 output.write(events, count, encoder, bufferThreadLocal.get()); 1363 if (immediateFlush) { 1364 output.flush(); 1365 } 1366 } 1367 finally { 1368 lock.unlock(); 1369 } 1370 } 1371 catch (Exception e) { 1372 alerts.error(getClass(), "appender '" + name + "' failed to append batch of " + count + " event(s)", e); 1373 metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, count); 1374 } 1375 } 1376 1377} 1378 1379/* 1380 * Like LockThreadLocalBufferLogAppender (encode outside the lock, lock only the final 1381 * write) except the buffer is a fresh allocation per event instead of a ThreadLocal - for 1382 * deployments that want a hard guarantee of no ThreadLocal anywhere in the logging path 1383 * without paying ReuseBufferLogAppender's lock-across-the-whole-encode cost. 1384 */ 1385final class LockNewBufferLogAppender extends LockLogAppender implements InternalLogAppender { 1386 1387 LockNewBufferLogAppender(String name, LogOutput output, LogEncoder encoder, Set<LogAppender.AppenderFlag> flags, 1388 ReentrantLock lock, LogAlerts alerts, LogMetrics metrics) { 1389 super(name, output, encoder, flags, lock, alerts, metrics); 1390 } 1391 1392 @Override 1393 public final void append(LogEvent event) { 1394 try { 1395 var buffer = encoder.buffer(output.bufferHints()); 1396 encoder.encode(event, buffer); 1397 writeLocked(event, buffer); 1398 } 1399 catch (Exception e) { 1400 alerts.error(getClass(), "appender '" + name + "' failed to append event", e); 1401 metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, 1); 1402 } 1403 } 1404 1405 private void writeLocked(LogEvent event, LogEncoder.Buffer buffer) { 1406 if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, 1)) { 1407 return; 1408 } 1409 lock.lock(); 1410 try { 1411 output.write(event, buffer); 1412 if (immediateFlush) { 1413 output.flush(); 1414 } 1415 } 1416 finally { 1417 lock.unlock(); 1418 } 1419 } 1420 1421 @Override 1422 public void append(LogEvent[] events, int count) { 1423 if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags, metrics, count)) { 1424 return; 1425 } 1426 try { 1427 var buffer = encoder.buffer(output.bufferHints()); 1428 lock.lock(); 1429 try { 1430 output.write(events, count, encoder, buffer); 1431 if (immediateFlush) { 1432 output.flush(); 1433 } 1434 } 1435 finally { 1436 lock.unlock(); 1437 } 1438 } 1439 catch (Exception e) { 1440 alerts.error(getClass(), "appender '" + name + "' failed to append batch of " + count + " event(s)", e); 1441 metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, count); 1442 } 1443 } 1444 1445} 1446 1447/* 1448 * Like LockThreadLocalBufferLogAppender (per-thread reused buffer, encode outside any 1449 * lock) but the final write is protected by a plain `synchronized` block on this 1450 * appender's own monitor instead of a ReentrantLock. Does not extend LockLogAppender - 1451 * there is no way to acquire a monitor in one method call and release it in another, so 1452 * this appender's critical sections are written as literal synchronized blocks instead. 1453 */ 1454final class SynchronizedThreadLocalBufferLogAppender extends AbstractLogAppender implements InternalLogAppender { 1455 1456 private final Object monitor = new Object(); 1457 1458 // See LockThreadLocalBufferLogAppender's identical field for why this suppression is 1459 // needed. 1460 @SuppressWarnings("nullness:type.argument") 1461 private final ThreadLocal<LogEncoder.Buffer> bufferThreadLocal; 1462 1463 SynchronizedThreadLocalBufferLogAppender(String name, LogOutput output, LogEncoder encoder, 1464 Set<LogAppender.AppenderFlag> flags, LogAlerts alerts, LogMetrics metrics) { 1465 super(name, output, encoder, flags, alerts, metrics); 1466 this.bufferThreadLocal = ThreadLocal.withInitial(() -> encoder.buffer(output.bufferHints())); 1467 } 1468 1469 @Override 1470 public void append(LogEvent event) { 1471 try { 1472 var buffer = bufferThreadLocal.get(); 1473 buffer.clear(); 1474 encoder.encode(event, buffer); 1475 writeSynchronized(event, buffer); 1476 } 1477 catch (Exception e) { 1478 alerts.error(getClass(), "appender '" + name + "' failed to append event", e); 1479 metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, 1); 1480 } 1481 } 1482 1483 private void writeSynchronized(LogEvent event, LogEncoder.Buffer buffer) { 1484 if (shouldDropForReentry(Thread.holdsLock(monitor), flags, metrics, 1)) { 1485 return; 1486 } 1487 synchronized (monitor) { 1488 output.write(event, buffer); 1489 if (immediateFlush) { 1490 output.flush(); 1491 } 1492 } 1493 } 1494 1495 @Override 1496 public void append(LogEvent[] events, int count) { 1497 if (shouldDropForReentry(Thread.holdsLock(monitor), flags, metrics, count)) { 1498 return; 1499 } 1500 try { 1501 synchronized (monitor) { 1502 output.write(events, count, encoder, bufferThreadLocal.get()); 1503 if (immediateFlush) { 1504 output.flush(); 1505 } 1506 } 1507 } 1508 catch (Exception e) { 1509 alerts.error(getClass(), "appender '" + name + "' failed to append batch of " + count + " event(s)", e); 1510 metrics.errorCounter(LogMetrics.EVENTS_FAILED_METRIC, count); 1511 } 1512 } 1513 1514 @Override 1515 public void close() { 1516 synchronized (monitor) { 1517 super.close(); 1518 } 1519 } 1520 1521 @Override 1522 public List<LogEvent> act(LogAction action) { 1523 synchronized (monitor) { 1524 return _request(action); 1525 } 1526 } 1527 1528}