View Javadoc
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 }