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