001package io.jstach.rainbowgum.format;
002
003import io.jstach.rainbowgum.LogFormatter;
004import io.jstach.rainbowgum.LogFormatter.LevelFormatter;
005import io.jstach.rainbowgum.LogFormatter.TimestampFormatter;
006import io.jstach.rainbowgum.annotation.CaseChanging;
007import io.jstach.rainbowgum.annotation.EnumAlias;
008
009/**
010 * The choices for each part of the TTLL (Time, Thread, Level, Logger) format, the default
011 * text format, configured with {@link TTLLFormatterBuilder} or properties of the
012 * {@value #SCHEMA} encoder. Property values are the lowercase constant names, for
013 * example: <pre>
014 * logging.appender.console.encoder.ttll.timestamp=iso
015 * logging.appender.console.encoder.ttll.keyValues=json5
016 * </pre> The defaults produce the classic layout with the event's key values, percent
017 * encoded, in braces after the logger name:
018 * <code>12:00:00.123 [main] INFO  com.example.App {requestId=42} - hello</code>. When an
019 * event has no key values the braces are left out, or written as <code>{}</code>,
020 * according to {@link KeyValuesWhenEmpty}, and {@link KeyValuesFormat#NONE} leaves the
021 * key values out entirely.
022 * <p>
023 * Every part also accepts <code>default</code>, which restores its default without naming
024 * it, and <code>true</code> and <code>false</code> to show or hide it; the
025 * {@link io.jstach.rainbowgum.annotation.EnumAlias} on each constant lists them.
026 */
027public sealed interface TTLL permits TTLLFormatter {
028
029        /**
030         * URI scheme of the TTLL encoder, the default encoder when no other is configured.
031         */
032        String SCHEMA = "ttll";
033
034        /**
035         * Time part. A property value that is not one of these names is used as a
036         * {@link java.time.format.DateTimeFormatter} pattern in UTC, like slf4j-simple's
037         * <code>dateTimeFormat</code>.
038         */
039        @CaseChanging
040        enum TimestampFormat {
041
042                /**
043                 * <code>HH:mm:ss.SSS</code> in UTC. The default.
044                 */
045                @EnumAlias({ "default", "true" })
046                TTLL {
047                        @Override
048                        public LogFormatter formatter() {
049                                return TimestampFormatter.of();
050                        }
051                },
052                /**
053                 * ISO 8601 UTC instant with milliseconds, <code>2026-10-05T12:00:00.123Z</code>.
054                 */
055                ISO {
056                        @Override
057                        public LogFormatter formatter() {
058                                return TimestampFormatter.ofISO();
059                        }
060                },
061                /**
062                 * No time.
063                 */
064                @EnumAlias("false")
065                NONE {
066                        @Override
067                        public LogFormatter formatter() {
068                                return LogFormatter.noop();
069                        }
070                };
071
072                /**
073                 * The formatter for this choice.
074                 * @return formatter.
075                 */
076                public abstract LogFormatter formatter();
077
078                static TimestampFormat parse(String value) {
079                        return switch (value.strip().toLowerCase(java.util.Locale.ROOT)) {
080                                case "default", "true" -> TTLL;
081                                case "false" -> NONE;
082                                default -> io.jstach.rainbowgum.LogProperty.enumValue(TimestampFormat.class, value, "true", "false",
083                                                "default");
084                        };
085                }
086
087        }
088
089        /**
090         * Thread part, written in square brackets.
091         */
092        @CaseChanging
093        enum ThreadFormat {
094
095                /**
096                 * Thread name. The default.
097                 */
098                @EnumAlias({ "default", "true" })
099                NAME {
100                        @Override
101                        public LogFormatter formatter() {
102                                return LogFormatter.builder().threadName().build();
103                        }
104                },
105                /**
106                 * Thread id.
107                 */
108                ID {
109                        @Override
110                        public LogFormatter formatter() {
111                                return LogFormatter.builder().threadId().build();
112                        }
113                },
114                /**
115                 * No thread.
116                 */
117                @EnumAlias("false")
118                NONE {
119                        @Override
120                        public LogFormatter formatter() {
121                                return LogFormatter.noop();
122                        }
123                };
124
125                /**
126                 * The formatter for this choice.
127                 * @return formatter.
128                 */
129                public abstract LogFormatter formatter();
130
131                static ThreadFormat parse(String value) {
132                        return switch (value.strip().toLowerCase(java.util.Locale.ROOT)) {
133                                case "default", "true" -> NAME;
134                                case "false" -> NONE;
135                                default ->
136                                        io.jstach.rainbowgum.LogProperty.enumValue(ThreadFormat.class, value, "true", "false", "default");
137                        };
138                }
139
140        }
141
142        /**
143         * Level part.
144         */
145        @CaseChanging
146        enum LevelFormat {
147
148                /**
149                 * Upper case, right padded to five characters so messages line up. The default.
150                 */
151                @EnumAlias({ "default", "true" })
152                PADDED {
153                        @Override
154                        public LogFormatter formatter() {
155                                return LevelFormatter.ofRightPadded();
156                        }
157                },
158                /**
159                 * Upper case without padding.
160                 */
161                PLAIN {
162                        @Override
163                        public LogFormatter formatter() {
164                                return LevelFormatter.of();
165                        }
166                },
167                /**
168                 * Upper case in square brackets, like slf4j-simple's
169                 * <code>levelInBrackets</code>: <code>[INFO]</code>.
170                 */
171                BRACKETED {
172                        @Override
173                        public LogFormatter formatter() {
174                                return LogFormatter.builder().text("[").add(LevelFormatter.of()).text("]").build();
175                        }
176                },
177                /**
178                 * No level.
179                 */
180                @EnumAlias("false")
181                NONE {
182                        @Override
183                        public LogFormatter formatter() {
184                                return LogFormatter.noop();
185                        }
186                };
187
188                /**
189                 * The formatter for this choice.
190                 * @return formatter.
191                 */
192                public abstract LogFormatter formatter();
193
194                static LevelFormat parse(String value) {
195                        return switch (value.strip().toLowerCase(java.util.Locale.ROOT)) {
196                                case "default", "true" -> PADDED;
197                                case "false" -> NONE;
198                                default ->
199                                        io.jstach.rainbowgum.LogProperty.enumValue(LevelFormat.class, value, "true", "false", "default");
200                        };
201                }
202
203        }
204
205        /**
206         * Logger name part.
207         */
208        @CaseChanging
209        enum LoggerFormat {
210
211                /**
212                 * Full logger name. The default.
213                 */
214                @EnumAlias({ "default", "true" })
215                FULL {
216                        @Override
217                        public LogFormatter formatter() {
218                                return LogFormatter.builder().loggerName().build();
219                        }
220                },
221                /**
222                 * Only the last component of the name, like slf4j-simple's
223                 * <code>showShortLogName</code>: <code>App</code> for
224                 * <code>com.example.App</code>.
225                 */
226                SHORT {
227                        @Override
228                        public LogFormatter formatter() {
229                                return ShortLoggerNameFormatter.INSTANCE;
230                        }
231                },
232                /**
233                 * No logger name.
234                 */
235                @EnumAlias("false")
236                NONE {
237                        @Override
238                        public LogFormatter formatter() {
239                                return LogFormatter.noop();
240                        }
241                };
242
243                /**
244                 * The formatter for this choice.
245                 * @return formatter.
246                 */
247                public abstract LogFormatter formatter();
248
249                static LoggerFormat parse(String value) {
250                        return switch (value.strip().toLowerCase(java.util.Locale.ROOT)) {
251                                case "default", "true" -> FULL;
252                                case "false" -> NONE;
253                                default ->
254                                        io.jstach.rainbowgum.LogProperty.enumValue(LoggerFormat.class, value, "true", "false", "default");
255                        };
256                }
257
258        }
259
260        /**
261         * Key values part, written in braces after the logger name; see
262         * {@link KeyValuesWhenEmpty} for events with no key values. {@link #NONE} leaves out
263         * the key values and their braces.
264         */
265        @CaseChanging
266        enum KeyValuesFormat {
267
268                /**
269                 * Key values and their braces are left out.
270                 */
271                @EnumAlias("false")
272                NONE {
273                        @Override
274                        public LogFormatter formatter() {
275                                return LogFormatter.noop();
276                        }
277                },
278                /**
279                 * Percent encoded like a URI query:
280                 * <code>requestId=42&amp;user=Ada%20Lovelace</code>. The default.
281                 */
282                @EnumAlias({ "true", "default" })
283                PERCENT {
284                        @Override
285                        public LogFormatter formatter() {
286                                return new KeyValuesFormatterBuilder().format(KeyValuesFormatterBuilder.Format.PERCENT).build();
287                        }
288                },
289                /**
290                 * JSON: <code>{"requestId":"42","user":"Ada Lovelace"}</code>. Null values are
291                 * written as {@code null}.
292                 */
293                JSON {
294                        @Override
295                        public LogFormatter formatter() {
296                                return new KeyValuesFormatterBuilder().format(KeyValuesFormatterBuilder.Format.JSON).buildUnbraced();
297                        }
298                },
299                /**
300                 * JSON5, with valid identifier keys left unquoted:
301                 * <code>{requestId:"42",user:"Ada Lovelace"}</code>. Null values are written as
302                 * {@code null}.
303                 */
304                JSON5 {
305                        @Override
306                        public LogFormatter formatter() {
307                                return new KeyValuesFormatterBuilder().format(KeyValuesFormatterBuilder.Format.JSON5).buildUnbraced();
308                        }
309                };
310
311                /**
312                 * The formatter for this choice, without the enclosing braces supplied by TTLL.
313                 * @return formatter.
314                 */
315                public abstract LogFormatter formatter();
316
317                static KeyValuesFormat parse(String value) {
318                        return switch (value.strip().toLowerCase(java.util.Locale.ROOT)) {
319                                case "true", "default" -> PERCENT;
320                                case "false" -> NONE;
321                                default -> io.jstach.rainbowgum.LogProperty.enumValue(KeyValuesFormat.class, value, "true", "false",
322                                                "default");
323                        };
324                }
325
326        }
327
328        /**
329         * Whether the key values braces are written when an event has no key values.
330         */
331        @CaseChanging
332        enum KeyValuesWhenEmpty {
333
334                /**
335                 * Left out for percent encoding and JSON5; <code>{}</code> for JSON, since
336                 * whatever reads JSON key values expects an object on every line. The default.
337                 */
338                @EnumAlias("default")
339                AUTO,
340                /**
341                 * Left out. Not allowed with {@link KeyValuesFormat#JSON}, whose readers expect
342                 * an object on every line.
343                 */
344                @EnumAlias("false")
345                OMIT,
346                /**
347                 * Always written, <code>{}</code> when there are no key values.
348                 */
349                @EnumAlias("true")
350                SHOW;
351
352                static KeyValuesWhenEmpty parse(String value) {
353                        return switch (value.strip().toLowerCase(java.util.Locale.ROOT)) {
354                                case "default" -> AUTO;
355                                case "false" -> OMIT;
356                                case "true" -> SHOW;
357                                default -> io.jstach.rainbowgum.LogProperty.enumValue(KeyValuesWhenEmpty.class, value, "true", "false",
358                                                "default");
359                        };
360                }
361
362        }
363
364        /**
365         * Whether to color, see {@link ColorTheme} for which colors. Color detection uses
366         * {@link AnsiSupport#isAnsiSupported()}, which only knows about the console, so the
367         * default encoder of outputs that are not a console is {@link #OFF} unless set. As a
368         * property <code>true</code> and <code>false</code> are aliases for
369         * <code>default</code> and <code>off</code>.
370         */
371        @CaseChanging
372        enum ColorMode {
373
374                /**
375                 * No color.
376                 */
377                @EnumAlias("false")
378                OFF,
379                /**
380                 * The theme, or {@link ColorTheme#RAINBOWGUM} when no theme is set, if ANSI is
381                 * detected. The default.
382                 */
383                @EnumAlias("true")
384                DEFAULT,
385                /**
386                 * The theme if one is set and ANSI is detected; no color without a theme.
387                 */
388                DETECT,
389                /**
390                 * The theme, or {@link ColorTheme#RAINBOWGUM} when no theme is set, regardless of
391                 * ANSI detection.
392                 */
393                FORCE;
394
395                static ColorMode parse(String value) {
396                        return switch (value.strip().toLowerCase(java.util.Locale.ROOT)) {
397                                case "true" -> DEFAULT;
398                                case "false" -> OFF;
399                                default -> io.jstach.rainbowgum.LogProperty.enumValue(ColorMode.class, value, "true", "false");
400                        };
401                }
402
403        }
404
405        /**
406         * ANSI color theme, the colors used when {@link ColorMode} decides to color. The
407         * built in themes are the constants of this interface; others are made with
408         * {@link #builder(String)}. A theme put in the
409         * {@link io.jstach.rainbowgum.ServiceRegistry} as a {@code ColorTheme} under its name
410         * can be chosen by name with the theme property.
411         */
412        sealed interface ColorTheme permits PaletteColorTheme, NamedColorTheme {
413
414                /**
415                 * The Rainbow Gum colors, the same as the pattern encoder's default: cyan time,
416                 * faint thread and key values, level highlighted (error and warn bold red, info
417                 * bold blue) and magenta logger name. Used when no theme is set. Property value
418                 * <code>rainbowgum</code> or <code>default</code>.
419                 */
420                ColorTheme RAINBOWGUM = PaletteColorTheme.RAINBOWGUM;
421
422                /**
423                 * Spring Boot's console colors: faint time and thread, level colored by severity
424                 * (error red, warn yellow, the rest green) and cyan logger name. Property value
425                 * <code>spring</code>.
426                 */
427                ColorTheme SPRING = PaletteColorTheme.SPRING;
428
429                /**
430                 * Atom One Dark's colors in 24 bit color, so they show the same in any terminal
431                 * that supports it: cyan time, gray thread and key values, magenta logger name,
432                 * and level bold red, yellow, or blue (error, warn, info) with debug and trace
433                 * gray. Meant for a dark terminal background. Property value
434                 * <code>one_dark</code>.
435                 */
436                ColorTheme ONE_DARK = PaletteColorTheme.ONE_DARK;
437
438                /**
439                 * IntelliJ Darcula's colors in 24 bit color: blue time, gray thread and key
440                 * values, orange logger name, and level bold red, yellow, or green (error, warn,
441                 * info) with debug and trace gray. Meant for a dark terminal background. Property
442                 * value <code>darcula</code>.
443                 */
444                ColorTheme DARCULA = PaletteColorTheme.DARCULA;
445
446                /**
447                 * The theme's name, which is its property value.
448                 * @return name like <code>one_dark</code>.
449                 */
450                String name();
451
452                /**
453                 * Starts a theme. Parts not set are not colored.
454                 * @param name theme name: lowercase letters, digits, and underscores, starting
455                 * with a letter, and not the name of a built in theme or <code>default</code>.
456                 * @return builder.
457                 * @throws IllegalArgumentException if the name is not allowed.
458                 */
459                static Builder builder(String name) {
460                        return new Builder(PaletteColorTheme.requireThemeName(name));
461                }
462
463                /**
464                 * Builds a {@link ColorTheme}. Colors are ANSI SGR parameters, the part of an
465                 * escape sequence between <code>ESC[</code> and <code>m</code>, such as
466                 * <code>36</code> for cyan, <code>1;31</code> for bold red, or
467                 * <code>38;2;198;120;221</code> for a 24 bit color.
468                 */
469                final class Builder {
470
471                        private final String name;
472
473                        private String timestamp = "";
474
475                        private String thread = "";
476
477                        private String logger = "";
478
479                        private String keyValues = "";
480
481                        private String error = "";
482
483                        private String warn = "";
484
485                        private String info = "";
486
487                        private String debug = "";
488
489                        private String trace = "";
490
491                        private Builder(String name) {
492                                this.name = name;
493                        }
494
495                        /**
496                         * Starts from another theme's colors.
497                         * @param theme theme to copy.
498                         * @return this.
499                         */
500                        public Builder from(ColorTheme theme) {
501                                var p = PaletteColorTheme.paletteOf(theme);
502                                this.timestamp = p.timestamp();
503                                this.thread = p.thread();
504                                this.logger = p.logger();
505                                this.keyValues = p.keyValues();
506                                this.error = p.error();
507                                this.warn = p.warn();
508                                this.info = p.info();
509                                this.debug = p.debug();
510                                this.trace = p.trace();
511                                return this;
512                        }
513
514                        /**
515                         * Timestamp color.
516                         * @param color SGR parameters.
517                         * @return this.
518                         */
519                        public Builder timestamp(String color) {
520                                this.timestamp = color;
521                                return this;
522                        }
523
524                        /**
525                         * Thread color.
526                         * @param color SGR parameters.
527                         * @return this.
528                         */
529                        public Builder thread(String color) {
530                                this.thread = color;
531                                return this;
532                        }
533
534                        /**
535                         * Logger name color.
536                         * @param color SGR parameters.
537                         * @return this.
538                         */
539                        public Builder logger(String color) {
540                                this.logger = color;
541                                return this;
542                        }
543
544                        /**
545                         * Key values color.
546                         * @param color SGR parameters.
547                         * @return this.
548                         */
549                        public Builder keyValues(String color) {
550                                this.keyValues = color;
551                                return this;
552                        }
553
554                        /**
555                         * Color of a level.
556                         * @param level error, warning, info, debug, or trace.
557                         * @param color SGR parameters.
558                         * @return this.
559                         * @throws IllegalArgumentException for any other level.
560                         */
561                        public Builder level(System.Logger.Level level, String color) {
562                                switch (level) {
563                                        case ERROR -> this.error = color;
564                                        case WARNING -> this.warn = color;
565                                        case INFO -> this.info = color;
566                                        case DEBUG -> this.debug = color;
567                                        case TRACE -> this.trace = color;
568                                        default -> throw new IllegalArgumentException(
569                                                        "Level " + level + " has no color. Use error, warning, info, debug, or trace.");
570                                }
571                                return this;
572                        }
573
574                        /**
575                         * Builds the theme.
576                         * @return theme.
577                         */
578                        public ColorTheme build() {
579                                return new PaletteColorTheme(name,
580                                                new Palette(timestamp, thread, logger, keyValues, error, warn, info, debug, trace));
581                        }
582
583                }
584
585        }
586
587}