001package io.jstach.rainbowgum; 002 003import java.io.PrintWriter; 004import java.lang.System.Logger.Level; 005import java.net.URI; 006import java.nio.charset.StandardCharsets; 007import java.time.Instant; 008import java.time.ZoneId; 009import java.time.ZoneOffset; 010import java.time.format.DateTimeFormatter; 011import java.util.ArrayList; 012import java.util.Arrays; 013import java.util.List; 014 015import org.eclipse.jdt.annotation.Nullable; 016 017import io.jstach.rainbowgum.KeyValues.KeyValuesConsumer; 018import io.jstach.rainbowgum.LogFormatter.EventFormatter; 019import io.jstach.rainbowgum.LogFormatter.LevelFormatter; 020import io.jstach.rainbowgum.LogFormatter.ThrowableFormatter; 021import io.jstach.rainbowgum.LogFormatter.TimestampFormatter; 022 023/** 024 * Formats a log event using a {@link StringBuilder}. <strong>All formatters should be 025 * thread-safe!</strong>. 026 * <p> 027 * The appender will make sure the {@link StringBuilder} is not shared with multiple 028 * threads so the formatter does not have to synchronize/lock on and should definitely not 029 * do that. 030 * <p> 031 * Because of various invariants the preferred way to compose formatters is to use the 032 * {@linkplain #builder() builder} which will do some optimization like combining static 033 * formatters etc. 034 * 035 * @see #builder() 036 * @see LogFormatter.EventFormatter 037 * @see LogEncoder#of(LogFormatter) 038 * @apiNote This class is sealed. An interface that has the same contract that can be 039 * implemented is {@link EventFormatter}. 040 */ 041public sealed interface LogFormatter { 042 043 /** 044 * Formats a log event. 045 * @param output buffer. 046 * @param event log event. 047 * @see EventFormatter 048 */ 049 public void format(StringBuilder output, LogEvent event); 050 051 /** 052 * See {@link EventFormatter#builder()}. 053 * @return builder. 054 */ 055 public static Builder builder() { 056 return new Builder(); 057 } 058 059 /** 060 * Create a formatter using event formatter that is a lambda so that 061 * {@code LogFormater.of((o, e) -> ...); } works. 062 * @param e will be returned. 063 * @return passed in formatter. 064 * @apiNote this is for ergonomics because LogFormatter is sealed. 065 */ 066 public static EventFormatter of(EventFormatter e) { 067 return e; 068 } 069 070 /** 071 * Ask the formatter if will do anything. 072 * @return true if promises not to write to builder. 073 * @see #noop() 074 */ 075 default boolean isNoop() { 076 return NoopFormatter.INSTANCE == this; 077 } 078 079 /** 080 * A special formatter that will do nothing. It is a singleton so identity comparison 081 * can be used. 082 * @return a formatter that implements all formatting interfaces but does nothing. 083 */ 084 public static NoopFormatter noop() { 085 return NoopFormatter.INSTANCE; 086 } 087 088 /** 089 * A special formatter that will append static text. 090 * 091 * @param content static text. 092 */ 093 public record StaticFormatter(String content) implements LogFormatter { 094 @Override 095 public void format(StringBuilder output, LogEvent event) { 096 output.append(content); 097 } 098 099 /** 100 * Creates a new formatter by concat the {@link #content()}. This is mainly used 101 * by the {@link EventFormatter#builder()} to coalesce multiple static text. 102 * @param next the text that will follow this formatter. 103 * @return new formatter. 104 */ 105 public StaticFormatter concat(StaticFormatter next) { 106 return new StaticFormatter(this.content + next.content); 107 } 108 109 /** 110 * Coalesce formatters that can be such as {@link StaticFormatter}. 111 * @param formatters list of formatters in the order of which they will be 112 * executed. 113 * @return an array of formatters where static formatters next to each other will 114 * be coalesced. 115 */ 116 private static LogFormatter[] coalesce(List<? extends LogFormatter> formatters) { 117 var flattened = CompositeFormatter.flatten(formatters); 118 List<LogFormatter> resolved = new ArrayList<>(); 119 StaticFormatter current = null; 120 for (var f : flattened) { 121 if (f.isNoop()) { 122 continue; 123 } 124 else if (current == null && f instanceof StaticFormatter sf) { 125 current = sf; 126 } 127 else if (current != null && f instanceof StaticFormatter sf) { 128 current = current.concat(sf); 129 } 130 else if (current != null) { 131 resolved.add(current); 132 resolved.add(f); 133 current = null; 134 } 135 else { 136 resolved.add(f); 137 } 138 } 139 if (current != null) { 140 resolved.add(current); 141 } 142 return resolved.toArray(new LogFormatter[] {}); 143 } 144 145 @Override 146 public String toString() { 147 return "STATIC['" + content + "']"; 148 } 149 } 150 151 /** 152 * Log formatter builder that is composed of other formatters. The 153 * {@link #add(LogFormatter)} are executed in insertion order. <strong> This builder 154 * is smart and will coalesce and consolidate formatters! </strong> For example if 155 * only formatter is added to the builder it will be returned instead of a new 156 * formatter. 157 */ 158 public final static class Builder { 159 160 private List<LogFormatter> formatters = new ArrayList<>(); 161 162 private Builder() { 163 } 164 165 /** 166 * Adds a formatter. 167 * @param formatter formatter to be added the list of formatters. 168 * @return this builder. 169 */ 170 public Builder add(LogFormatter formatter) { 171 formatters.add(formatter); 172 return this; 173 } 174 175 /** 176 * Adds an event formatter. 177 * @param formatter event formatter to be added the list of formatters. 178 * @return this builder. 179 */ 180 public Builder event(LogFormatter.EventFormatter formatter) { 181 formatters.add(formatter); 182 return this; 183 } 184 185 /** 186 * Append the timestamp in ISO format. 187 * @return this builder. 188 */ 189 public Builder timeStamp() { 190 formatters.add(DefaultInstantFormatter.ISO); 191 return this; 192 } 193 194 /** 195 * Formatter for {@link LogEvent#timestamp()} derived from standard 196 * {@link DateTimeFormatter}. 197 * @param dateTimeFormatter formatter for {@link LogEvent#timestamp()} 198 * @return this builder. 199 */ 200 public Builder timeStamp(DateTimeFormatter dateTimeFormatter) { 201 formatters.add(new DateTimeFormatterInstantFormatter(dateTimeFormatter)); 202 return this; 203 } 204 205 /** 206 * Adds the default level formatter. 207 * @return this builder. 208 * @see LevelFormatter 209 */ 210 public Builder level() { 211 formatters.add(LogFormatter.LevelFormatter.of()); 212 return this; 213 } 214 215 /** 216 * Adds the default logger name formatter. 217 * @return this builder. 218 */ 219 public Builder loggerName() { 220 formatters.add(DefaultNameFormatter.LOGGER_NAME_FORMATTER); 221 return this; 222 } 223 224 /** 225 * Formats the message by calling 226 * {@link LogEvent#formattedMessage(StringBuilder)}. 227 * @return this builder. 228 */ 229 public Builder message() { 230 formatters.add(DefaultMessageFormatter.MESSAGE_FORMATTER); 231 return this; 232 } 233 234 /** 235 * Appends static text. 236 * @param content static text. 237 * @return this builder. 238 * @see StaticFormatter 239 */ 240 public Builder text(String content) { 241 formatters.add(new StaticFormatter(content)); 242 return this; 243 } 244 245 /** 246 * Creates a formatter that will print <strong>ALL</strong> of the key values by 247 * percent encoding (RFC 3986 URI aka the format usually used in 248 * {@link URI#getQuery()}). 249 * @return formatter. 250 */ 251 public Builder keyValues() { 252 return add(DefaultKeyValuesFormatter.INSTANCE); 253 } 254 255 /** 256 * Creates a formatter that will print the key values in order of the passed in 257 * keys if they exist in percent encoding (RFC 3986 URI aka the format usually 258 * used in {@link URI#getQuery()}). <strong>An empty list is considered a noop and 259 * no keys will be ommitted!</strong> If you want to all keys use 260 * {@link #keyValues()}. 261 * @param keys keys where order is important. 262 * @return this. 263 */ 264 public Builder keyValues(List<String> keys) { 265 if (keys.isEmpty()) { 266 return this; 267 } 268 return add(new ListKeyValuesFormatter(keys)); 269 } 270 271 /** 272 * Creates a formatter that will print a single key value in percent encoding (RFC 273 * 3986 URI aka the format usually used in {@link URI#getQuery()}). 274 * @param key key to select. 275 * @param fallback if the value is null the fallback will be used. 276 * @return this. 277 */ 278 public Builder keyValue(String key, @Nullable String fallback) { 279 return add(new SingleKeyValueFormatter(key, fallback)); 280 } 281 282 /** 283 * Appends a space. 284 * @return this builder. 285 */ 286 public Builder space() { 287 formatters.add(new StaticFormatter(" ")); 288 return this; 289 } 290 291 /** 292 * Appends a newline using the platforms line separator. 293 * @return this builder. 294 */ 295 public Builder newline() { 296 text(System.lineSeparator()); 297 return this; 298 } 299 300 /** 301 * Appends a thread name : {@link Thread#getName()}. 302 * @return this builder. 303 */ 304 public Builder threadName() { 305 formatters.add(DefaultThreadFormatter.THREAD_NAME_FORMATTER); 306 return this; 307 } 308 309 /** 310 * Appends a thread ID : {@link Thread#threadId()}. 311 * @return this builder. 312 */ 313 public Builder threadId() { 314 formatters.add(DefaultThreadFormatter.THREAD_ID_FORMATTER); 315 return this; 316 } 317 318 /** 319 * Appends the events throwable stack trace. 320 * @return this. 321 */ 322 public Builder throwable() { 323 formatters.add(ThrowableFormatter.of()); 324 return this; 325 } 326 327 /** 328 * Will create a generic log formatter that has the inner formatters coalesced if 329 * possible and will noop if there are no formatters. 330 * @return flattened formatter. 331 */ 332 public LogFormatter build() { 333 var array = StaticFormatter.coalesce(formatters); 334 if (array.length == 0) { 335 return NoopFormatter.INSTANCE; 336 } 337 if (array.length == 1) { 338 return array[0]; 339 } 340 return EventFormatter.of(formatters); 341 } 342 343 /** 344 * Creates the formatter and converts it to an encoder. 345 * @return encoder. 346 * @apiNote for ergonomics 347 */ 348 public LogEncoder encoder() { 349 return LogEncoder.of(build()); 350 } 351 352 } 353 354 /** 355 * Generic event formatting that is lambda friendly. 356 */ 357 @FunctionalInterface 358 public non-sealed interface EventFormatter extends LogFormatter { 359 360 @Override 361 public void format(StringBuilder output, LogEvent event); 362 363 private static EventFormatter of(List<? extends LogFormatter> formatters) { 364 return new CompositeFormatter(StaticFormatter.coalesce(formatters)); 365 } 366 367 } 368 369 /** 370 * Formats a {@link Level}. 371 */ 372 public sealed interface LevelFormatter extends LogFormatter { 373 374 /** 375 * Formats the level. 376 * @param output buffer. 377 * @param level level. 378 */ 379 void formatLevel(StringBuilder output, Level level); 380 381 @Override 382 default void format(StringBuilder output, LogEvent event) { 383 formatLevel(output, event.level()); 384 } 385 386 /** 387 * Formats a level. 388 * @param level level 389 * @return formatted level as a string. 390 */ 391 default String formatLevel(Level level) { 392 StringBuilder sb = new StringBuilder(); 393 formatLevel(sb, level); 394 return sb.toString(); 395 } 396 397 /** 398 * Default implementation calls {@link LevelFormatter#toString(Level)} 399 * @return formatter. 400 */ 401 public static LevelFormatter of() { 402 return DefaultLevelFormatter.LEVEL_FORMATTER; 403 } 404 405 /** 406 * Default implementation calls {@link LevelFormatter#rightPadded(Level)} 407 * @return formatter. 408 */ 409 public static LevelFormatter ofRightPadded() { 410 return DefaultLevelFormatter.RIGHT_PAD_LEVEL_FORMATTER; 411 } 412 413 /** 414 * Turns a Level into a SLF4J like level String that is all upper case. 415 * {@link Level#ALL} is "<code>TRACE</code>", {@link Level#OFF} is 416 * "<code>ERROR</code>" and {@link Level#WARNING} is "<code>WARN</code>". 417 * @param level system logger level. 418 * @return upper case string of level. 419 */ 420 public static String toString(Level level) { 421 return switch (level) { 422 case DEBUG -> "DEBUG"; 423 case ALL -> "TRACE"; 424 case ERROR -> "ERROR"; 425 case INFO -> "INFO"; 426 case OFF -> "ERROR"; 427 case TRACE -> "TRACE"; 428 case WARNING -> "WARN"; 429 }; 430 } 431 432 /** 433 * Turns a Level into a SLF4J like level String that is all upper case and same 434 * length with right padding. {@link Level#ALL} is "<code>TRACE</code>", 435 * {@link Level#OFF} is "<code>ERROR</code>" and {@link Level#WARNING} is 436 * "<code>WARN</code>". 437 * @param level system logger level. 438 * @return upper case string of level. 439 */ 440 public static String rightPadded(Level level) { 441 return switch (level) { 442 case DEBUG -> /* */ "DEBUG"; 443 case ALL -> /* */ "TRACE"; 444 case ERROR -> /* */ "ERROR"; 445 case INFO -> /* */ "INFO "; 446 case OFF -> /* */ "ERROR"; 447 case TRACE -> /* */ "TRACE"; 448 case WARNING -> /* */ "WARN "; 449 }; 450 } 451 452 } 453 454 /** 455 * Formats event timestamps. 456 */ 457 public sealed interface TimestampFormatter extends LogFormatter { 458 459 /** 460 * The default timestamp format used in many logging frameworks which does not 461 * have dates and only time at millisecond precision. 462 * <p> 463 * It is called TTLL as that is the name of the format where it is used in 464 * logback, log4j etc. 465 */ 466 public static String TTLL_TIME_FORMAT = "HH:mm:ss.SSS"; 467 468 /** 469 * Format timestamp. 470 * @param output buffer. 471 * @param instant timestamp. 472 */ 473 void formatTimestamp(StringBuilder output, Instant instant); 474 475 @Override 476 default void format(StringBuilder output, LogEvent event) { 477 formatTimestamp(output, event.timestamp()); 478 } 479 480 /** 481 * Formats timestamp using {@link #TTLL_TIME_FORMAT}. 482 * @return formatter. 483 */ 484 public static TimestampFormatter of() { 485 return DefaultInstantFormatter.TTLL; 486 } 487 488 /** 489 * Formats a timestamp using ISO format. 490 * @return formatter. 491 */ 492 public static TimestampFormatter ofISO() { 493 return DefaultInstantFormatter.ISO; 494 } 495 496 /** 497 * Micro seconds over the events last second. This is for logback compatibility. 498 * @return microseconds zero padded. 499 */ 500 public static TimestampFormatter ofMicros() { 501 return DefaultInstantFormatter.MICROS; 502 } 503 504 /** 505 * Formats a timestamp using standard JDK date time formatter. 506 * @param dateTimeFormatter date time formatter. 507 * @return timestamp formatter. 508 */ 509 public static TimestampFormatter of(DateTimeFormatter dateTimeFormatter) { 510 return new DateTimeFormatterInstantFormatter(dateTimeFormatter); 511 } 512 513 } 514 515 /** 516 * Formats a throwable. 517 */ 518 public non-sealed interface ThrowableFormatter extends LogFormatter { 519 520 /** 521 * Formats a throwable and appends. 522 * @param output buffer. 523 * @param throwable throwable. 524 */ 525 void formatThrowable(StringBuilder output, Throwable throwable); 526 527 @Override 528 default void format(StringBuilder output, LogEvent event) { 529 var t = event.throwableOrNull(); 530 if (t != null) { 531 formatThrowable(output, t); 532 } 533 } 534 535 /** 536 * Default implementation uses {@link Throwable#printStackTrace(PrintWriter)}. 537 * @return formatter. 538 */ 539 public static ThrowableFormatter of() { 540 return DefaultThrowableFormatter.INSTANT; 541 } 542 543 /** 544 * Convenience to append a throwable to string builder. 545 * @param b buffer. 546 * @param t throwable. 547 * @apiNote this call creates garbage. 548 */ 549 public static void appendThrowable(StringBuilder b, Throwable t) { 550 /* 551 * TODO optimize 552 */ 553 t.printStackTrace(Internal.StringBuilderPrintWriter.of(b)); 554 } 555 556 } 557 558 /** 559 * Tests if the log formatter is noop or is null which will be considered as noop. 560 * @param logFormatter formatter which <strong>can be <code>null</code></strong>! 561 * @return true if the formatter should not be used. 562 */ 563 public static boolean isNoopOrNull(@Nullable LogFormatter logFormatter) { 564 return logFormatter == null || logFormatter.isNoop(); 565 } 566 567 /** 568 * Pads the right hand side of text with space. 569 * @param sb buffer. 570 * @param s string that will be appended first (left hand) and will not be longer than 571 * the <code>n</code> parameter. 572 * @param n the size of string. If the size is bigger than passed in string the result 573 * will have padding otherwise the passed in string will be cut to the size of this 574 * parameter. 575 */ 576 public static void padRight(StringBuilder sb, CharSequence s, int n) { 577 int length = s.length(); 578 if (length >= n) { 579 sb.append(s, 0, n); 580 return; 581 } 582 sb.append(s); 583 spacePad(sb, n - length); 584 } 585 586 /** 587 * Pads the left hand side of text with space. 588 * @param sb buffer. 589 * @param s string that will be appended second (right hand) and will not be longer 590 * than the <code>n</code> parameter. 591 * @param n the size of string. If the size is bigger than passed in string the result 592 * will have padding otherwise the passed in string will be cut to the size of this 593 * parameter. 594 */ 595 public static void padLeft(StringBuilder sb, CharSequence s, int n) { 596 int length = s.length(); 597 if (length >= n) { 598 sb.append(s, 0, n); 599 return; 600 } 601 spacePad(sb, n - length); 602 sb.append(s); 603 } 604 605 /** 606 * Fast space padding method. 607 */ 608 private static void spacePad(final StringBuilder sbuf, final int length) { 609 int l = length; 610 while (l >= 32) { 611 sbuf.append(CompositeFormatter.SPACES[5]); 612 l -= 32; 613 } 614 615 for (int i = 4; i >= 0; i--) { 616 if ((l & (1 << i)) != 0) { 617 sbuf.append(CompositeFormatter.SPACES[i]); 618 } 619 } 620 } 621 622 /** 623 * A special formatter that will do nothing. 624 */ 625 enum NoopFormatter implements TimestampFormatter, ThrowableFormatter, LevelFormatter { 626 627 /** 628 * instance. 629 */ 630 INSTANCE; 631 632 @Override 633 public void formatThrowable(StringBuilder output, Throwable throwable) { 634 } 635 636 @Override 637 public void formatTimestamp(StringBuilder output, Instant instant) { 638 } 639 640 @Override 641 public void formatLevel(StringBuilder output, Level level) { 642 } 643 644 @Override 645 public void format(StringBuilder output, LogEvent event) { 646 } 647 648 } 649 650} 651 652@SuppressWarnings("ArrayRecordComponent") 653record CompositeFormatter(LogFormatter[] formatters) implements EventFormatter { 654 655 static String[] SPACES = { " ", " ", " ", " ", // 1,2,4,8 spaces 656 " ", // 16 spaces 657 " " }; // 32 spaces 658 659 @Override 660 public void format(StringBuilder output, LogEvent event) { 661 for (var formatter : formatters) { 662 formatter.format(output, event); 663 } 664 } 665 666 @Override 667 public String toString() { 668 return getClass().getSimpleName() + Arrays.toString(formatters); 669 } 670 671 public static List<LogFormatter> flatten(List<? extends LogFormatter> formatters) { 672 return List.copyOf(_flatten(formatters)); 673 } 674 675 private static List<LogFormatter> _flatten(List<? extends LogFormatter> formatters) { 676 List<LogFormatter> result = new ArrayList<>(); 677 for (var f : formatters) { 678 if (f instanceof CompositeFormatter cf) { 679 result.addAll(_flatten(cf)); 680 } 681 else { 682 result.add(f); 683 } 684 } 685 return result; 686 } 687 688 private static List<LogFormatter> _flatten(CompositeFormatter formatter) { 689 var formatters = formatter.formatters; 690 return _flatten(Arrays.asList(formatters)); 691 } 692 693} 694 695enum DefaultMessageFormatter implements LogFormatter { 696 697 MESSAGE_FORMATTER; 698 699 @Override 700 public void format(StringBuilder output, LogEvent event) { 701 event.formattedMessage(output); 702 703 } 704 705} 706 707enum DefaultNameFormatter implements LogFormatter { 708 709 LOGGER_NAME_FORMATTER; 710 711 @Override 712 public void format(StringBuilder output, LogEvent event) { 713 output.append(event.loggerName()); 714 715 } 716 717} 718 719enum DefaultLevelFormatter implements LevelFormatter { 720 721 LEVEL_FORMATTER { 722 @Override 723 public void formatLevel(StringBuilder output, Level level) { 724 output.append(LevelFormatter.toString(level)); 725 } 726 }, 727 RIGHT_PAD_LEVEL_FORMATTER { 728 @Override 729 public void formatLevel(StringBuilder output, Level level) { 730 output.append(LevelFormatter.rightPadded(level)); 731 } 732 } 733 734} 735 736enum DefaultThreadFormatter implements LogFormatter { 737 738 THREAD_NAME_FORMATTER() { 739 @Override 740 public void format(StringBuilder output, LogEvent event) { 741 output.append(event.threadName()); 742 } 743 }, 744 THREAD_ID_FORMATTER() { 745 @Override 746 public void format(StringBuilder output, LogEvent event) { 747 output.append(event.threadId()); 748 } 749 } 750 751} 752 753enum DefaultInstantFormatter implements TimestampFormatter { 754 755 TTLL(DateTimeFormatter.ofPattern(TTLL_TIME_FORMAT).withZone(ZoneId.from(ZoneOffset.UTC))), 756 ISO(DateTimeFormatter.ISO_DATE_TIME.withZone(ZoneId.from(ZoneOffset.UTC))), 757 MICROS(DateTimeFormatter.ISO_DATE_TIME) { 758 @Override 759 @SuppressWarnings("JavaInstantGetSecondsGetNano") 760 public void formatTimestamp(StringBuilder output, Instant instant) { 761 int nanos = instant.getNano(); 762 763 int millis_and_micros = nanos / 1000; 764 int micros = millis_and_micros % 1000; 765 766 if (micros >= 100) { 767 output.append(micros); 768 } 769 else if (micros >= 10) { 770 output.append("0").append(micros); 771 } 772 else { 773 output.append("00").append(micros); 774 } 775 } 776 }; 777 778 private final DateTimeFormatter formatter; 779 780 DefaultInstantFormatter(DateTimeFormatter formatter) { 781 this.formatter = formatter; 782 } 783 784 @Override 785 public void formatTimestamp(StringBuilder output, Instant instant) { 786 formatter.formatTo(instant, output); 787 } 788 789} 790 791record DateTimeFormatterInstantFormatter(DateTimeFormatter dateTimeFormatter) implements TimestampFormatter { 792 793 @Override 794 public void formatTimestamp(StringBuilder output, Instant instant) { 795 dateTimeFormatter.formatTo(instant, output); 796 } 797} 798 799enum DefaultThrowableFormatter implements ThrowableFormatter { 800 801 INSTANT; 802 803 @Override 804 public void formatThrowable(StringBuilder output, Throwable throwable) { 805 ThrowableFormatter.appendThrowable(output, throwable); 806 } 807 808} 809 810enum DefaultKeyValuesFormatter implements LogFormatter, KeyValuesConsumer<StringBuilder> { 811 812 INSTANCE; 813 814 @Override 815 public void format(StringBuilder output, LogEvent event) { 816 var keyValues = event.keyValues(); 817 keyValues.forEach(this, 0, output); 818 } 819 820 static void formatKeyValue(StringBuilder output, String k, @Nullable String v) { 821 PercentCodec.encode(output, k, StandardCharsets.UTF_8); 822 if (v != null) { 823 output.append("="); 824 PercentCodec.encode(output, v, StandardCharsets.UTF_8); 825 } 826 } 827 828 @Override 829 public int accept(KeyValues values, String key, @Nullable String value, int index, StringBuilder storage) { 830 if (index > 0) { 831 storage.append("&"); 832 } 833 formatKeyValue(storage, key, value); 834 return index + 1; 835 } 836 837} 838 839final class ListKeyValuesFormatter implements LogFormatter { 840 841 private final String[] keys; 842 843 @SuppressWarnings("nullness") 844 ListKeyValuesFormatter(List<String> keys) { 845 var ks = List.copyOf(keys); 846 this.keys = ks.toArray(new String[] {}); 847 } 848 849 @Override 850 public void format(StringBuilder output, LogEvent event) { 851 var kvs = event.keyValues(); 852 formatKeyValues(output, kvs); 853 } 854 855 void formatKeyValues(StringBuilder output, KeyValues keyValues) { 856 boolean first = true; 857 for (String k : keys) { 858 String v = keyValues.getValueOrNull(k); 859 if (v == null) { 860 continue; 861 } 862 if (first) { 863 first = false; 864 } 865 else { 866 output.append("&"); 867 } 868 DefaultKeyValuesFormatter.formatKeyValue(output, k, v); 869 } 870 } 871 872} 873 874record SingleKeyValueFormatter(String key, @Nullable String fallback) implements LogFormatter { 875 @Override 876 public void format(StringBuilder output, LogEvent event) { 877 var kvs = event.keyValues(); 878 formatKeyValues(output, kvs); 879 } 880 881 void formatKeyValues(StringBuilder output, KeyValues keyValues) { 882 String v = keyValues.getValueOrNull(key); 883 if (v == null) { 884 v = fallback; 885 } 886 DefaultKeyValuesFormatter.formatKeyValue(output, key, v); 887 } 888 889}