001package io.jstach.rainbowgum; 002 003import java.io.UncheckedIOException; 004import java.util.ArrayList; 005import java.util.Arrays; 006import java.util.Collection; 007import java.util.EnumSet; 008import java.util.List; 009import java.util.Locale; 010import java.util.Objects; 011import java.util.Set; 012import java.util.concurrent.atomic.AtomicBoolean; 013import java.util.concurrent.locks.ReentrantLock; 014 015import org.eclipse.jdt.annotation.NonNull; 016import org.eclipse.jdt.annotation.Nullable; 017 018import io.jstach.rainbowgum.LogResponse.Status; 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, LogEventConsumer { 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 * Batch of events. <strong>DO NOT MODIFY THE ARRAY</strong>. Do not use the 062 * <code>length</code> of the passed in array but instead use <code>count</code> 063 * parameter. 064 * @param events an array guaranteed to be smaller than count. 065 * @param count the number of items. 066 */ 067 public void append(LogEvent[] events, int count); 068 069 @Override 070 public void append(LogEvent event); 071 072 /** 073 * Boolean like flags for appender that can be set with 074 * {@link LogAppender#APPENDER_FLAGS_PROPERTY}. Publisher may choose to add flags to 075 * the appenders and will be added if no flags are set on the appenders. Consequently 076 * great care should be taken when setting flags as performance maybe greatly impacted 077 * if a publisher is not designed for the flag. 078 */ 079 @CaseChanging 080 public enum AppenderFlag { 081 082 /** 083 * The appender will create a single buffer that will be reused and will be 084 * protected by the appenders locking. 085 */ 086 REUSE_BUFFER, 087 /** 088 * The appender will give each thread its own reusable buffer (a 089 * {@link ThreadLocal}) instead of allocating a new buffer per event. Unlike 090 * {@link #REUSE_BUFFER} the encoding is done <strong>outside</strong> the 091 * appender's lock (the thread's buffer is only visited by that thread so no 092 * protection is needed while encoding) with the lock only held for the final 093 * write to the output, which is the same trade-off {@link #REUSE_BUFFER} makes 094 * except without serializing the encoding step itself. 095 * <p> 096 * This flag is ignored if {@link #REUSE_BUFFER} is also set. 097 * <p> 098 * The same {@link ThreadLocal} buffer is used regardless of whether the calling 099 * thread is a platform or virtual thread. A virtual thread's entry becomes 100 * collectible once the thread itself terminates, and a typical unit of work (e.g. 101 * one HTTP request) logs several times on the same thread, so reusing the buffer 102 * across those calls still pays off even for short-lived virtual threads. 103 * <p> 104 * This is the strategy an appender uses <strong>even when no flag is explicitly 105 * set</strong> - see {@code DirectLogAppender#defaultAppender} for the default 106 * selection, and {@link #SYNCHRONIZED_THREAD_LOCAL_BUFFER} for the alternative 107 * lock-kind opt-in. 108 */ 109 LOCK_THREAD_LOCAL_BUFFER, 110 /** 111 * Like {@link #LOCK_THREAD_LOCAL_BUFFER} (a reused per-thread buffer, encoding 112 * done outside any lock) except the final write to the output is protected by a 113 * plain {@code synchronized} block (the JVM's intrinsic monitor) instead of a 114 * {@link ReentrantLock}. 115 * <p> 116 * The Java language has no way to acquire a monitor in one method call and 117 * release it in another, so this appender's critical sections are written as 118 * literal {@code synchronized} blocks rather than going through a shared lock 119 * abstraction the way every other flag combination does. {@link #REENTRY_DROP} 120 * and {@link #REENTRY_LOG} are still honored though - 121 * {@link Thread#holdsLock(Object)} is the {@code synchronized} equivalent of 122 * {@code ReentrantLock}'s {@code isHeldByCurrentThread()}, so reentrancy is 123 * detected the same way. 124 * <p> 125 * Motivated by Log4j2's own garbage-free appenders using {@code synchronized} 126 * rather than a {@code java.util.concurrent} lock around their buffer-transfer 127 * step, and confirmed by real-workload benchmarking to outperform 128 * {@link ReentrantLock} under platform-thread contention - this was the default 129 * for a time. Real-workload benchmarking under virtual threads found the 130 * opposite, a large and reproducible loss versus {@link ReentrantLock} for 131 * reasons not fully understood (classic JEP 491 pinning was checked and ruled 132 * out), so {@link #LOCK_THREAD_LOCAL_BUFFER} is the default now and this flag is 133 * an opt-in for platform-thread-heavy deployments that want the edge. Takes 134 * precedence over {@link #LOCK_THREAD_LOCAL_BUFFER} (redundant if both are set) 135 * but not {@link #REUSE_BUFFER}. 136 * <p> 137 * <strong>Explicitly setting this flag honors it</strong> even on a JDK where 138 * {@code synchronized} still pins the carrier platform thread when called from a 139 * virtual thread (before <a href="https://openjdk.org/jeps/491">JEP 491</a>, 140 * finalized in JDK 24) - the one exception is 141 * {@code LogProperties#GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY}: when that global 142 * property is active it downgrades even an explicit request for this flag to 143 * {@link #LOCK_THREAD_LOCAL_BUFFER}, since its whole point is a hard guarantee 144 * independent of anything else in the configuration. 145 */ 146 SYNCHRONIZED_THREAD_LOCAL_BUFFER, 147 /** 148 * By default the appender will call flush on each item appended or if in async 149 * batch mode for each batch. This flag disables that behavior so that flushing is 150 * left up to the output (or an external mechanism) instead. 151 */ 152 DISABLE_IMMEDIATE_FLUSH, 153 /** 154 * The appender will drop events on reentry which happens if an appender during 155 * its append causes recursive appending in the same thread. This is an analog to 156 * what 157 * <a href="https://logback.qos.ch/manual/appenders.html#AppenderBase">Logback 158 * does by default</a>. Note that this is done using {@link ReentrantLock} and not 159 * ThreadLocal like logback <strong>and is not done by default hence the 160 * flag!</strong> 161 * <p> 162 * This flag is to allow outputs that do logging themselves. For performance 163 * reasons and to allow async publishers it is recommended that you fix the output 164 * code such that it does not do logging. This flag is ignored if 165 * {@link #REENTRY_LOG} is set. 166 * <p> 167 * <strong>This flag will not fix outputs causing lool like logging if an async 168 * publisher is used!</strong> That is why it is recommended you fix the output by 169 * dropping events that would cause infinite loop like logging. 170 * @see #REENTRY_LOG 171 */ 172 REENTRY_DROP, 173 /** 174 * The appender will log events as errors to std error on reentry which happens if 175 * an appender during its append causes recursive appending in the same thread. 176 * This is an analog to what 177 * <a href="https://logback.qos.ch/manual/appenders.html#AppenderBase">Logback 178 * does by default</a>. Note that this is done using {@link ReentrantLock} and not 179 * ThreadLocal like logback <strong>and is not done by default hence the 180 * flag!</strong> This flag is to resolve failures of outputs that then do 181 * logging. 182 * <p> 183 * This flag takes precedence over {@link #REENTRY_DROP}. 184 */ 185 REENTRY_LOG; 186 187 static Set<AppenderFlag> parse(Collection<String> value) { 188 if (value.isEmpty()) { 189 return EnumSet.noneOf(AppenderFlag.class); 190 } 191 var s = EnumSet.noneOf(AppenderFlag.class); 192 for (var v : value) { 193 s.add(parse(v)); 194 } 195 return s; 196 } 197 198 static AppenderFlag parse(String value) { 199 String v = value.toUpperCase(Locale.ROOT); 200 return AppenderFlag.valueOf(v); 201 } 202 203 } 204 205 /** 206 * Creates a builder. 207 * @param name appender name. 208 * @return builder. 209 */ 210 public static Builder builder(String name) { 211 return new Builder(name); 212 } 213 214 /** 215 * Builder for creating standard appenders. 216 * <p> 217 * If the output is not set standard out will be used. If the encoder is not set a 218 * default encoder will be resolved from the output. 219 */ 220 public static final class Builder { 221 222 private @Nullable LogProvider<? extends LogOutput> output = null; 223 224 private @Nullable LogProvider<? extends LogEncoder> encoder = null; 225 226 private @Nullable EnumSet<AppenderFlag> flags = null; 227 228 private final String name; 229 230 private Builder(String name) { 231 this.name = name; 232 } 233 234 /** 235 * Name of the appender. 236 * @return name. 237 */ 238 public String name() { 239 return this.name; 240 } 241 242 /** 243 * Sets output. 244 * @param output output. 245 * @return builder. 246 */ 247 public Builder output(LogProvider<? extends LogOutput> output) { 248 this.output = output; 249 return this; 250 } 251 252 /** 253 * Sets output. 254 * @param output output. 255 * @return builder. 256 */ 257 public Builder output(LogOutput output) { 258 this.output = LogProvider.of(output); 259 return this; 260 } 261 262 /** 263 * Sets formatter as encoder. 264 * @param formatter formatter to be converted to encoder. 265 * @return builder. 266 * @see LogEncoder#of(LogFormatter) 267 */ 268 public Builder formatter(LogFormatter formatter) { 269 this.encoder = LogProvider.of(LogEncoder.of(formatter)); 270 return this; 271 } 272 273 /** 274 * Sets formatter as encoder. 275 * @param formatter formatter to be converted to encoder. 276 * @return builder. 277 * @see LogEncoder#of(LogFormatter) 278 */ 279 public Builder formatter(LogFormatter.EventFormatter formatter) { 280 this.encoder = LogProvider.of(LogEncoder.of(formatter)); 281 return this; 282 } 283 284 /** 285 * Sets encoder. 286 * @param encoder encoder not <code>null</code>. 287 * @return builder. 288 */ 289 public Builder encoder(LogProvider<? extends LogEncoder> encoder) { 290 this.encoder = encoder; 291 return this; 292 } 293 294 /** 295 * Sets encoder. 296 * @param encoder encoder not <code>null</code>. 297 * @return builder. 298 */ 299 public Builder encoder(LogEncoder encoder) { 300 this.encoder = LogProvider.of(encoder); 301 return this; 302 } 303 304 /** 305 * Sets appender flags. 306 * @param flags flags will replace all flags currently set. 307 * @return this. 308 */ 309 public Builder flags(Collection<AppenderFlag> flags) { 310 _flags().addAll(flags); 311 return this; 312 } 313 314 private EnumSet<AppenderFlag> _flags() { 315 EnumSet<AppenderFlag> flags = this.flags; 316 if (flags == null) { 317 this.flags = flags = EnumSet.noneOf(AppenderFlag.class); 318 } 319 return flags; 320 } 321 322 /** 323 * Adds a flag. 324 * @param flag flag. 325 * @return this. 326 */ 327 public Builder flag(AppenderFlag flag) { 328 _flags().add(flag); 329 return this; 330 } 331 332 /** 333 * Builds. 334 * @return an appender factory. 335 */ 336 public LogProvider<LogAppender> build() { 337 /* 338 * We need to capture parameters since appender creation needs to be lazy. 339 */ 340 var _name = name; 341 var _output = output; 342 var _encoder = encoder; 343 var _flags = flags; 344 /* 345 * TODO should we use the parent name for resolution? 346 */ 347 return (n, config) -> { 348 AppenderConfig a = new AppenderConfig(_name, LogProvider.provideOrNull(_output, _name, config), 349 LogProvider.provideOrNull(_encoder, _name, config), _flags); 350 return DefaultAppenderRegistry.appender(a, config); 351 }; 352 } 353 354 } 355 356 /** 357 * Provides appenders safely to the publisher. The providing calls of 358 * <code>asXXX</code> can only be called once as they register the appenders. 359 */ 360 class Appenders { 361 362 private final AtomicBoolean created = new AtomicBoolean(); 363 364 private final String name; 365 366 private final LogConfig config; 367 368 private final List<LogProvider<LogAppender>> appenders; 369 370 private Set<LogAppender.AppenderFlag> flags = EnumSet.noneOf(LogAppender.AppenderFlag.class); 371 372 Appenders(String name, LogConfig config, List<LogProvider<LogAppender>> appenders) { 373 super(); 374 this.name = name; 375 this.config = config; 376 this.appenders = appenders; 377 } 378 379 /** 380 * Sets flags for the appenders which should be done prior to <code>asXXX</code>. 381 * @param flags appender flags. 382 * @return this; 383 */ 384 public Appenders flags(Set<LogAppender.AppenderFlag> flags) { 385 this.flags = flags; 386 return this; 387 } 388 389 /** 390 * Return the appenders as a list. 391 * @return list of appenders. 392 * @throws IllegalStateException if appenders are already registered. 393 */ 394 public List<? extends LogAppender> asList() throws IllegalStateException { 395 if (created.compareAndSet(false, true)) { 396 var apps = appenders(); 397 List<LogAppender> appenders = new ArrayList<>(); 398 for (var a : apps) { 399 appenders.add(register(a)); 400 } 401 return appenders; 402 } 403 else { 404 throw new IllegalStateException("Appenders already provided."); 405 } 406 407 } 408 409 /** 410 * Consolidate the appenders as a single appender, appended synchronously. If more 411 * than one appender is combined, each keeps its own independent lock and is 412 * appended to directly - see {@link CompositeLogAppender}. 413 * @return single appender. 414 * @throws IllegalStateException if appenders are already registered. 415 */ 416 public LogAppender asSingle() throws IllegalStateException { 417 if (created.compareAndSet(false, true)) { 418 var apps = appenders(); 419 var appender = composite(apps); 420 return register(appender); 421 } 422 else { 423 throw new IllegalStateException("Appenders already provided."); 424 } 425 } 426 427 private LogAppender register(LogAppender appender) { 428 return switch (appender) { 429 case DirectLogAppender ia -> { 430 var _a = ia.withFlags(flags); 431 config.serviceRegistry().put(LogAppender.class, name + "." + _a.name(), _a); 432 yield _a; 433 } 434 case CompositeLogAppender ca -> { 435 var _a = ca.withFlags(flags); 436 config.serviceRegistry().put(LogAppender.class, name, _a); 437 yield _a; 438 } 439 default -> { 440 throw new IllegalStateException(); 441 } 442 }; 443 } 444 445 private List<LogAppender> appenders() { 446 return LogProvider.flatten(appenders) 447 .describe(n -> "Appenders for route: '" + n + "'") 448 .provide(name, config); 449 } 450 451 /** 452 * Creates a composite log appender from many, each keeping its own independent 453 * lock. 454 * @param appenders appenders. 455 * @return appender. 456 */ 457 private static LogAppender composite(List<? extends LogAppender> appenders) { 458 if (appenders.isEmpty()) { 459 throw new IllegalArgumentException("A single appender is required"); 460 } 461 if (appenders.size() == 1) { 462 return Objects.requireNonNull(appenders.get(0)); 463 } 464 return CompositeLogAppender.of(appenders, Set.of()); 465 } 466 467 } 468 469 @Override 470 public void close(); 471 472} 473 474interface AppenderVisitor { 475 476 boolean consume(DirectLogAppender appender); 477 478} 479 480/** 481 * This is a JAVADOC BUG 482 */ 483sealed interface InternalLogAppender extends LogAppender, Actor { 484 485 static InternalLogAppender of(LogAppender appender) { 486 return Objects.requireNonNull((InternalLogAppender) appender); // TODO eclipse 487 // bug. 488 } 489 490 /** 491 * An appender can act on actions. One of the key actions is reopening files. 492 * @param action action to run. 493 * @return responses. 494 */ 495 @Override 496 public List<LogResponse> act(LogAction action); 497 498} 499 500sealed interface DirectLogAppender extends InternalLogAppender { 501 502 String name(); 503 504 LogOutput output(); 505 506 LogEncoder encoder(); 507 508 default List<LogResponse> _request(LogAction action) { 509 List<LogResponse> r = switch (action) { 510 case LogAction.StandardAction a -> switch (a) { 511 case LogAction.StandardAction.REOPEN -> List.of(reopen()); 512 case LogAction.StandardAction.FLUSH -> List.of(flush()); 513 }; 514 }; 515 return r; 516 } 517 518 default LogResponse reopen() { 519 var status = output().reopen(); 520 return new Response(LogOutput.class, name(), status); 521 } 522 523 default LogResponse flush() { 524 output().flush(); 525 return new Response(LogOutput.class, name(), LogResponse.Status.StandardStatus.OK); 526 } 527 528 static DirectLogAppender of(String name, LogOutput output, LogEncoder encoder, 529 Set<LogAppender.AppenderFlag> flags) { 530 flags = AbstractLogAppender.guardSynchronizedFlag(flags); 531 if (flags.contains(AppenderFlag.REUSE_BUFFER)) { 532 return new ReuseBufferLogAppender(name, output, encoder, flags, new ReentrantLock()); 533 } 534 if (flags.contains(AppenderFlag.SYNCHRONIZED_THREAD_LOCAL_BUFFER)) { 535 return new SynchronizedThreadLocalBufferLogAppender(name, output, encoder, flags); 536 } 537 if (flags.contains(AppenderFlag.LOCK_THREAD_LOCAL_BUFFER)) { 538 return new LockThreadLocalBufferLogAppender(name, output, encoder, flags, new ReentrantLock()); 539 } 540 return defaultAppender(name, output, encoder, flags); 541 } 542 543 /** 544 * Picks the appender used when no {@link AppenderFlag} explicitly requests a 545 * buffer/lock strategy: {@link AppenderFlag#LOCK_THREAD_LOCAL_BUFFER}, the same 546 * appender a caller gets from setting that flag explicitly. Supports 547 * {@link AppenderFlag#REENTRY_DROP}/{@link AppenderFlag#REENTRY_LOG} directly (see 548 * {@link AbstractLogAppender#shouldDropForReentry}), so no fallback to a third 549 * appender is needed here for those flags. 550 * <p> 551 * {@link AppenderFlag#SYNCHRONIZED_THREAD_LOCAL_BUFFER} measured faster under 552 * platform-thread contention in real-workload benchmarking and was the default for a 553 * time, but real-workload benchmarking under virtual threads found the opposite - a 554 * large, reproducible win for {@code LOCK_THREAD_LOCAL_BUFFER} there, for reasons not 555 * fully understood (checked and ruled out classic JEP 491 pinning as the cause). 556 * Given RainbowGum's own audience skews toward newer JDKs and virtual-thread 557 * workloads, {@code LOCK_THREAD_LOCAL_BUFFER} is the safer default; 558 * {@code synchronized} remains available as an explicit opt-in for 559 * platform-thread-heavy deployments that want that edge. 560 */ 561 static DirectLogAppender defaultAppender(String name, LogOutput output, LogEncoder encoder, 562 Set<LogAppender.AppenderFlag> flags) { 563 return new LockThreadLocalBufferLogAppender(name, output, encoder, flags, new ReentrantLock()); 564 } 565 566 // @Override 567 DirectLogAppender withFlags(Set<LogAppender.AppenderFlag> flags); 568 569} 570 571/** 572 * An abstract appender to help create custom appenders. 573 */ 574sealed abstract class AbstractLogAppender implements DirectLogAppender { 575 576 /* 577 * Set once from LogProperties#GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY during 578 * LogConfig construction (see DefaultLogConfig) - a global, process-wide guarantee 579 * that no appender will ever use `synchronized`, for deployments that want that 580 * guaranteed even when something explicitly requests 581 * SYNCHRONIZED_THREAD_LOCAL_BUFFER. Global (not per-route/per-appender) by design, 582 * matching the property's own scope. 583 */ 584 static volatile boolean forceReentrantLockAppenders = false; 585 586 /** 587 * Downgrades an explicit 588 * {@link LogAppender.AppenderFlag#SYNCHRONIZED_THREAD_LOCAL_BUFFER} to 589 * {@link LogAppender.AppenderFlag#LOCK_THREAD_LOCAL_BUFFER} if 590 * {@link #forceReentrantLockAppenders} is active - the enforcement point that makes 591 * the global no-synchronized guarantee a real guarantee rather than just a changed 592 * default, since an explicit flag would otherwise bypass 593 * {@link DirectLogAppender#defaultAppender} entirely. 594 * @param flags flags as given to an appender factory method. 595 * @return {@code flags} unchanged, unless the guarantee is active and 596 * {@code SYNCHRONIZED_THREAD_LOCAL_BUFFER} was requested, in which case a copy with 597 * that flag replaced by {@code LOCK_THREAD_LOCAL_BUFFER}. 598 */ 599 static Set<LogAppender.AppenderFlag> guardSynchronizedFlag(Set<LogAppender.AppenderFlag> flags) { 600 if (!forceReentrantLockAppenders 601 || !flags.contains(LogAppender.AppenderFlag.SYNCHRONIZED_THREAD_LOCAL_BUFFER)) { 602 return flags; 603 } 604 var copy = EnumSet.copyOf(flags); 605 copy.remove(LogAppender.AppenderFlag.SYNCHRONIZED_THREAD_LOCAL_BUFFER); 606 copy.add(LogAppender.AppenderFlag.LOCK_THREAD_LOCAL_BUFFER); 607 return copy; 608 } 609 610 /** 611 * Whether an appender should drop (or drop-and-log) an append call because it is 612 * reentrant - i.e. the current thread is already inside a previous call to the same 613 * appender's write path, which happens if an output does logging itself during its 614 * own write. Shared by every appender that can detect reentrancy, regardless of 615 * whether it does so via a {@link ReentrantLock} ( 616 * {@code lock.isHeldByCurrentThread()}) or a {@code synchronized} block ( 617 * {@link Thread#holdsLock(Object)}) - callers pass in whichever check applies to 618 * them. 619 * @param reentrant whether the current thread already holds this appender's 620 * lock/monitor. 621 * @param flags the appender's flags. 622 * @return {@code true} if the caller should drop the event without appending. 623 */ 624 static boolean shouldDropForReentry(boolean reentrant, Set<LogAppender.AppenderFlag> flags) { 625 if (!reentrant) { 626 return false; 627 } 628 if (flags.contains(LogAppender.AppenderFlag.REENTRY_LOG)) { 629 Exception exception = new Exception("reentrant appender"); 630 MetaLog.error(LogAppender.class, exception); 631 return true; 632 } 633 return flags.contains(LogAppender.AppenderFlag.REENTRY_DROP); 634 } 635 636 /** 637 * name. 638 */ 639 protected final String name; 640 641 /** 642 * output 643 */ 644 protected final LogOutput output; 645 646 /** 647 * encoder 648 */ 649 protected final LogEncoder encoder; 650 651 protected final Set<LogAppender.AppenderFlag> flags; 652 653 protected final boolean immediateFlush; 654 655 /** 656 * Creates an appender from an output and encoder. 657 * @param output set the output field and will be started and closed with the 658 * appender. 659 * @param encoder set the encoder field. 660 */ 661 protected AbstractLogAppender(String name, LogOutput output, LogEncoder encoder, 662 Set<LogAppender.AppenderFlag> flags) { 663 super(); 664 this.name = name; 665 this.output = output; 666 this.encoder = encoder; 667 this.flags = flags; 668 this.immediateFlush = !flags.contains(LogAppender.AppenderFlag.DISABLE_IMMEDIATE_FLUSH); 669 } 670 671 @Override 672 public void start(LogConfig config) { 673 output.start(config); 674 } 675 676 @Override 677 public void close() { 678 output.close(); 679 } 680 681 @Override 682 public String toString() { 683 return getClass().getName() + "[name=" + name + " encoder=" + encoder + ", " + "output=" + output + ", flags=" 684 + flags + "]"; 685 } 686 687 @Override 688 public String name() { 689 return this.name; 690 } 691 692 @Override 693 public LogOutput output() { 694 return this.output; 695 } 696 697 @Override 698 public LogEncoder encoder() { 699 return this.encoder; 700 } 701 702} 703 704/** 705 * Combines more than one appender on a route into one {@link LogAppender}. Each appender 706 * keeps the independent lock it was already constructed with, and 707 * {@link #append(LogEvent)}/{@link #append(LogEvent[], int)} skip locking at the 708 * composite level entirely and append to every component directly - so e.g. a console 709 * appender and a file appender under the same route never contend on the same lock for 710 * unrelated I/O. {@link #start(LogConfig)}/{@link #close()}/{@link #act(LogAction)} do 711 * the same - there is no composite-owned mutable state to protect, only a loop over 712 * components that already handle their own synchronization where it matters. 713 */ 714@SuppressWarnings("ArrayRecordComponent") 715record CompositeLogAppender(DirectLogAppender[] appenders) implements InternalLogAppender { 716 717 public static CompositeLogAppender of(List<? extends LogAppender> appenders, Set<LogAppender.AppenderFlag> flags) { 718 @SuppressWarnings("null") // TODO Eclipse issue here 719 DirectLogAppender @NonNull [] array = appenders.stream() 720 .map(CompositeLogAppender::cast) 721 .map(a -> a.withFlags(flags)) 722 .toArray(i -> new DirectLogAppender[i]); 723 return new CompositeLogAppender(array); 724 } 725 726 private static DirectLogAppender cast(LogAppender appender) { 727 return (DirectLogAppender) appender; 728 } 729 730 @Override 731 public void append(LogEvent event) { 732 for (var appender : appenders) { 733 appender.append(event); 734 } 735 } 736 737 @Override 738 public void append(LogEvent[] event, int count) { 739 for (var appender : appenders) { 740 appender.append(event, count); 741 } 742 } 743 744 @Override 745 public void close() { 746 for (var appender : appenders) { 747 appender.close(); 748 } 749 } 750 751 @Override 752 public void start(LogConfig config) { 753 for (var appender : appenders) { 754 appender.start(config); 755 } 756 } 757 758 @Override 759 public List<LogResponse> act(LogAction action) { 760 return Actor.act(appenders, action); 761 } 762 763 public CompositeLogAppender withFlags(Set<LogAppender.AppenderFlag> flags) { 764 if (flags.isEmpty()) { 765 return this; 766 } 767 return of(List.of(appenders), flags); 768 } 769 770 @Override 771 public String toString() { 772 return getClass().getName() + "[appenders=" + Arrays.toString(appenders) + "]"; 773 } 774 775} 776 777sealed abstract class LockLogAppender extends AbstractLogAppender implements InternalLogAppender { 778 779 protected final ReentrantLock lock; 780 781 public LockLogAppender(String name, LogOutput output, LogEncoder encoder, Set<LogAppender.AppenderFlag> flags, 782 ReentrantLock lock) { 783 super(name, output, encoder, flags); 784 this.lock = lock; 785 } 786 787 @Override 788 public List<LogResponse> act(LogAction action) { 789 lock.lock(); 790 try { 791 return _request(action); 792 } 793 catch (UncheckedIOException ioe) { 794 return List.of(new Response(LogOutput.class, name, Status.ErrorStatus.of(ioe))); 795 } 796 finally { 797 lock.unlock(); 798 } 799 } 800 801 @Override 802 public void close() { 803 lock.lock(); 804 try { 805 super.close(); 806 } 807 finally { 808 lock.unlock(); 809 } 810 } 811 812 @Override 813 public DirectLogAppender withFlags(Set<LogAppender.AppenderFlag> flags) { 814 if (flags.isEmpty()) { 815 return this; 816 } 817 if (this.flags.containsAll(flags)) { 818 return this; 819 } 820 flags = EnumSet.copyOf(flags); 821 flags.addAll(this.flags); 822 flags = guardSynchronizedFlag(flags); 823 if (flags.contains(LogAppender.AppenderFlag.REUSE_BUFFER)) { 824 return new ReuseBufferLogAppender(name, output, encoder, flags, lock); 825 } 826 if (flags.contains(LogAppender.AppenderFlag.SYNCHRONIZED_THREAD_LOCAL_BUFFER)) { 827 return new SynchronizedThreadLocalBufferLogAppender(name, output, encoder, flags); 828 } 829 if (flags.contains(LogAppender.AppenderFlag.LOCK_THREAD_LOCAL_BUFFER)) { 830 return new LockThreadLocalBufferLogAppender(name, output, encoder, flags, lock); 831 } 832 return DirectLogAppender.defaultAppender(name, output, encoder, flags); 833 } 834 835} 836 837/* 838 * The idea here is to reuse the buffer trading lock contention for less GC. 839 */ 840final class ReuseBufferLogAppender extends LockLogAppender implements InternalLogAppender { 841 842 private final LogEncoder.Buffer buffer; 843 844 ReuseBufferLogAppender(String name, LogOutput output, LogEncoder encoder, Set<LogAppender.AppenderFlag> flags, 845 ReentrantLock lock) { 846 super(name, output, encoder, flags, lock); 847 this.buffer = encoder.buffer(output.bufferHints()); 848 } 849 850 @Override 851 public final void append(LogEvent event) { 852 if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags)) { 853 return; 854 } 855 lock.lock(); 856 try { 857 buffer.clear(); 858 encoder.encode(event, buffer); 859 output.write(event, buffer); 860 if (immediateFlush) { 861 output.flush(); 862 } 863 } 864 finally { 865 lock.unlock(); 866 } 867 } 868 869 @Override 870 public void append(LogEvent[] events, int count) { 871 if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags)) { 872 return; 873 } 874 lock.lock(); 875 try { 876 output.write(events, count, encoder, buffer); 877 if (immediateFlush) { 878 output.flush(); 879 } 880 } 881 finally { 882 lock.unlock(); 883 } 884 } 885 886 @Override 887 public void close() { 888 lock.lock(); 889 try { 890 super.close(); 891 buffer.close(); 892 } 893 finally { 894 lock.unlock(); 895 } 896 } 897 898} 899 900/* 901 * The idea here is to encode outside the lock. Instead of allocating a fresh buffer per 902 * event or sharing (and thus serializing access to) a single buffer 903 * (ReuseBufferLogAppender), each thread gets its own buffer that only it will ever touch, 904 * so encoding never needs to be guarded by the lock at all - only the final write to the 905 * output does. 906 */ 907final class LockThreadLocalBufferLogAppender extends LockLogAppender implements InternalLogAppender { 908 909 /* 910 * There is no way to enumerate every thread's buffer to close it on appender close so 911 * we rely on Buffer implementations not holding onto real resources (today they are 912 * all just wrapped in-memory builders) and let the ThreadLocal itself (and 913 * consequently the per-thread entries) become collectible once this appender is 914 * discarded. 915 */ 916 // CheckerFramework's ThreadLocal stub declares T as inherently @Nullable since get() 917 // can return null before initialValue() runs, but withInitial(...) below guarantees 918 // it never does here. 919 @SuppressWarnings("nullness:type.argument") 920 private final ThreadLocal<LogEncoder.Buffer> bufferThreadLocal; 921 922 LockThreadLocalBufferLogAppender(String name, LogOutput output, LogEncoder encoder, 923 Set<LogAppender.AppenderFlag> flags, ReentrantLock lock) { 924 super(name, output, encoder, flags, lock); 925 this.bufferThreadLocal = ThreadLocal.withInitial(() -> encoder.buffer(output.bufferHints())); 926 } 927 928 @Override 929 public final void append(LogEvent event) { 930 var buffer = bufferThreadLocal.get(); 931 buffer.clear(); 932 encoder.encode(event, buffer); 933 writeLocked(event, buffer); 934 } 935 936 private void writeLocked(LogEvent event, LogEncoder.Buffer buffer) { 937 if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags)) { 938 return; 939 } 940 lock.lock(); 941 try { 942 output.write(event, buffer); 943 if (immediateFlush) { 944 output.flush(); 945 } 946 } 947 finally { 948 lock.unlock(); 949 } 950 } 951 952 @Override 953 public void append(LogEvent[] events, int count) { 954 if (shouldDropForReentry(lock.isHeldByCurrentThread(), flags)) { 955 return; 956 } 957 lock.lock(); 958 try { 959 output.write(events, count, encoder, bufferThreadLocal.get()); 960 if (immediateFlush) { 961 output.flush(); 962 } 963 } 964 finally { 965 lock.unlock(); 966 } 967 } 968 969} 970 971/* 972 * Like LockThreadLocalBufferLogAppender (per-thread reused buffer, encode outside any 973 * lock) but the final write is protected by a plain `synchronized` block on this 974 * appender's own monitor instead of a ReentrantLock. Does not extend LockLogAppender - 975 * there is no way to acquire a monitor in one method call and release it in another, so 976 * this appender's critical sections are written as literal synchronized blocks instead. 977 */ 978final class SynchronizedThreadLocalBufferLogAppender extends AbstractLogAppender implements InternalLogAppender { 979 980 private final Object monitor = new Object(); 981 982 // See LockThreadLocalBufferLogAppender's identical field for why this suppression is 983 // needed. 984 @SuppressWarnings("nullness:type.argument") 985 private final ThreadLocal<LogEncoder.Buffer> bufferThreadLocal; 986 987 SynchronizedThreadLocalBufferLogAppender(String name, LogOutput output, LogEncoder encoder, 988 Set<LogAppender.AppenderFlag> flags) { 989 super(name, output, encoder, flags); 990 this.bufferThreadLocal = ThreadLocal.withInitial(() -> encoder.buffer(output.bufferHints())); 991 } 992 993 @Override 994 public void append(LogEvent event) { 995 var buffer = bufferThreadLocal.get(); 996 buffer.clear(); 997 encoder.encode(event, buffer); 998 writeSynchronized(event, buffer); 999 } 1000 1001 private void writeSynchronized(LogEvent event, LogEncoder.Buffer buffer) { 1002 if (shouldDropForReentry(Thread.holdsLock(monitor), flags)) { 1003 return; 1004 } 1005 synchronized (monitor) { 1006 output.write(event, buffer); 1007 if (immediateFlush) { 1008 output.flush(); 1009 } 1010 } 1011 } 1012 1013 @Override 1014 public void append(LogEvent[] events, int count) { 1015 if (shouldDropForReentry(Thread.holdsLock(monitor), flags)) { 1016 return; 1017 } 1018 synchronized (monitor) { 1019 output.write(events, count, encoder, bufferThreadLocal.get()); 1020 if (immediateFlush) { 1021 output.flush(); 1022 } 1023 } 1024 } 1025 1026 @Override 1027 public void close() { 1028 synchronized (monitor) { 1029 super.close(); 1030 } 1031 } 1032 1033 @Override 1034 public List<LogResponse> act(LogAction action) { 1035 synchronized (monitor) { 1036 try { 1037 return _request(action); 1038 } 1039 catch (UncheckedIOException ioe) { 1040 return List.of(new Response(LogOutput.class, name, Status.ErrorStatus.of(ioe))); 1041 } 1042 } 1043 } 1044 1045 @Override 1046 public DirectLogAppender withFlags(Set<LogAppender.AppenderFlag> flags) { 1047 if (flags.isEmpty()) { 1048 return this; 1049 } 1050 if (this.flags.containsAll(flags)) { 1051 return this; 1052 } 1053 flags = EnumSet.copyOf(flags); 1054 flags.addAll(this.flags); 1055 flags = guardSynchronizedFlag(flags); 1056 if (flags.contains(LogAppender.AppenderFlag.REUSE_BUFFER)) { 1057 return new ReuseBufferLogAppender(name, output, encoder, flags, new ReentrantLock()); 1058 } 1059 if (flags.contains(LogAppender.AppenderFlag.SYNCHRONIZED_THREAD_LOCAL_BUFFER)) { 1060 return new SynchronizedThreadLocalBufferLogAppender(name, output, encoder, flags); 1061 } 1062 if (flags.contains(LogAppender.AppenderFlag.LOCK_THREAD_LOCAL_BUFFER)) { 1063 return new LockThreadLocalBufferLogAppender(name, output, encoder, flags, new ReentrantLock()); 1064 } 1065 return DirectLogAppender.defaultAppender(name, output, encoder, flags); 1066 } 1067 1068}