1 package org.djutils.logger;
2
3 import java.util.Collection;
4 import java.util.HashMap;
5 import java.util.HashSet;
6 import java.util.LinkedHashMap;
7 import java.util.Map;
8 import java.util.Objects;
9 import java.util.concurrent.ConcurrentHashMap;
10 import java.util.function.BooleanSupplier;
11 import java.util.function.Supplier;
12
13 import org.slf4j.LoggerFactory;
14 import org.slf4j.MDC;
15 import org.slf4j.spi.CallerBoundaryAware;
16 import org.slf4j.spi.LoggingEventBuilder;
17
18 import ch.qos.logback.classic.Level;
19 import ch.qos.logback.classic.Logger;
20 import ch.qos.logback.classic.LoggerContext;
21 import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
22 import ch.qos.logback.classic.spi.ILoggingEvent;
23 import ch.qos.logback.core.Appender;
24 import ch.qos.logback.core.rolling.RollingFileAppender;
25 import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
26
27 /**
28 * The CategoryLogger can log for specific Categories. The way to call the logger for messages that always need to be logged,
29 * such as an error with an exception is:
30 *
31 * <pre>
32 * CategoryLogger.always().error(exception, "Parameter {} did not initialize correctly", param1.toString());
33 * </pre>
34 *
35 * It is also possible to indicate the category / categories for the message, which will only be logged if at least one of the
36 * indicated categories is turned on with addLogCategory() or setLogCategories(), or if one of the added or set LogCategories is
37 * LogCategory.ALL:
38 *
39 * <pre>
40 * CategoryLogger.with(Cat.BASE).debug("Parameter {} initialized correctly", param1.toString());
41 * </pre>
42 * <p>
43 * Copyright (c) 2018-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
44 * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
45 * distributed under a three-clause BSD-style license, which can be found at
46 * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>.
47 * </p>
48 * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank"> Alexander Verbraeck</a>
49 */
50 @SuppressWarnings("checkstyle:needbraces")
51 public final class CategoryLogger
52 {
53 /** Has the CategoryLogger been initialized? */
54 private static volatile boolean initialized = false;
55
56 /** The LoggerContext to store settings, appenders, etc. */
57 private static final LoggerContext CTX = (LoggerContext) LoggerFactory.getILoggerFactory();
58
59 /** The default message format. */
60 public static final String DEFAULT_PATTERN = "%date{HH:mm:ss} %-5level %-6logger{0} %class.%method:%line - %msg%n";
61
62 /** The current message format. */
63 private static String defaultPattern = DEFAULT_PATTERN;
64
65 /** The default logging level. */
66 public static final Level DEFAULT_LEVEL = Level.INFO;
67
68 /** The current default logging level for new category loggers. */
69 private static Level defaultLevel = DEFAULT_LEVEL;
70
71 /** The levels and pattern used per LogCategory. */
72 private static final Map<LogCategory, CategoryConfig> CATEGORY_CFG = new LinkedHashMap<>();
73
74 /** The logger and appenders used per LogCategory. */
75 private static final Map<LogCategory, CategoryState> CATEGORY_STATE = new LinkedHashMap<>();
76
77 /** The factory for appenders, with an id for later removal. */
78 private static final Map<String, CategoryAppenderFactory> APPENDER_FACTORIES = new LinkedHashMap<>();
79
80 /** The delegate loggers per category. */
81 private static final Map<LogCategory, DelegateLogger> DELEGATES = new ConcurrentHashMap<>();
82
83 /** The log category for the always() method. */
84 public static final LogCategory CAT_ALWAYS = new LogCategory("ALWAYS");
85
86 /** The base DelegateLogger for the always() method. */
87 private static final DelegateLogger BASE_DELEGATE = new DelegateLogger(LoggerFactory.getLogger(CAT_ALWAYS.toString()));
88
89 /** The NO_LOGGER is the DelegateLogger that does not output anything after when() or with(). */
90 private static final DelegateLogger NO_LOGGER = new DelegateLogger(null, CategoryLogger.DelegateLogger.class, false);
91
92 /** */
93 private CategoryLogger()
94 {
95 // Utility class.
96 }
97
98 /* ---------------------------------------------------------------------------------------------------------------- */
99 /* -------------------------------------------- INTERNAL HELPER METHODS ------------------------------------------- */
100 /* ---------------------------------------------------------------------------------------------------------------- */
101
102 /**
103 * Check if the CategoryLogger has been initialized, and initialize the class when not.
104 */
105 private static void ensureInit()
106 {
107 if (initialized)
108 return;
109 synchronized (CategoryLogger.class)
110 {
111 // Bootstrap default category so .always() works immediately
112 initialized = true;
113 addLogCategory(CAT_ALWAYS);
114 setLogLevel(CAT_ALWAYS, Level.TRACE);
115 addLogCategory(LogCategory.ALL);
116 setLogLevel(LogCategory.ALL, defaultLevel);
117 }
118 }
119
120 /**
121 * Prepare a Logger for the provided log category, and give it at least a console appender.
122 * @param category the log category
123 * @param cfg the record with the configuration for this category
124 */
125 private static void wireCategoryLogger(final LogCategory category, final CategoryConfig cfg)
126 {
127 Logger logger = getOrCreateLogger(category);
128 logger.setAdditive(false);
129 logger.setLevel(cfg.level);
130 CategoryState st = new CategoryState(logger);
131 CATEGORY_STATE.put(category, st);
132
133 // Create per-category instances for every registered factory
134 for (CategoryAppenderFactory f : APPENDER_FACTORIES.values())
135 {
136 Appender<ILoggingEvent> app = f.create(f.id(), category, cfg.pattern, CTX);
137 app.start();
138 logger.addAppender(app);
139 st.appendersByFactoryId.put(f.id(), app);
140 }
141
142 // If no factories yet, at least wire a default console appender for visibility
143 if (APPENDER_FACTORIES.isEmpty())
144 {
145 CategoryAppenderFactory fallback = new ConsoleAppenderFactory("CONSOLE");
146 APPENDER_FACTORIES.putIfAbsent("CONSOLE", fallback);
147 Appender<ILoggingEvent> app = fallback.create("CONSOLE", category, cfg.pattern, CTX);
148 app.start();
149 logger.addAppender(app);
150 st.appendersByFactoryId.put("CONSOLE", app);
151 }
152 }
153
154 /**
155 * Give a logger for a log category its appenders.
156 * @param category the log category
157 */
158 private static void rebuildCategoryAppenders(final LogCategory category)
159 {
160 CategoryState st = CATEGORY_STATE.get(category);
161 CategoryConfig cfg = CATEGORY_CFG.get(category);
162 if (st == null || cfg == null)
163 return;
164
165 // detach & stop existing
166 st.appendersByFactoryId.forEach((id, app) ->
167 {
168 st.logger.detachAppender(app);
169 safeStop(app);
170 });
171 st.appendersByFactoryId.clear();
172
173 // build with new pattern
174 for (CategoryAppenderFactory f : APPENDER_FACTORIES.values())
175 {
176 Appender<ILoggingEvent> app = f.create(f.id(), category, cfg.pattern, CTX);
177 app.start();
178 st.logger.addAppender(app);
179 st.appendersByFactoryId.put(f.id(), app);
180 }
181 }
182
183 /**
184 * Get an existing logger based on its name, or create it when it does not exist yet.
185 * @param category the category with the name under which the logger is registered
186 * @return an existing logger based on its name, or create it when it does not exist yet
187 */
188 private static Logger getOrCreateLogger(final LogCategory category)
189 {
190 Logger logger = CTX.getLogger(category.toString());
191 return logger;
192 }
193
194 /**
195 * Stop an appender for when we change the configuration.
196 * @param appender the appender to stop
197 */
198 private static void safeStop(final Appender<ILoggingEvent> appender)
199 {
200 try
201 {
202 appender.stop();
203 }
204 catch (RuntimeException ignore)
205 {
206 }
207 }
208
209 /* ---------------------------------------------------------------------------------------------------------------- */
210 /* ------------------------------------------ CATEGORYLOGGER API METHODS ------------------------------------------ */
211 /* ---------------------------------------------------------------------------------------------------------------- */
212
213 /**
214 * Always log to the registered appenders, still observing the default log level.
215 * @return the DelegateLogger for method chaining, e.g., CategoryLogger.always().info("message");
216 */
217 public static DelegateLogger always()
218 {
219 ensureInit();
220 return BASE_DELEGATE;
221 }
222
223 /**
224 * Only log when the condition is true.
225 * @param condition the condition to check
226 * @return the DelegateLogger for method chaining, e.g., CategoryLogger.when(condition).info("message");
227 */
228 public static DelegateLogger when(final boolean condition)
229 {
230 ensureInit();
231 return condition ? BASE_DELEGATE : NO_LOGGER;
232 }
233
234 /**
235 * Only log when the boolean supplier provides a true value.
236 * @param booleanSupplier the supplier that provides true or false
237 * @return the DelegateLogger for method chaining, e.g., CategoryLogger.when(() -> condition()).info("message");
238 */
239 public static DelegateLogger when(final BooleanSupplier booleanSupplier)
240 {
241 return when(booleanSupplier.getAsBoolean());
242 }
243
244 /**
245 * Only log when the category has been registered in the CategoryLogger, and with the settings of that logger.
246 * @param category the category to check
247 * @return the DelegateLogger for method chaining, e.g., CategoryLogger.with(Cat.BASE).info("message");
248 */
249 public static DelegateLogger with(final LogCategory category)
250 {
251 ensureInit();
252 return DELEGATES.getOrDefault(category, NO_LOGGER);
253 }
254
255 /**
256 * Register a log category that can log with the CategoryLogger. Note that unregistered loggers for which you use with() do
257 * not log.
258 * @param category the log category to register.
259 */
260 public static synchronized void addLogCategory(final LogCategory category)
261 {
262 ensureInit();
263 if (CATEGORY_CFG.containsKey(category))
264 return;
265 CategoryConfig cfg = new CategoryConfig(defaultLevel, defaultPattern);
266 CATEGORY_CFG.put(category, cfg);
267 org.slf4j.Logger slf = LoggerFactory.getLogger(category.toString());
268 var delegate = new DelegateLogger(slf);
269 DELEGATES.put(category, delegate);
270 wireCategoryLogger(category, cfg);
271 }
272
273 /**
274 * Register a log category that can log with the CategoryLogger, with a 'boundary' class that indicates what to hide in the
275 * call stack. Note that unregistered loggers for which you use with() do not log.
276 * @param category the log category to register.
277 * @param callerBoundary class that defines what to hide in the call stack
278 */
279 public static synchronized void addLogCategory(final LogCategory category, final Class<?> callerBoundary)
280 {
281 ensureInit();
282 if (CATEGORY_CFG.containsKey(category))
283 return;
284 CategoryConfig cfg = new CategoryConfig(defaultLevel, defaultPattern);
285 CATEGORY_CFG.put(category, cfg);
286 org.slf4j.Logger slf = LoggerFactory.getLogger(category.toString());
287 var delegate = new DelegateLogger(slf, callerBoundary, true);
288 DELEGATES.put(category, delegate);
289 wireCategoryLogger(category, cfg);
290 }
291
292 /**
293 * Remove a log category from logging with the CategoryLogger. Note that unregistered loggers for which you use with() do
294 * not log.
295 * @param category the log category to unregister.
296 */
297 public static synchronized void removeLogCategory(final LogCategory category)
298 {
299 ensureInit();
300 CategoryState st = CATEGORY_STATE.remove(category);
301 CATEGORY_CFG.remove(category);
302 if (st != null)
303 {
304 // detach & stop this category's appenders
305 Logger logger = st.logger;
306 st.appendersByFactoryId.values().forEach(app ->
307 {
308 logger.detachAppender(app);
309 safeStop(app);
310 });
311 // silence the logger
312 logger.setLevel(Level.OFF);
313 logger.setAdditive(false);
314 }
315 DELEGATES.remove(category);
316 }
317
318 /**
319 * Return the registered appenders for the LogCategory.
320 * @param category the category to look up
321 * @return the appenders for the LogCategory
322 */
323 public static Collection<Appender<ILoggingEvent>> getAppenders(final LogCategory category)
324 {
325 ensureInit();
326 var st = CATEGORY_STATE.get(category);
327 return st == null ? new HashSet<>() : st.appendersByFactoryId.values();
328 }
329
330 /**
331 * Set the log category for a single log category.
332 * @param category the log category
333 * @param level the new log level
334 */
335 public static synchronized void setLogLevel(final LogCategory category, final Level level)
336 {
337 ensureInit();
338 addLogCategory(category); // create if missing
339 CATEGORY_CFG.get(category).level = level;
340 Logger logger = CATEGORY_STATE.get(category).logger;
341 logger.setLevel(level);
342 }
343
344 /**
345 * Set the log category for all log categories, except ALWAYS.
346 * @param level the new log level for all log categories, except ALWAYS
347 */
348 public static synchronized void setLogLevelAll(final Level level)
349 {
350 ensureInit();
351 defaultLevel = level;
352 for (var cat : CATEGORY_CFG.keySet())
353 {
354 if (cat.equals(CAT_ALWAYS))
355 continue;
356 CATEGORY_CFG.get(cat).level = level;
357 CATEGORY_STATE.get(cat).logger.setLevel(level);
358 }
359 }
360
361 /**
362 * Return the log level for a log category.
363 * @param category the log category
364 * @return the log level for the given category
365 */
366 public static Level getLogLevel(final LogCategory category)
367 {
368 ensureInit();
369 if (CATEGORY_CFG.containsKey(category))
370 return CATEGORY_CFG.get(category).level;
371 return DEFAULT_LEVEL;
372 }
373
374 /**
375 * Set the pattern for a single log category.
376 *
377 * <pre>
378 * %date{HH:mm:ss.SSS} Timestamp (default format shown; many options like ISO8601)
379 * %level / %-5level Log level (pad to fixed width with %-5level)
380 * %logger / %logger{0} Logger name (full or last component only; {n} = # of segments)
381 * %thread Thread name
382 * %msg / %message The actual log message
383 * %n Platform-specific newline
384 * %class / %class{1} Calling class (full or just last segment with {1})
385 * %method Calling method
386 * %line Source line number
387 * %file Source file name
388 * %caller Shortcut for class, method, file, and line in one
389 * %marker SLF4J marker (if present)
390 * %X{key} MDC value for given key
391 * %replace(p){r,e} Apply regex replacement to pattern part p
392 * %highlight(%msg) ANSI colored message (useful on console)
393 * </pre>
394 *
395 * Example:
396 *
397 * <pre>
398 * "%date{HH:mm:ss} %-5level %-6logger{0} %class{1}.%method:%line - %msg%n"
399 * → 12:34:56 INFO http HttpHandler.handle:42 - GET /users -> 200
400 * </pre>
401 *
402 * @param category the log category
403 * @param pattern the new pattern
404 */
405 public static synchronized void setPattern(final LogCategory category, final String pattern)
406 {
407 ensureInit();
408 addLogCategory(category); // create if missing
409 CATEGORY_CFG.get(category).pattern = Objects.requireNonNull(pattern);
410 // Rebuild this category's appenders with the new pattern
411 rebuildCategoryAppenders(category);
412 }
413
414 /**
415 * Set the pattern for a all log categories.
416 *
417 * <pre>
418 * %date{HH:mm:ss.SSS} Timestamp (default format shown; many options like ISO8601)
419 * %level / %-5level Log level (pad to fixed width with %-5level)
420 * %logger / %logger{0} Logger name (full or last component only; {n} = # of segments)
421 * %thread Thread name
422 * %msg / %message The actual log message
423 * %n Platform-specific newline
424 * %class / %class{1} Calling class (full or just last segment with {1})
425 * %method Calling method
426 * %line Source line number
427 * %file Source file name
428 * %caller Shortcut for class, method, file, and line in one
429 * %marker SLF4J marker (if present)
430 * %X{key} MDC value for given key
431 * %replace(p){r,e} Apply regex replacement to pattern part p
432 * %highlight(%msg) ANSI colored message (useful on console)
433 * </pre>
434 *
435 * Example:
436 *
437 * <pre>
438 * "%date{HH:mm:ss} %-5level %-6logger{0} %class{1}.%method:%line - %msg%n"
439 * → 12:34:56 INFO http HttpHandler.handle:42 - GET /users -> 200
440 * </pre>
441 *
442 * @param pattern the new pattern
443 */
444 public static synchronized void setPatternAll(final String pattern)
445 {
446 ensureInit();
447 defaultPattern = Objects.requireNonNull(pattern);
448 CATEGORY_CFG.replaceAll((c, cfg) ->
449 {
450 cfg.pattern = pattern;
451 return cfg;
452 });
453 CATEGORY_CFG.keySet().forEach(CategoryLogger::rebuildCategoryAppenders);
454 }
455
456 /**
457 * Return the pattern for a log category.
458 * @param category the log category
459 * @return the pattern for the given category
460 */
461 public static String getPattern(final LogCategory category)
462 {
463 ensureInit();
464 if (CATEGORY_CFG.containsKey(category))
465 return CATEGORY_CFG.get(category).pattern;
466 return DEFAULT_PATTERN;
467 }
468
469 /**
470 * Register a global appender factory. A separate Appender instance will be created for each registered category.
471 * @param id the id to register the appender on, so it can be removed later
472 * @param factory the factory that creates the appender with a create(..) method
473 */
474 public static synchronized void addAppender(final String id, final CategoryAppenderFactory factory)
475 {
476 ensureInit();
477 if (APPENDER_FACTORIES.containsKey(id))
478 throw new IllegalArgumentException("factory id exists: " + id);
479 APPENDER_FACTORIES.put(id, factory);
480 // Create & attach instances for all existing categories
481 for (var e : CATEGORY_CFG.entrySet())
482 {
483 LogCategory cat = e.getKey();
484 CategoryConfig cfg = e.getValue();
485 CategoryState st = CATEGORY_STATE.get(cat);
486 Appender<ILoggingEvent> app = factory.create(id, cat, cfg.pattern, CTX);
487 app.start();
488 st.logger.addAppender(app);
489 st.appendersByFactoryId.put(id, app);
490 }
491 }
492
493 /**
494 * Remove a global appender factory; detaches and stops per-category instances.
495 * @param id the id the appender was registered with
496 */
497 public static synchronized void removeAppender(final String id)
498 {
499 ensureInit();
500 if (APPENDER_FACTORIES.remove(id) == null)
501 return;
502 for (CategoryState st : CATEGORY_STATE.values())
503 {
504 Appender<ILoggingEvent> app = st.appendersByFactoryId.remove(id);
505 if (app != null)
506 {
507 st.logger.detachAppender(app);
508 safeStop(app);
509 }
510 }
511 }
512
513 /**
514 * Add a find-replace formatter on the delegate logger for this category.
515 * @param category the category to use
516 * @param find the string to find in the pattern
517 * @param replaceSupplier the supplier for the replacement string
518 */
519 public static synchronized void addFormatter(final LogCategory category, final String find,
520 final Supplier<String> replaceSupplier)
521 {
522 ensureInit();
523 DELEGATES.get(category).addFormatter(find, replaceSupplier);
524 }
525
526 /**
527 * Remove a find-replace formatter on the delegate logger for this category.
528 * @param category the category to use
529 * @param find the string to find in the pattern
530 */
531 public static synchronized void removeFormatter(final LogCategory category, final String find)
532 {
533 ensureInit();
534 DELEGATES.get(category).removeFormatter(find);
535 }
536
537 /**
538 * Add a callback method for the delegate logger for this category.
539 * @param category the category to use
540 * @param callback the runnable that is called just before logging
541 */
542 public static synchronized void setCallback(final LogCategory category, final Runnable callback)
543 {
544 ensureInit();
545 DELEGATES.get(category).setCallback(callback);
546 }
547
548 /**
549 * Remove the callback method for the delegate logger for this category.
550 * @param category the category to use
551 */
552 public static synchronized void removeCallback(final LogCategory category)
553 {
554 ensureInit();
555 DELEGATES.get(category).setCallback(null);
556 }
557
558 /* ---------------------------------------------------------------------------------------------------------------- */
559 /* ------------------------------------------- HELPER CLASSES AND RECORDS ----------------------------------------- */
560 /* ---------------------------------------------------------------------------------------------------------------- */
561
562 /**
563 * Class to store the logging level and pattern for a log category.
564 */
565 private static final class CategoryConfig
566 {
567 /** the logging level for a category. */
568 private Level level;
569
570 /** the String pattern to use for a category. */
571 private String pattern;
572
573 /**
574 * Create a record for storing the logging level and pattern for a log category.
575 * @param level the logging level for a category
576 * @param pattern the pattern for a category
577 */
578 private CategoryConfig(final Level level, final String pattern)
579 {
580 this.level = Objects.requireNonNull(level);
581 this.pattern = Objects.requireNonNull(pattern);
582 }
583 }
584
585 /**
586 * Class to store the logger and the appenders for a log category.
587 */
588 private static final class CategoryState
589 {
590 /** The logger to use. */
591 private final Logger logger;
592
593 /** The appenders for this log category. */
594 private final Map<String, Appender<ILoggingEvent>> appendersByFactoryId = new HashMap<>();
595
596 /**
597 * Instantiate a category state.
598 * @param logger the logger to use for the category that is connected to this state
599 */
600 CategoryState(final Logger logger)
601 {
602 this.logger = logger;
603 }
604 }
605
606 /* ---------------------------------------------------------------------------------------------------------------- */
607 /* ----------------------------------------------- APPENDER FACTORIES --------------------------------------------- */
608 /* ---------------------------------------------------------------------------------------------------------------- */
609
610 /**
611 * The interface for the appender instance per category. The id is used for later removal.
612 */
613 public interface CategoryAppenderFactory
614 {
615 /**
616 * Return the id to be used for later removal.
617 * @return the id to be used for later removal
618 */
619 String id();
620
621 /**
622 * Create an appender instance for a category.
623 * @param id the id to be used for later removal
624 * @param category the logging category
625 * @param messageFormat the pattern to use for printing the log message
626 * @param ctx the context to use
627 * @return an appender with the above features
628 */
629 Appender<ILoggingEvent> create(String id, LogCategory category, String messageFormat, LoggerContext ctx);
630 }
631
632 /** Console appender factory (uses the category's pattern). */
633 public static final class ConsoleAppenderFactory implements CategoryAppenderFactory
634 {
635 /** the id to be used for later removal. */
636 private final String id;
637
638 /**
639 * Instantiate the factory for the console appender.
640 * @param id the id to be used for later removal
641 */
642 public ConsoleAppenderFactory(final String id)
643 {
644 this.id = id;
645 }
646
647 @Override
648 public String id()
649 {
650 return this.id;
651 }
652
653 @Override
654 @SuppressWarnings("checkstyle:hiddenfield")
655 public Appender<ILoggingEvent> create(final String id, final LogCategory category, final String messageFormat,
656 final LoggerContext ctx)
657 {
658 PatternLayoutEncoder enc = new PatternLayoutEncoder();
659 enc.setContext(ctx);
660 enc.setPattern(messageFormat);
661 enc.start();
662
663 ch.qos.logback.core.ConsoleAppender<ILoggingEvent> app = new ch.qos.logback.core.ConsoleAppender<>();
664 app.setName(id + "@" + category.toString());
665 app.setContext(ctx);
666 app.setEncoder(enc);
667 return app;
668 }
669 }
670
671 /** Rolling file appender factory (per-category file pattern). */
672 public static final class RollingFileAppenderFactory implements CategoryAppenderFactory
673 {
674 /** the id to be used for later removal. */
675 private final String id;
676
677 /** The filename pattern, e.g. "logs/%s-%d{yyyy-MM-dd}.log.gz" (use %s for category). */
678 private final String fileNamePattern;
679
680 /**
681 * Instantiate the factory for the rolling file appender.
682 * @param id the id to be used for later removal
683 * @param fileNamePattern the filename pattern, e.g. "logs/%s-%d{yyyy-MM-dd}.log.gz" (use %s for category)
684 */
685 public RollingFileAppenderFactory(final String id, final String fileNamePattern)
686 {
687 this.id = id;
688 this.fileNamePattern = fileNamePattern;
689 }
690
691 @Override
692 public String id()
693 {
694 return this.id;
695 }
696
697 @Override
698 @SuppressWarnings("checkstyle:hiddenfield")
699 public Appender<ILoggingEvent> create(final String id, final LogCategory category, final String messageFormat,
700 final LoggerContext ctx)
701 {
702 PatternLayoutEncoder enc = new PatternLayoutEncoder();
703 enc.setContext(ctx);
704 enc.setPattern(messageFormat);
705 enc.start();
706
707 RollingFileAppender<ILoggingEvent> file = new RollingFileAppender<>();
708 file.setName(id + "@" + category.toString());
709 file.setContext(ctx);
710 file.setEncoder(enc);
711
712 final TimeBasedRollingPolicy<ILoggingEvent> policy = new TimeBasedRollingPolicy<>();
713 policy.setContext(ctx);
714 policy.setParent(file);
715
716 // IMPORTANT: replace only the category placeholder; keep %d{...} intact for Logback
717 final String effectivePattern = this.fileNamePattern.replace("%s", category.toString());
718 policy.setFileNamePattern(effectivePattern);
719
720 policy.start();
721 file.setRollingPolicy(policy);
722
723 return file; // caller will start() it
724 }
725 }
726
727 /* ---------------------------------------------------------------------------------------------------------------- */
728 /* ------------------------------------------------ DELEGATE LOGGER ----------------------------------------------- */
729 /* ---------------------------------------------------------------------------------------------------------------- */
730
731 /**
732 * DelegateLogger class that takes care of actually logging the message and/or exception. <br>
733 * <p>
734 * Copyright (c) 2003-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights
735 * reserved.<br>
736 * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
737 * </p>
738 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
739 */
740 public static final class DelegateLogger
741 {
742 /** The logger facade from slf4j. */
743 private final org.slf4j.Logger logger;
744
745 /** The fully-qualified class name that defines the class to hide in the call stack. */
746 private final String boundaryFqcn;
747
748 /** Whether we should log or not (the NO_LOGGER does not log). */
749 private final boolean log;
750
751 /** the formatters with find-replace strings. */
752 private final Map<String, Supplier<String>> formatters = new HashMap<>();
753
754 /** a callback method that is called just before logging for e.g., MDC. */
755 private Runnable callback = null;
756
757 /**
758 * Create a DelegateLogger with a class that indicates what to hide in the call stack.
759 * @param slf4jLogger the logger facade from slf4j, can be null in case no logging is done
760 * @param callerBoundary class that defines what to hide in the call stack
761 * @param log whether we should log or not (the NO_LOGGER does not log)
762 */
763 protected DelegateLogger(final org.slf4j.Logger slf4jLogger, final Class<?> callerBoundary, final boolean log)
764 {
765 this.logger = slf4jLogger;
766 this.boundaryFqcn = Objects.requireNonNull(callerBoundary).getName();
767 this.log = log;
768 }
769
770 /**
771 * Create a DelegateLogger with DelegateLogger as the only class to hide from the call stack.
772 * @param slf4jLogger the logger facade from slf4j, can be null in case no logging is done
773 */
774 protected DelegateLogger(final org.slf4j.Logger slf4jLogger)
775 {
776 this(slf4jLogger, CategoryLogger.DelegateLogger.class, true);
777 }
778
779 /**
780 * The conditional filter that will result in the usage of a DelegateLogger.
781 * @param condition the condition that should be evaluated
782 * @return the logger that further processes logging (DelegateLogger)
783 */
784 public DelegateLogger when(final boolean condition)
785 {
786 if (condition)
787 return this;
788 return CategoryLogger.NO_LOGGER;
789 }
790
791 /**
792 * The conditional filter that will result in the usage of a DelegateLogger.
793 * @param supplier the function evaluating the condition
794 * @return the logger that further processes logging (DelegateLogger)
795 */
796 public DelegateLogger when(final BooleanSupplier supplier)
797 {
798 if (supplier.getAsBoolean())
799 return this;
800 return CategoryLogger.NO_LOGGER;
801 }
802
803 /**
804 * helper method to return the LoggingEventBuilder WITH a boundary to leave out part of the call stack.
805 * @param leb The original LoggingEventBuilder
806 * @return the boundary-based LoggerEventBuilder
807 */
808 private LoggingEventBuilder withBoundary(final LoggingEventBuilder leb)
809 {
810 if (leb instanceof CallerBoundaryAware cba)
811 cba.setCallerBoundary(this.boundaryFqcn);
812 return leb;
813 }
814
815 /**
816 * Add a find-replace formatter on the delegate logger for this delegate logger.
817 * @param find the string to find in the pattern
818 * @param replaceSupplier the supplier for the replacement string
819 */
820 public void addFormatter(final String find, final Supplier<String> replaceSupplier)
821 {
822 this.formatters.put(find, replaceSupplier);
823 }
824
825 /**
826 * Remove a find-replace formatter on the delegate logger for this delegate logger.
827 * @param find the string to find in the pattern
828 */
829 public void removeFormatter(final String find)
830 {
831 this.formatters.remove(find);
832 }
833
834 /**
835 * Apply the formatter suppliers to the mdc context.
836 */
837 protected void mdc()
838 {
839 MDC.clear();
840 for (var find : this.formatters.keySet())
841 {
842 MDC.put(find, this.formatters.get(find).get());
843 }
844 }
845
846 /**
847 * Set the callback.
848 * @param callback the callback runnable
849 */
850 public void setCallback(final Runnable callback)
851 {
852 this.callback = callback;
853 }
854
855 /**
856 * Carry out the callback.
857 */
858 protected void doCallback()
859 {
860 if (this.callback != null)
861 {
862 this.callback.run();
863 }
864 }
865
866 /* ---------------------------------------------------------------------------------------------------------------- */
867 /* ----------------------------------------------------- TRACE ---------------------------------------------------- */
868 /* ---------------------------------------------------------------------------------------------------------------- */
869
870 /**
871 * Create a debug log entry that will be output if TRACE is enabled for this DelegateLogger.
872 * @param object the result of the <code>toString()</code> method of <code>object</code> will be logged
873 */
874 public void trace(final Object object)
875 {
876 if (!this.log || !this.logger.isTraceEnabled())
877 return;
878 doCallback();
879 mdc();
880 withBoundary(this.logger.atTrace()).log(object.toString());
881 }
882
883 /**
884 * Create a trace log entry that will be output if TRACE is enabled for this DelegateLogger.
885 * @param message the message to log
886 */
887 public void trace(final String message)
888 {
889 if (!this.log || !this.logger.isTraceEnabled())
890 return;
891 doCallback();
892 mdc();
893 withBoundary(this.logger.atTrace()).log(message);
894 }
895
896 /**
897 * Create a trace log entry that will be output if TRACE is enabled for this DelegateLogger.
898 * @param message the message to be logged, where {} entries will be replaced by arguments
899 * @param arguments the arguments to substitute for the {} entries in the message string
900 */
901 public void trace(final String message, final Object... arguments)
902 {
903 if (!this.log || !this.logger.isTraceEnabled())
904 return;
905 doCallback();
906 mdc();
907 withBoundary(this.logger.atTrace()).log(message, arguments);
908 }
909
910 /**
911 * Create a trace log entry that will be output if TRACE is enabled for this DelegateLogger.
912 * @param throwable the throwable to log
913 */
914 public void trace(final Throwable throwable)
915 {
916 if (!this.log || !this.logger.isTraceEnabled())
917 return;
918 doCallback();
919 mdc();
920 withBoundary(this.logger.atTrace()).setCause(throwable)
921 .log((() -> throwable.getClass().getSimpleName() + "(" + Objects.requireNonNullElse(throwable.getMessage(), "")
922 + ")"));
923 }
924
925 /**
926 * Create a trace log entry that will be output if TRACE is enabled for this DelegateLogger.
927 * @param throwable the throwable to log
928 * @param message the message to log
929 */
930 public void trace(final Throwable throwable, final String message)
931 {
932 if (!this.log || !this.logger.isTraceEnabled())
933 return;
934 doCallback();
935 mdc();
936 withBoundary(this.logger.atTrace()).setCause(throwable).log(message);
937 }
938
939 /**
940 * Create a trace log entry that will be output if TRACE is enabled for this DelegateLogger.
941 * @param throwable the exception to log
942 * @param message the message to log, where {} entries will be replaced by arguments
943 * @param arguments the arguments to substitute for the {} entries in the message string
944 */
945 public void trace(final Throwable throwable, final String message, final Object... arguments)
946 {
947 if (!this.log || !this.logger.isTraceEnabled())
948 return;
949 doCallback();
950 mdc();
951 withBoundary(this.logger.atTrace()).setCause(throwable).log(message, arguments);
952 }
953
954 /* ---------------------------------------------------------------------------------------------------------------- */
955 /* ----------------------------------------------------- DEBUG ---------------------------------------------------- */
956 /* ---------------------------------------------------------------------------------------------------------------- */
957
958 /**
959 * Create a debug log entry that will be output if DEBUG is enabled for this DelegateLogger.
960 * @param object the result of the <code>toString()</code> method of <code>object</code> will be logged
961 */
962 public void debug(final Object object)
963 {
964 if (!this.log || !this.logger.isDebugEnabled())
965 return;
966 doCallback();
967 mdc();
968 withBoundary(this.logger.atDebug()).log(object.toString());
969 }
970
971 /**
972 * Create a debug log entry that will be output if DEBUG is enabled for this DelegateLogger.
973 * @param message the message to log
974 */
975 public void debug(final String message)
976 {
977 if (!this.log || !this.logger.isDebugEnabled())
978 return;
979 doCallback();
980 mdc();
981 withBoundary(this.logger.atDebug()).log(message);
982 }
983
984 /**
985 * Create a debug log entry that will be output if DEBUG is enabled for this DelegateLogger.
986 * @param message the message to be logged, where {} entries will be replaced by arguments
987 * @param arguments the arguments to substitute for the {} entries in the message string
988 */
989 public void debug(final String message, final Object... arguments)
990 {
991 if (!this.log || !this.logger.isDebugEnabled())
992 return;
993 doCallback();
994 mdc();
995 withBoundary(this.logger.atDebug()).log(message, arguments);
996 }
997
998 /**
999 * Create a debug log entry that will be output if DEBUG is enabled for this DelegateLogger.
1000 * @param throwable the throwable to log
1001 */
1002 public void debug(final Throwable throwable)
1003 {
1004 if (!this.log || !this.logger.isDebugEnabled())
1005 return;
1006 doCallback();
1007 mdc();
1008 withBoundary(this.logger.atDebug()).setCause(throwable)
1009 .log((() -> throwable.getClass().getSimpleName() + "(" + Objects.requireNonNullElse(throwable.getMessage(), "")
1010 + ")"));
1011 }
1012
1013 /**
1014 * Create a debug log entry that will be output if DEBUG is enabled for this DelegateLogger.
1015 * @param throwable the throwable to log
1016 * @param message the message to log
1017 */
1018 public void debug(final Throwable throwable, final String message)
1019 {
1020 if (!this.log || !this.logger.isDebugEnabled())
1021 return;
1022 doCallback();
1023 mdc();
1024 withBoundary(this.logger.atDebug()).setCause(throwable).log(message);
1025 }
1026
1027 /**
1028 * Create a debug log entry that will be output if DEBUG is enabled for this DelegateLogger.
1029 * @param throwable the exception to log
1030 * @param message the message to log, where {} entries will be replaced by arguments
1031 * @param arguments the arguments to substitute for the {} entries in the message string
1032 */
1033 public void debug(final Throwable throwable, final String message, final Object... arguments)
1034 {
1035 if (!this.log || !this.logger.isDebugEnabled())
1036 return;
1037 doCallback();
1038 mdc();
1039 withBoundary(this.logger.atDebug()).setCause(throwable).log(message, arguments);
1040 }
1041
1042 /* ---------------------------------------------------------------------------------------------------------------- */
1043 /* ----------------------------------------------------- INFO ----------------------------------------------------- */
1044 /* ---------------------------------------------------------------------------------------------------------------- */
1045
1046 /**
1047 * Create a info log entry that will be output if INFO is enabled for this DelegateLogger.
1048 * @param object the result of the <code>toString()</code> method of <code>object</code> will be logged
1049 */
1050 public void info(final Object object)
1051 {
1052 if (!this.log || !this.logger.isInfoEnabled())
1053 return;
1054 doCallback();
1055 mdc();
1056 withBoundary(this.logger.atInfo()).log(object.toString());
1057 }
1058
1059 /**
1060 * Create a info log entry that will be output if INFO is enabled for this DelegateLogger.
1061 * @param message the message to log
1062 */
1063 public void info(final String message)
1064 {
1065 if (!this.log || !this.logger.isInfoEnabled())
1066 return;
1067 doCallback();
1068 mdc();
1069 withBoundary(this.logger.atInfo()).log(message);
1070 }
1071
1072 /**
1073 * Create a info log entry that will be output if INFO is enabled for this DelegateLogger.
1074 * @param message the message to be logged, where {} entries will be replaced by arguments
1075 * @param arguments the arguments to substitute for the {} entries in the message string
1076 */
1077 public void info(final String message, final Object... arguments)
1078 {
1079 if (!this.log || !this.logger.isInfoEnabled())
1080 return;
1081 doCallback();
1082 mdc();
1083 withBoundary(this.logger.atInfo()).log(message, arguments);
1084 }
1085
1086 /**
1087 * Create a info log entry that will be output if INFO is enabled for this DelegateLogger.
1088 * @param throwable the throwable to log
1089 */
1090 public void info(final Throwable throwable)
1091 {
1092 if (!this.log || !this.logger.isInfoEnabled())
1093 return;
1094 doCallback();
1095 mdc();
1096 withBoundary(this.logger.atInfo()).setCause(throwable)
1097 .log((() -> throwable.getClass().getSimpleName() + "(" + Objects.requireNonNullElse(throwable.getMessage(), "")
1098 + ")"));
1099 }
1100
1101 /**
1102 * Create a info log entry that will be output if INFO is enabled for this DelegateLogger.
1103 * @param throwable the throwable to log
1104 * @param message the message to log
1105 */
1106 public void info(final Throwable throwable, final String message)
1107 {
1108 if (!this.log || !this.logger.isInfoEnabled())
1109 return;
1110 doCallback();
1111 mdc();
1112 withBoundary(this.logger.atInfo()).setCause(throwable).log(message);
1113 }
1114
1115 /**
1116 * Create a info log entry that will be output if INFO is enabled for this DelegateLogger.
1117 * @param throwable the exception to log
1118 * @param message the message to log, where {} entries will be replaced by arguments
1119 * @param arguments the arguments to substitute for the {} entries in the message string
1120 */
1121 public void info(final Throwable throwable, final String message, final Object... arguments)
1122 {
1123 if (!this.log || !this.logger.isInfoEnabled())
1124 return;
1125 doCallback();
1126 mdc();
1127 withBoundary(this.logger.atInfo()).setCause(throwable).log(message, arguments);
1128 }
1129
1130 /* ---------------------------------------------------------------------------------------------------------------- */
1131 /* ----------------------------------------------------- WARN ----------------------------------------------------- */
1132 /* ---------------------------------------------------------------------------------------------------------------- */
1133
1134 /**
1135 * Create a warn log entry that will be output if WARN is enabled for this DelegateLogger.
1136 * @param object the result of the <code>toString()</code> method of <code>object</code> will be logged
1137 */
1138 public void warn(final Object object)
1139 {
1140 if (!this.log || !this.logger.isWarnEnabled())
1141 return;
1142 doCallback();
1143 mdc();
1144 withBoundary(this.logger.atWarn()).log(object.toString());
1145 }
1146
1147 /**
1148 * Create a warn log entry that will be output if WARN is enabled for this DelegateLogger.
1149 * @param message the message to log
1150 */
1151 public void warn(final String message)
1152 {
1153 if (!this.log || !this.logger.isWarnEnabled())
1154 return;
1155 doCallback();
1156 mdc();
1157 withBoundary(this.logger.atWarn()).log(message);
1158 }
1159
1160 /**
1161 * Create a warn log entry that will be output if WARN is enabled for this DelegateLogger.
1162 * @param message the message to be logged, where {} entries will be replaced by arguments
1163 * @param arguments the arguments to substitute for the {} entries in the message string
1164 */
1165 public void warn(final String message, final Object... arguments)
1166 {
1167 if (!this.log || !this.logger.isWarnEnabled())
1168 return;
1169 doCallback();
1170 mdc();
1171 withBoundary(this.logger.atWarn()).log(message, arguments);
1172 }
1173
1174 /**
1175 * Create a warn log entry that will be output if WARN is enabled for this DelegateLogger.
1176 * @param throwable the throwable to log
1177 */
1178 public void warn(final Throwable throwable)
1179 {
1180 if (!this.log || !this.logger.isWarnEnabled())
1181 return;
1182 doCallback();
1183 mdc();
1184 withBoundary(this.logger.atWarn()).setCause(throwable)
1185 .log((() -> throwable.getClass().getSimpleName() + "(" + Objects.requireNonNullElse(throwable.getMessage(), "")
1186 + ")"));
1187 }
1188
1189 /**
1190 * Create a warn log entry that will be output if WARN is enabled for this DelegateLogger.
1191 * @param throwable the throwable to log
1192 * @param message the message to log
1193 */
1194 public void warn(final Throwable throwable, final String message)
1195 {
1196 if (!this.log || !this.logger.isWarnEnabled())
1197 return;
1198 doCallback();
1199 mdc();
1200 withBoundary(this.logger.atWarn()).setCause(throwable).log(message);
1201 }
1202
1203 /**
1204 * Create a warn log entry that will be output if WARN is enabled for this DelegateLogger.
1205 * @param throwable the exception to log
1206 * @param message the message to log, where {} entries will be replaced by arguments
1207 * @param arguments the arguments to substitute for the {} entries in the message string
1208 */
1209 public void warn(final Throwable throwable, final String message, final Object... arguments)
1210 {
1211 if (!this.log || !this.logger.isWarnEnabled())
1212 return;
1213 doCallback();
1214 mdc();
1215 withBoundary(this.logger.atWarn()).setCause(throwable).log(message, arguments);
1216 }
1217
1218 /* ---------------------------------------------------------------------------------------------------------------- */
1219 /* ----------------------------------------------------- ERROR ---------------------------------------------------- */
1220 /* ---------------------------------------------------------------------------------------------------------------- */
1221
1222 /**
1223 * Create a error log entry that will be output if ERROR is enabled for this DelegateLogger.
1224 * @param object the result of the <code>toString()</code> method of <code>object</code> will be logged
1225 */
1226 public void error(final Object object)
1227 {
1228 if (!this.log || !this.logger.isErrorEnabled())
1229 return;
1230 doCallback();
1231 mdc();
1232 withBoundary(this.logger.atError()).log(object.toString());
1233 }
1234
1235 /**
1236 * Create a error log entry that will be output if ERROR is enabled for this DelegateLogger.
1237 * @param message the message to log
1238 */
1239 public void error(final String message)
1240 {
1241 if (!this.log || !this.logger.isErrorEnabled())
1242 return;
1243 doCallback();
1244 mdc();
1245 withBoundary(this.logger.atError()).log(message);
1246 }
1247
1248 /**
1249 * Create a error log entry that will be output if ERROR is enabled for this DelegateLogger.
1250 * @param message the message to be logged, where {} entries will be replaced by arguments
1251 * @param arguments the arguments to substitute for the {} entries in the message string
1252 */
1253 public void error(final String message, final Object... arguments)
1254 {
1255 if (!this.log || !this.logger.isErrorEnabled())
1256 return;
1257 doCallback();
1258 mdc();
1259 withBoundary(this.logger.atError()).log(message, arguments);
1260 }
1261
1262 /**
1263 * Create a error log entry that will be output if ERROR is enabled for this DelegateLogger.
1264 * @param throwable the throwable to log
1265 */
1266 public void error(final Throwable throwable)
1267 {
1268 if (!this.log || !this.logger.isErrorEnabled())
1269 return;
1270 doCallback();
1271 mdc();
1272 withBoundary(this.logger.atError()).setCause(throwable)
1273 .log((() -> throwable.getClass().getSimpleName() + "(" + Objects.requireNonNullElse(throwable.getMessage(), "")
1274 + ")"));
1275 }
1276
1277 /**
1278 * Create a error log entry that will be output if ERROR is enabled for this DelegateLogger.
1279 * @param throwable the throwable to log
1280 * @param message the message to log
1281 */
1282 public void error(final Throwable throwable, final String message)
1283 {
1284 if (!this.log || !this.logger.isErrorEnabled())
1285 return;
1286 doCallback();
1287 mdc();
1288 withBoundary(this.logger.atError()).setCause(throwable).log(message);
1289 }
1290
1291 /**
1292 * Create a error log entry that will be output if ERROR is enabled for this DelegateLogger.
1293 * @param throwable the exception to log
1294 * @param message the message to log, where {} entries will be replaced by arguments
1295 * @param arguments the arguments to substitute for the {} entries in the message string
1296 */
1297 public void error(final Throwable throwable, final String message, final Object... arguments)
1298 {
1299 if (!this.log || !this.logger.isErrorEnabled())
1300 return;
1301 doCallback();
1302 mdc();
1303 withBoundary(this.logger.atError()).setCause(throwable).log(message, arguments);
1304 }
1305 }
1306
1307 }