001package io.jstach.rainbowgum;
002
003import java.lang.StackWalker.Option;
004import java.lang.StackWalker.StackFrame;
005import java.lang.System.Logger.Level;
006import java.time.Instant;
007import java.util.ArrayList;
008import java.util.List;
009import java.util.function.Supplier;
010
011import org.eclipse.jdt.annotation.Nullable;
012
013import io.jstach.rainbowgum.KeyValues.MutableKeyValues;
014import io.jstach.rainbowgum.LogEvent.Builder;
015import io.jstach.rainbowgum.LogEvent.Caller;
016import io.jstach.rainbowgum.LogRouter.Router;
017
018/**
019 * A LogEvent is a container for a single call to a logger. An event should not be created
020 * unless a route or logger is actually enabled.
021 *
022 * @author agentgt
023 * @apiNote LogEvent is currently sealed. The reason there are so many static creation
024 * methods is for optimization purposes because other than actual outputting creating
025 * events is generally the most expensive operation (mostly in terms of memory) a logging
026 * system does.
027 * @see LogMessageFormatter
028 */
029public sealed interface LogEvent {
030
031        /**
032         * Creates a log event.
033         * @param level the logging level.
034         * @param loggerName the name of the logger which is usually a class name.
035         * @param formattedMessage the unformatted message.
036         * @param keyValues key values that come from MDC or an SLF4J Event Builder.
037         * @param throwable an exception if passed maybe <code>null</code>.
038         * @return event
039         * @see LevelResolver
040         * @apiNote the message is already assumed to be formatted as no arguments are passed.
041         */
042        public static LogEvent of(System.Logger.Level level, String loggerName, String formattedMessage,
043                        KeyValues keyValues, @Nullable Throwable throwable) {
044                Instant timeStamp = Instant.now();
045                Thread currentThread = Thread.currentThread();
046                String threadName = currentThread.getName();
047                long threadId = currentThread.threadId();
048
049                return new DefaultLogEvent(timeStamp, threadName, threadId, level, loggerName, formattedMessage, keyValues,
050                                throwable);
051
052        }
053
054        /**
055         * Creates a log event.
056         * @param level the logging level.
057         * @param loggerName the name of the logger which is usually a class name.
058         * @param formattedMessage the unformatted message.
059         * @param throwable an exception if passed maybe <code>null</code>.
060         * @return event
061         * @see LevelResolver
062         * @apiNote the message is already assumed to be formatted as no arguments are passed.
063         */
064        static LogEvent of(System.Logger.Level level, String loggerName, String formattedMessage,
065                        @Nullable Throwable throwable) {
066                return of(level, loggerName, formattedMessage, KeyValues.of(), throwable);
067        }
068
069        /**
070         * Creates a log event.
071         * @param level the logging level.
072         * @param loggerName the name of the logger which is usually a class name.
073         * @param message the unformatted message.
074         * @param keyValues key values that come from MDC or an SLF4J Event Builder.
075         * @param messageFormatter formatter to use for rendering a message when
076         * #{@link LogEvent#formattedMessage(StringBuilder)} is called.
077         * @param arg1 argument that will be passed to messageFormatter.
078         * @return event
079         * @see LevelResolver
080         * @see LogMessageFormatter
081         */
082        public static LogEvent of(System.Logger.Level level, String loggerName, String message, KeyValues keyValues,
083                        LogMessageFormatter messageFormatter, @Nullable Object arg1) {
084                Instant timeStamp = Instant.now();
085                Thread currentThread = Thread.currentThread();
086                String threadName = currentThread.getName();
087                long threadId = currentThread.threadId();
088                if (arg1 instanceof Throwable t) {
089                        return new DefaultLogEvent(timeStamp, threadName, threadId, level, loggerName, message, keyValues, t);
090                }
091                return new OneArgLogEvent(timeStamp, threadName, threadId, level, loggerName, message, keyValues,
092                                messageFormatter, null, arg1);
093        }
094
095        /**
096         * Creates a log event.
097         * @param level the logging level.
098         * @param loggerName the name of the logger which is usually a class name.
099         * @param message the unformatted message.
100         * @param keyValues key values that come from MDC or an SLF4J Event Builder.
101         * @param messageFormatter formatter to use for rendering a message when
102         * #{@link LogEvent#formattedMessage(StringBuilder)} is called.
103         * @param arg1 argument that will be passed to messageFormatter.
104         * @param arg2 argument that will be passed to messageFormatter.
105         * @return event
106         * @see LevelResolver
107         * @see LogMessageFormatter
108         */
109        public static LogEvent of(System.Logger.Level level, String loggerName, String message, KeyValues keyValues,
110                        LogMessageFormatter messageFormatter, @Nullable Object arg1, @Nullable Object arg2) {
111                Instant timeStamp = Instant.now();
112                Thread currentThread = Thread.currentThread();
113                String threadName = currentThread.getName();
114                long threadId = currentThread.threadId();
115                if (arg2 instanceof Throwable t) {
116                        return new OneArgLogEvent(timeStamp, threadName, threadId, level, loggerName, message, keyValues,
117                                        messageFormatter, t, arg1);
118                }
119                return new TwoArgLogEvent(timeStamp, threadName, threadId, level, loggerName, message, keyValues,
120                                messageFormatter, null, arg1, arg2);
121        }
122
123        /**
124         * Creates a log event.
125         * @param level the logging level.
126         * @param loggerName the name of the logger which is usually a class name.
127         * @param message the unformatted message.
128         * @param keyValues key values that come from MDC or an SLF4J Event Builder.
129         * @param messageFormatter formatter to use for rendering a message when
130         * #{@link LogEvent#formattedMessage(StringBuilder)} is called.
131         * @param args an array of arguments that will be passed to messageFormatter. The
132         * contents maybe null elements but the array itself should not be null.
133         * @return event
134         * @see LevelResolver
135         * @see LogMessageFormatter
136         */
137        public static LogEvent ofArgs(System.Logger.Level level, String loggerName, String message, KeyValues keyValues,
138                        LogMessageFormatter messageFormatter, @Nullable Object[] args) {
139                Instant timeStamp = Instant.now();
140                Thread currentThread = Thread.currentThread();
141                String threadName = currentThread.getName();
142                long threadId = currentThread.threadId();
143                return ofAll(timeStamp, threadName, threadId, level, loggerName, message, keyValues, null, messageFormatter,
144                                args);
145        }
146
147        /**
148         * Creates a log event with everything specified.
149         * @param timestamp time of event
150         * @param threadName or empty string
151         * @param threadId thread id or 0 if that cannot be resolved.
152         * @param level the logging level.
153         * @param loggerName the name of the logger which is usually a class name.
154         * @param message the unformatted message.
155         * @param keyValues key values that come from MDC or an SLF4J Event Builder.
156         * @param throwable or <code>null</code>.
157         * @param messageFormatter formatter to use for rendering a message when
158         * #{@link LogEvent#formattedMessage(StringBuilder)} is called.
159         * @param args an array of arguments that will be passed to messageFormatter. The
160         * contents maybe null elements but the array itself should not be null.
161         * @return event
162         * @see LevelResolver
163         * @see LogMessageFormatter
164         */
165        public static LogEvent ofAll(Instant timestamp, String threadName, long threadId, System.Logger.Level level,
166                        String loggerName, String message, KeyValues keyValues, @Nullable Throwable throwable,
167                        LogMessageFormatter messageFormatter, @SuppressWarnings("exports") @Nullable List<@Nullable Object> args) {
168
169                if (args == null) {
170                        return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
171                                        throwable);
172                }
173                int length = args.size();
174                if (throwable == null && length > 0 && args.get(length - 1) instanceof Throwable t) {
175                        throwable = t;
176                        length = length - 1;
177                }
178                return switch (length) {
179                        case 0 ->
180                                new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues, throwable);
181                        case 1 -> new OneArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
182                                        messageFormatter, throwable, args.get(0));
183                        case 2 -> new TwoArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
184                                        messageFormatter, throwable, args.get(0), args.get(1));
185                        default -> new ArrayArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
186                                        messageFormatter, throwable, args.toArray(), length);
187                };
188        }
189
190        /**
191         * Creates a log event with everything specified.
192         * @param timestamp time of event
193         * @param threadName or empty string
194         * @param threadId thread id or 0 if that cannot be resolved.
195         * @param level the logging level.
196         * @param loggerName the name of the logger which is usually a class name.
197         * @param message the unformatted message.
198         * @param keyValues key values that come from MDC or an SLF4J Event Builder.
199         * @param throwable or <code>null</code>.
200         * @param messageFormatter formatter to use for rendering a message when
201         * #{@link LogEvent#formattedMessage(StringBuilder)} is called.
202         * @param args an array of arguments that will be passed to messageFormatter. The
203         * contents maybe null elements but the array itself should not be null.
204         * @return event
205         * @see LevelResolver
206         * @see LogMessageFormatter
207         */
208        public static LogEvent ofAll(Instant timestamp, String threadName, long threadId, System.Logger.Level level,
209                        String loggerName, String message, KeyValues keyValues, @Nullable Throwable throwable,
210                        LogMessageFormatter messageFormatter, @SuppressWarnings("exports") @Nullable Object @Nullable [] args) {
211
212                if (args == null) {
213                        return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
214                                        throwable);
215                }
216                int length = args.length;
217                if (throwable == null && length > 0 && args[length - 1] instanceof Throwable t) {
218                        throwable = t;
219                        length = length - 1;
220                }
221                return switch (length) {
222                        case 0 ->
223                                new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues, throwable);
224                        case 1 -> new OneArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
225                                        messageFormatter, throwable, args[0]);
226                        case 2 -> new TwoArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
227                                        messageFormatter, throwable, args[0], args[1]);
228                        default -> new ArrayArgLogEvent(timestamp, threadName, threadId, level, loggerName, message, keyValues,
229                                        messageFormatter, throwable, args, length);
230                };
231        }
232
233        /**
234         * Creates a new event with the caller info attached. <em> This method always returns
235         * a new event does not check if the original has caller info. </em>
236         * @param event original event.
237         * @param caller caller info.
238         * @return new log event.
239         */
240        public static LogEvent withCaller(LogEvent event, Caller caller) {
241                return new StackFrameLogEvent(event, caller);
242        }
243
244        /**
245         * Timestamp when the event was created.
246         * @return instant when the event was created.
247         */
248        public Instant timestamp();
249
250        /**
251         * Name of the thread.
252         * @return thread name.
253         * @apiNote this maybe empty and often is if virtual threads are used.
254         */
255        public String threadName();
256
257        /**
258         * Thread id.
259         * @return thread id.
260         */
261        public long threadId();
262
263        /**
264         * The logging level. {@linkplain java.lang.System.Logger.Level#ALL} and
265         * {@linkplain java.lang.System.Logger.Level#OFF} should not be returned as they have
266         * special meaning.
267         * @return level.
268         */
269        public System.Logger.Level level();
270
271        /**
272         * Name of logger.
273         * @return name of logger.
274         */
275        public String loggerName();
276
277        /**
278         * Unformatted message.
279         * @return unformatted message
280         * @see #formattedMessage(StringBuilder)
281         */
282        public String message();
283
284        /**
285         * Appends the formatted message.
286         * @param sb string builder to use.
287         * @see LogMessageFormatter
288         */
289        public void formattedMessage(StringBuilder sb);
290
291        /**
292         * Throwable at the time of the event passed from the logger.
293         * @return if the event does not have a throwable <code>null</code> will be returned.
294         */
295        public @Nullable Throwable throwableOrNull();
296
297        /**
298         * Key values that usually come from MDC or an SLF4J Event Builder.
299         * @return key values.
300         */
301        public KeyValues keyValues();
302
303        /**
304         * Returns info about caller or <code>null</code> if not supported.
305         * @return caller info
306         */
307        default @Nullable Caller callerOrNull() {
308                return null;
309        }
310
311        /**
312         * Freeze will return a LogEvent that is safe to use in a different thread. Usually
313         * this entails copying the data or checking if it is already immutable. Freeze should
314         * be called before passing an event to an {@link LogPublisher.AsyncLogPublisher}.
315         * @return thread safe log event.
316         */
317        public LogEvent freeze();
318
319        /**
320         * Freeze and replace with the given timestamp.
321         * @param timestamp instant to replace timestamp in this.
322         * @return a copy of this with the given timestamp.
323         */
324        public LogEvent freeze(Instant timestamp);
325
326        /**
327         * A builder to create events by calling {@link Router#eventBuilder(String, Level)}.
328         * <strong>{@link #log()}</strong> should be called at the very end otherwise the
329         * event will not be logged. <strong> Because of potential future optimizations (see
330         * API note) It is best to assume this builder returns a different builder on every
331         * setter call even if it may not currently! </strong>
332         *
333         * @apiNote unless the builder gets marked for escape analysis the static
334         * {@link LogEvent} factory methods are more likely to be superior in performance. EA
335         * is tricky business and often requires the object be small enough and should not
336         * leave the method it is constructed in but the JDK is continuously getting better
337         * optimizing immutable objects. Thus it is best to assume this builder will return a
338         * different builder on every call.
339         */
340        public sealed interface Builder {
341
342                /**
343                 * Timestamp when the event was created.
344                 * @param timestamp time the event happened.
345                 * @return this.
346                 */
347                public Builder timestamp(Instant timestamp);
348
349                /**
350                 * Add an argument to the event being built.
351                 * @param argSupplier supplier will be called immediately if this event is to be
352                 * logged.
353                 * @return this.
354                 */
355                Builder arg(Supplier<?> argSupplier);
356
357                /**
358                 * Add an argument to the event being built.
359                 * @param arg maybe null.
360                 * @return this.
361                 */
362                Builder arg(@Nullable Object arg);
363
364                /**
365                 * Sets the message formatter which interpolates argument place holders.
366                 * @param messageFormatter formatter if not set
367                 * {@link LogMessageFormatter.StandardMessageFormatter#SLF4J} will be used.
368                 * @return this.
369                 */
370                public Builder messageFormatter(LogMessageFormatter messageFormatter);
371
372                /**
373                 * Name of the thread.
374                 * @param threadName name of thread.
375                 * @return this.
376                 * @apiNote this maybe empty and often is if virtual threads are used.
377                 */
378                public Builder threadName(String threadName);
379
380                /**
381                 * Thread id.
382                 * @param threadId {@link Thread#getId()}.
383                 * @return this.
384                 */
385                public Builder threadId(long threadId);
386
387                /**
388                 * Unformatted message.
389                 * @param message unformatted message.
390                 * @return unformatted message
391                 */
392                public Builder message(String message);
393
394                /**
395                 * Throwable at the time of the event passed from the logger.
396                 * @param throwable exception at the time of the event.
397                 * @return if the event does not have a throwable <code>null</code> will be
398                 * returned.
399                 */
400                public Builder throwable(@Nullable Throwable throwable);
401
402                /**
403                 * Key values that usually come from MDC or an SLF4J Event Builder.
404                 * @param keyValues will use the passed in key values for the event.
405                 * @return key values.
406                 */
407                public Builder keyValues(KeyValues keyValues);
408
409                /**
410                 * Adds caller info.
411                 * @param caller caller maybe <code>null</code>.
412                 * @return this.
413                 */
414                public Builder caller(@Nullable Caller caller);
415
416                /**
417                 * Will log the event with the current values.
418                 * @return true if accepted.
419                 */
420                public boolean log();
421
422                /**
423                 * Will generate the event if the router can accept otherwise <code>null</code>.
424                 * @return event or <code>null</code>.
425                 * @apiNote The event is not logged but just created if possible if logging is
426                 * desired use {@link #log()} or check if the return is not <code>null</code> and
427                 * then log.
428                 */
429                public @Nullable LogEvent eventOrNull();
430
431        }
432
433        /**
434         * Caller info usually derived from Stack walking.
435         */
436        public sealed interface Caller {
437
438                /**
439                 * Creates caller info from a stack frame.
440                 * @param stackFrame stack frame must have
441                 * {@link java.lang.StackWalker.Option#RETAIN_CLASS_REFERENCE}.
442                 * @return caller info.
443                 */
444                public static Caller of(StackFrame stackFrame) {
445                        return new StackFrameCallerInfo(stackFrame);
446                }
447
448                /**
449                 * Returns caller from a certain depth or <code>null</code>
450                 * @param depth how deep in the stack to pull stack frame.
451                 * @return caller or <code>null</code>.
452                 */
453                public static @Nullable Caller ofDepthOrNull(int depth) {
454                        return StackFrameCallerInfo.stackWalker.<@Nullable Caller>walk(
455                                        s -> s.skip(depth + 1).limit(1).map(f -> Caller.of(f)).findFirst().orElse(null));
456                }
457
458                /**
459                 * See {@link StackFrame#getClassName()}.
460                 * @return class name.
461                 */
462                public String className();
463
464                /**
465                 * See {@link StackFrame#getFileName()}.
466                 * @return file name.
467                 */
468                public @Nullable String fileNameOrNull();
469
470                /**
471                 * See {@link StackFrame#getLineNumber()}.
472                 * @return line number.
473                 */
474                public int lineNumber();
475
476                /**
477                 * See {@link StackFrame#getMethodName()}.
478                 * @return method name.
479                 */
480                public String methodName();
481
482                /**
483                 * Make the caller info immutable.
484                 * @return immutable caller info and if this is already immutable return this.
485                 */
486                public Caller freeze();
487
488                /**
489                 * Convenience toString for caller.
490                 * @param caller if caller is <code>null</code> the string "null" will be
491                 * returned.
492                 * @return string representation.
493                 */
494                public static String toString(@Nullable Caller caller) {
495                        /*
496                         * TODO maybe move this to formatters?
497                         */
498                        if (caller == null)
499                                return "null";
500                        return caller.fileNameOrNull() + ":" + caller.lineNumber() + "/" + caller.className() + "."
501                                        + caller.methodName();
502                }
503
504        }
505
506}
507
508record FrozenCallerInfo(String className, @Nullable String fileNameOrNull, int lineNumber,
509                String methodName) implements LogEvent.Caller {
510        @Override
511        public Caller freeze() {
512                return this;
513        }
514}
515
516record StackFrameCallerInfo(StackFrame stackFrame) implements LogEvent.Caller {
517
518        static final StackWalker stackWalker = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE);
519
520        @Override
521        public String className() {
522                return stackFrame.getClassName();
523        }
524
525        @Override
526        public @Nullable String fileNameOrNull() {
527                /*
528                 * TODO checker did not know file name is null.
529                 */
530                return stackFrame.getFileName();
531        }
532
533        @Override
534        public int lineNumber() {
535                return stackFrame.getLineNumber();
536        }
537
538        @Override
539        public String methodName() {
540                return stackFrame.getMethodName();
541        }
542
543        @Override
544        public Caller freeze() {
545                return new FrozenCallerInfo(className(), fileNameOrNull(), lineNumber(), methodName());
546        }
547
548}
549
550final class LogEventBuilder implements LogEvent.Builder {
551
552        private final LogEventLogger logger;
553
554        private final Level level;
555
556        private final String loggerName;
557
558        private Instant timestamp = Instant.now();
559
560        private String threadName = Thread.currentThread().getName();
561
562        private long threadId = Thread.currentThread().threadId();
563
564        private String message = "";
565
566        private @Nullable Throwable throwable;
567
568        private KeyValues keyValues = KeyValues.of();
569
570        private @Nullable List<@Nullable Object> args = null;
571
572        private LogMessageFormatter messageFormatter = LogMessageFormatter.StandardMessageFormatter.SLF4J;
573
574        private @Nullable Caller caller = null;
575
576        LogEventBuilder(LogEventLogger logger, Level level, String loggerName) {
577                super();
578                this.logger = logger;
579                this.level = level;
580                this.loggerName = loggerName;
581        }
582
583        @Override
584        public Builder timestamp(Instant timestamp) {
585                this.timestamp = timestamp;
586                return this;
587        }
588
589        @Override
590        public Builder threadName(String threadName) {
591                this.threadName = threadName;
592                return this;
593        }
594
595        @Override
596        public Builder threadId(long threadId) {
597                this.threadId = threadId;
598                return this;
599        }
600
601        @Override
602        public Builder message(String message) {
603                this.message = message;
604                return this;
605        }
606
607        @Override
608        public Builder messageFormatter(LogMessageFormatter messageFormatter) {
609                this.messageFormatter = messageFormatter;
610                return this;
611        }
612
613        @Override
614        public Builder throwable(@Nullable Throwable throwable) {
615                this.throwable = throwable;
616                return this;
617        }
618
619        @Override
620        public Builder keyValues(KeyValues keyValues) {
621                this.keyValues = keyValues;
622                return this;
623        }
624
625        @Override
626        public Builder arg(@Nullable Object arg) {
627                var list = this.args;
628                if (list == null) {
629                        list = this.args = new ArrayList<>();
630                }
631                list.add(arg);
632                return this;
633        }
634
635        @Override
636        public Builder arg(Supplier<?> argSupplier) {
637                return arg(argSupplier.get());
638        }
639
640        @Override
641        public Builder caller(@Nullable Caller caller) {
642                this.caller = caller;
643                return this;
644        }
645
646        @Override
647        public boolean log() {
648                var event = eventOrNull();
649                logger.log(event);
650                return true;
651        }
652
653        @Override
654        public LogEvent eventOrNull() {
655                var event = LogEvent.ofAll(timestamp, threadName, threadId, level, loggerName, message, keyValues, throwable,
656                                messageFormatter, this.args);
657                var c = caller;
658                if (c != null) {
659                        return LogEvent.withCaller(event, c);
660                }
661                return event;
662        }
663
664}
665
666enum NoOpLogEventBuilder implements LogEvent.Builder {
667
668        NOOP;
669
670        @Override
671        public Builder timestamp(Instant timestamp) {
672                return this;
673        }
674
675        @Override
676        public Builder threadName(String threadName) {
677                return this;
678        }
679
680        @Override
681        public Builder threadId(long threadId) {
682                return this;
683        }
684
685        @Override
686        public Builder message(String message) {
687                return this;
688        }
689
690        @Override
691        public Builder throwable(@Nullable Throwable throwable) {
692                return this;
693        }
694
695        @Override
696        public Builder keyValues(KeyValues keyValues) {
697                return this;
698        }
699
700        @Override
701        public Builder messageFormatter(LogMessageFormatter messageFormatter) {
702                return this;
703        }
704
705        @Override
706        public boolean log() {
707                return false;
708        }
709
710        @Override
711        public @Nullable LogEvent eventOrNull() {
712                return null;
713        }
714
715        @Override
716        public Builder arg(Supplier<?> argSupplier) {
717                return this;
718        }
719
720        @Override
721        public Builder arg(@Nullable Object arg) {
722                return this;
723        }
724
725        @Override
726        public Builder caller(@Nullable Caller caller) {
727                return this;
728        }
729
730}
731
732record OneArgLogEvent(Instant timestamp, String threadName, long threadId, System.Logger.Level level, String loggerName,
733                String message, KeyValues keyValues, LogMessageFormatter messageFormatter, @Nullable Throwable throwableOrNull,
734                @Nullable Object arg1) implements LogEvent {
735
736        @Override
737        public void formattedMessage(StringBuilder sb) {
738                messageFormatter.format(sb, message, arg1);
739        }
740
741        @Override
742        public LogEvent freeze() {
743                return freeze(timestamp);
744        }
745
746        @Override
747        public LogEvent freeze(Instant timestamp) {
748                StringBuilder sb = new StringBuilder(message.length());
749                formattedMessage(sb);
750                return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, sb.toString(),
751                                keyValues.freeze(), throwableOrNull);
752        }
753
754}
755
756record TwoArgLogEvent(Instant timestamp, String threadName, long threadId, System.Logger.Level level, String loggerName,
757                String message, KeyValues keyValues, LogMessageFormatter messageFormatter, @Nullable Throwable throwableOrNull,
758                @Nullable Object arg1, @Nullable Object arg2) implements LogEvent {
759
760        @Override
761        public void formattedMessage(StringBuilder sb) {
762                messageFormatter.format(sb, message, arg1, arg2);
763        }
764
765        @Override
766        public LogEvent freeze() {
767                return freeze(timestamp);
768        }
769
770        @Override
771        public LogEvent freeze(Instant timestamp) {
772                StringBuilder sb = new StringBuilder(message.length());
773                formattedMessage(sb);
774                return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, sb.toString(),
775                                keyValues.freeze(), throwableOrNull);
776        }
777}
778
779@SuppressWarnings("ArrayRecordComponent")
780record ArrayArgLogEvent(Instant timestamp, String threadName, long threadId, System.Logger.Level level,
781                String loggerName, String message, KeyValues keyValues, LogMessageFormatter messageFormatter,
782                @Nullable Throwable throwableOrNull, @Nullable Object[] args, int length) implements LogEvent {
783
784        @Override
785        public void formattedMessage(StringBuilder sb) {
786                messageFormatter.formatArray(sb, message, args, length);
787        }
788
789        public int argCount() {
790                return args.length;
791        }
792
793        @Override
794        public LogEvent freeze() {
795                return freeze(timestamp);
796        }
797
798        @Override
799        public LogEvent freeze(Instant timestamp) {
800                StringBuilder sb = new StringBuilder(message.length());
801                formattedMessage(sb);
802                return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, sb.toString(),
803                                keyValues.freeze(), throwableOrNull);
804        }
805
806}
807
808record DefaultLogEvent(Instant timestamp, String threadName, long threadId, System.Logger.Level level,
809                String loggerName, String formattedMessage, KeyValues keyValues,
810                @Nullable Throwable throwableOrNull) implements LogEvent {
811
812        @Override
813        public void formattedMessage(StringBuilder sb) {
814                sb.append(this.formattedMessage);
815        }
816
817        @Override
818        public String message() {
819                return this.formattedMessage;
820        }
821
822        @Override
823        public LogEvent freeze() {
824                if (keyValues instanceof MutableKeyValues mkvs) {
825                        return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, formattedMessage,
826                                        mkvs.freeze(), throwableOrNull);
827                }
828                return this;
829        }
830
831        @Override
832        public LogEvent freeze(Instant timestamp) {
833                if (keyValues instanceof MutableKeyValues mkvs) {
834                        return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, formattedMessage,
835                                        mkvs.freeze(), throwableOrNull);
836                }
837                return new DefaultLogEvent(timestamp, threadName, threadId, level, loggerName, formattedMessage, keyValues,
838                                throwableOrNull);
839        }
840
841}
842
843record StackFrameLogEvent(LogEvent event, Caller callerOrNull) implements LogEvent {
844
845        @Override
846        public Instant timestamp() {
847                return event.timestamp();
848        }
849
850        @Override
851        public String threadName() {
852                return event.threadName();
853        }
854
855        @Override
856        public long threadId() {
857                return event.threadId();
858        }
859
860        @Override
861        public Level level() {
862                return event.level();
863        }
864
865        @Override
866        public String loggerName() {
867                return event.loggerName();
868        }
869
870        @Override
871        public String message() {
872                return event.message();
873        }
874
875        @Override
876        public void formattedMessage(StringBuilder sb) {
877                event.formattedMessage(sb);
878
879        }
880
881        @Override
882        public @Nullable Throwable throwableOrNull() {
883                return event.throwableOrNull();
884        }
885
886        @Override
887        public KeyValues keyValues() {
888                return event.keyValues();
889        }
890
891        @Override
892        public LogEvent freeze() {
893                var e = event.freeze();
894                var info = callerOrNull.freeze();
895                if (e == event && info == callerOrNull) {
896                        return this;
897                }
898
899                return new StackFrameLogEvent(e, info);
900        }
901
902        @Override
903        public LogEvent freeze(Instant timestamp) {
904                var e = event.freeze(timestamp);
905                var info = callerOrNull.freeze();
906                if (e == event && info == callerOrNull) {
907                        return this;
908                }
909                return new StackFrameLogEvent(e, info);
910        }
911
912}