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&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}