CategoryLogger.java

package org.djutils.logger;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BooleanSupplier;

import org.slf4j.LoggerFactory;
import org.slf4j.spi.CallerBoundaryAware;
import org.slf4j.spi.LoggingEventBuilder;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;

/**
 * The CategoryLogger can log for specific Categories. The way to call the logger for messages that always need to be logged,
 * such as an error with an exception is:
 * 
 * <pre>
 * CategoryLogger.always().error(exception, "Parameter {} did not initialize correctly", param1.toString());
 * </pre>
 * 
 * It is also possible to indicate the category / categories for the message, which will only be logged if at least one of the
 * indicated categories is turned on with addLogCategory() or setLogCategories(), or if one of the added or set LogCategories is
 * LogCategory.ALL:
 * 
 * <pre>
 * CategoryLogger.filter(Cat.BASE).debug("Parameter {} initialized correctly", param1.toString());
 * </pre>
 * <p>
 * Copyright (c) 2018-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
 * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
 * distributed under a three-clause BSD-style license, which can be found at
 * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>.
 * </p>
 * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank"> Alexander Verbraeck</a>
 */
@SuppressWarnings("checkstyle:needbraces")
public final class CategoryLogger
{
    /** Has the CategoryLogger been initialized? */
    private static volatile boolean initialized = false;

    /** The LoggerContext to store settings, appenders, etc. */
    private static final LoggerContext CTX = (LoggerContext) LoggerFactory.getILoggerFactory();

    /** The default message format. */
    public static final String DEFAULT_PATTERN = "%date{HH:mm:ss} %-5level %-6logger{0} %class.%method:%line - %msg%n";

    /** The current message format. */
    private static String defaultPattern = DEFAULT_PATTERN;

    /** The current default logging level for new category loggers. */
    private static Level defaultLevel = Level.INFO;

    /** The levels and pattern used per LogCategory. */
    private static final Map<LogCategory, CategoryConfig> CATEGORY_CFG = new LinkedHashMap<>();

    /** The logger and appenders used per LogCategory. */
    private static final Map<LogCategory, CategoryState> CATEGORY_STATE = new LinkedHashMap<>();

    /** The factory for appenders, with an id for later removal. */
    private static final Map<String, CategoryAppenderFactory> APPENDER_FACTORIES = new LinkedHashMap<>();

    /** The delegate loggers per category. */
    private static final Map<LogCategory, DelegateLogger> DELEGATES = new ConcurrentHashMap<>();

    /** The log category for the always() method. */
    public static final LogCategory CAT_ALWAYS = new LogCategory("ALWAYS");

    /** The base DelegateLogger for the always() method. */
    private static final DelegateLogger BASE_DELEGATE = new DelegateLogger(LoggerFactory.getLogger(CAT_ALWAYS.toString()));

    /** The NO_LOGGER is the DelegateLogger that does not output anything after when() or filter(). */
    private static final DelegateLogger NO_LOGGER = new DelegateLogger(null, CategoryLogger.DelegateLogger.class, false);

    /** */
    private CategoryLogger()
    {
        // Utility class.
    }

    /* ---------------------------------------------------------------------------------------------------------------- */
    /* -------------------------------------------- INTERNAL HELPER METHODS ------------------------------------------- */
    /* ---------------------------------------------------------------------------------------------------------------- */

    /**
     * Check if the CategoryLogger has been initialized, and initialize the class when not.
     */
    private static void ensureInit()
    {
        if (initialized)
            return;
        synchronized (CategoryLogger.class)
        {
            if (initialized)
                return;
            // Bootstrap default category so .always() works immediately
            initialized = true;
            addLogCategory(CAT_ALWAYS);
            setLogLevel(CAT_ALWAYS, Level.TRACE);
            addLogCategory(LogCategory.ALL);
            setLogLevel(LogCategory.ALL, defaultLevel);
        }
    }

    /**
     * Prepare a Logger for the provided log category, and give it at least a console appender.
     * @param category the log category
     * @param cfg the record with the configuration for this category
     */
    private static void wireCategoryLogger(final LogCategory category, final CategoryConfig cfg)
    {
        Logger logger = getOrCreateLogger(category);
        logger.setAdditive(false);
        logger.setLevel(cfg.level);
        CategoryState st = new CategoryState(logger);
        CATEGORY_STATE.put(category, st);

        // Create per-category instances for every registered factory
        for (CategoryAppenderFactory f : APPENDER_FACTORIES.values())
        {
            Appender<ILoggingEvent> app = f.create(f.id(), category, cfg.pattern, CTX);
            app.start();
            logger.addAppender(app);
            st.appendersByFactoryId.put(f.id(), app);
        }

        // If no factories yet, at least wire a default console appender for visibility
        if (APPENDER_FACTORIES.isEmpty())
        {
            CategoryAppenderFactory fallback = new ConsoleAppenderFactory("CONSOLE");
            APPENDER_FACTORIES.putIfAbsent("CONSOLE", fallback);
            Appender<ILoggingEvent> app = fallback.create("CONSOLE", category, cfg.pattern, CTX);
            app.start();
            logger.addAppender(app);
            st.appendersByFactoryId.put("CONSOLE", app);
        }
    }

    /**
     * Give a logger for a log category its appenders.
     * @param category the log category
     */
    private static void rebuildCategoryAppenders(final LogCategory category)
    {
        CategoryState st = CATEGORY_STATE.get(category);
        CategoryConfig cfg = CATEGORY_CFG.get(category);
        if (st == null || cfg == null)
            return;

        // detach & stop existing
        st.appendersByFactoryId.forEach((id, app) ->
        { st.logger.detachAppender(app); safeStop(app); });
        st.appendersByFactoryId.clear();

        // build with new pattern
        for (CategoryAppenderFactory f : APPENDER_FACTORIES.values())
        {
            Appender<ILoggingEvent> app = f.create(f.id(), category, cfg.pattern, CTX);
            app.start();
            st.logger.addAppender(app);
            st.appendersByFactoryId.put(f.id(), app);
        }
    }

    /**
     * Get an existing logger based on its name, or create it when it does not exist yet.
     * @param category the category with the name under which the logger is registered
     * @return an existing logger based on its name, or create it when it does not exist yet
     */
    private static Logger getOrCreateLogger(final LogCategory category)
    {
        Logger logger = CTX.getLogger(category.toString());
        return logger;
    }

    /**
     * Stop an appender for when we change the configuration.
     * @param appender the appender to stop
     */
    private static void safeStop(final Appender<ILoggingEvent> appender)
    {
        try
        {
            appender.stop();
        }
        catch (RuntimeException ignore)
        {
        }
    }

    /* ---------------------------------------------------------------------------------------------------------------- */
    /* ------------------------------------------ CATEGORYLOGGER API METHODS ------------------------------------------ */
    /* ---------------------------------------------------------------------------------------------------------------- */

    /**
     * Always log to the registered appenders, still observing the default log level.
     * @return the DelegateLogger for method chaining, e.g., CategoryLogger.always().info("message");
     */
    public static DelegateLogger always()
    {
        ensureInit();
        return BASE_DELEGATE;
    }

    /**
     * Only log when the condition is true.
     * @param condition the condition to check
     * @return the DelegateLogger for method chaining, e.g., CategoryLogger.when(condition).info("message");
     */
    public static DelegateLogger when(final boolean condition)
    {
        ensureInit();
        return condition ? BASE_DELEGATE : NO_LOGGER;
    }

    /**
     * Only log when the boolean supplier provides a true value.
     * @param booleanSupplier the supplier that provides true or false
     * @return the DelegateLogger for method chaining, e.g., CategoryLogger.when(() -> condition()).info("message");
     */
    public static DelegateLogger when(final BooleanSupplier booleanSupplier)
    {
        return when(booleanSupplier.getAsBoolean());
    }

    /**
     * Only log when the category has been registered in the CategoryLogger.
     * @param category the category to check
     * @return the DelegateLogger for method chaining, e.g., CategoryLogger.filter(Cat.BASE).info("message");
     */
    public static DelegateLogger filter(final LogCategory category)
    {
        ensureInit();
        return DELEGATES.getOrDefault(category, NO_LOGGER);
    }

    /**
     * Register a log category that can log with the CategoryLogger. Note that unregistered loggers for which you use filter()
     * do not log.
     * @param category the log category to register.
     */
    public static synchronized void addLogCategory(final LogCategory category)
    {
        ensureInit();
        if (CATEGORY_CFG.containsKey(category))
            return;
        CategoryConfig cfg = new CategoryConfig(defaultLevel, defaultPattern);
        CATEGORY_CFG.put(category, cfg);
        org.slf4j.Logger slf = LoggerFactory.getLogger(category.toString());
        var delegate = new DelegateLogger(slf);
        DELEGATES.put(category, delegate);
        wireCategoryLogger(category, cfg);
    }

    /**
     * Remove a log category from logging with the CategoryLogger. Note that unregistered loggers for which you use filter() do
     * not log.
     * @param category the log category to unregister.
     */
    public static synchronized void removeLogCategory(final LogCategory category)
    {
        ensureInit();
        CategoryState st = CATEGORY_STATE.remove(category);
        CATEGORY_CFG.remove(category);
        if (st != null)
        {
            // detach & stop this category's appenders
            Logger logger = st.logger;
            st.appendersByFactoryId.values().forEach(app ->
            { logger.detachAppender(app); safeStop(app); });
            // silence the logger
            logger.setLevel(Level.OFF);
            logger.setAdditive(false);
        }
        DELEGATES.remove(category);
    }

    /**
     * Return the registered appenders for the LogCategory.
     * @param category the category to look up
     * @return the appenders for the LogCategory
     */
    public static Collection<Appender<ILoggingEvent>> getAppenders(final LogCategory category)
    {
        ensureInit();
        var st = CATEGORY_STATE.get(category);
        return st == null ? new HashSet<>() : st.appendersByFactoryId.values();
    }

    /**
     * Set the log category for a single log category.
     * @param category the log category
     * @param level the new log level
     */
    public static synchronized void setLogLevel(final LogCategory category, final Level level)
    {
        ensureInit();
        addLogCategory(category); // create if missing
        CATEGORY_CFG.get(category).level = level;
        Logger logger = CATEGORY_STATE.get(category).logger;
        logger.setLevel(level);
    }

    /**
     * Set the log category for all log categories, except ALWAYS.
     * @param level the new log level for all log categories, except ALWAYS
     */
    public static synchronized void setLogLevelAll(final Level level)
    {
        ensureInit();
        defaultLevel = level;
        for (var cat : CATEGORY_CFG.keySet())
        {
            if (cat.equals(CAT_ALWAYS))
                continue;
            CATEGORY_CFG.get(cat).level = level;
            CATEGORY_STATE.get(cat).logger.setLevel(level);
        }
    }

    /**
     * Set the pattern for a single log category.
     * 
     * <pre>
     * %date{HH:mm:ss.SSS}   Timestamp (default format shown; many options like ISO8601)
     * %level / %-5level     Log level (pad to fixed width with %-5level)
     * %logger / %logger{0}  Logger name (full or last component only; {n} = # of segments)
     * %thread               Thread name
     * %msg / %message       The actual log message
     * %n                    Platform-specific newline
     * %class / %class{1}    Calling class (full or just last segment with {1})
     * %method               Calling method
     * %line                 Source line number
     * %file                 Source file name
     * %caller               Shortcut for class, method, file, and line in one
     * %marker               SLF4J marker (if present)
     * %X{key}               MDC value for given key
     * %replace(p){r,e}      Apply regex replacement to pattern part p
     * %highlight(%msg)      ANSI colored message (useful on console)
     * </pre>
     *
     * Example:
     * 
     * <pre>
     * "%date{HH:mm:ss} %-5level %-6logger{0} %class{1}.%method:%line - %msg%n"
     *   → 12:34:56 INFO  http   HttpHandler.handle:42 - GET /users -> 200
     * </pre>
     * 
     * @param category the log category
     * @param pattern the new pattern
     */
    public static synchronized void setPattern(final LogCategory category, final String pattern)
    {
        ensureInit();
        addLogCategory(category); // create if missing
        CATEGORY_CFG.get(category).pattern = Objects.requireNonNull(pattern);
        // Rebuild this category's appenders with the new pattern
        rebuildCategoryAppenders(category);
    }

    /**
     * Set the pattern for a all log categories.
     * 
     * <pre>
     * %date{HH:mm:ss.SSS}   Timestamp (default format shown; many options like ISO8601)
     * %level / %-5level     Log level (pad to fixed width with %-5level)
     * %logger / %logger{0}  Logger name (full or last component only; {n} = # of segments)
     * %thread               Thread name
     * %msg / %message       The actual log message
     * %n                    Platform-specific newline
     * %class / %class{1}    Calling class (full or just last segment with {1})
     * %method               Calling method
     * %line                 Source line number
     * %file                 Source file name
     * %caller               Shortcut for class, method, file, and line in one
     * %marker               SLF4J marker (if present)
     * %X{key}               MDC value for given key
     * %replace(p){r,e}      Apply regex replacement to pattern part p
     * %highlight(%msg)      ANSI colored message (useful on console)
     * </pre>
     *
     * Example:
     * 
     * <pre>
     * "%date{HH:mm:ss} %-5level %-6logger{0} %class{1}.%method:%line - %msg%n"
     *   → 12:34:56 INFO  http   HttpHandler.handle:42 - GET /users -> 200
     * </pre>
     * 
     * @param pattern the new pattern
     */
    public static synchronized void setPatternAll(final String pattern)
    {
        ensureInit();
        defaultPattern = Objects.requireNonNull(pattern);
        CATEGORY_CFG.replaceAll((c, cfg) ->
        { cfg.pattern = pattern; return cfg; });
        CATEGORY_CFG.keySet().forEach(CategoryLogger::rebuildCategoryAppenders);
    }

    /**
     * Register a global appender factory. A separate Appender instance will be created for each registered category.
     * @param id the id to register the appender on, so it can be removed later
     * @param factory the factory that creates the appender with a create(..) method
     */
    public static synchronized void addAppender(final String id, final CategoryAppenderFactory factory)
    {
        ensureInit();
        if (APPENDER_FACTORIES.containsKey(id))
            throw new IllegalArgumentException("factory id exists: " + id);
        APPENDER_FACTORIES.put(id, factory);
        // Create & attach instances for all existing categories
        for (var e : CATEGORY_CFG.entrySet())
        {
            LogCategory cat = e.getKey();
            CategoryConfig cfg = e.getValue();
            CategoryState st = CATEGORY_STATE.get(cat);
            Appender<ILoggingEvent> app = factory.create(id, cat, cfg.pattern, CTX);
            app.start();
            st.logger.addAppender(app);
            st.appendersByFactoryId.put(id, app);
        }
    }

    /**
     * Remove a global appender factory; detaches and stops per-category instances.
     * @param id the id the appender was registered with
     */
    public static synchronized void removeAppender(final String id)
    {
        ensureInit();
        if (APPENDER_FACTORIES.remove(id) == null)
            return;
        for (CategoryState st : CATEGORY_STATE.values())
        {
            Appender<ILoggingEvent> app = st.appendersByFactoryId.remove(id);
            if (app != null)
            {
                st.logger.detachAppender(app);
                safeStop(app);
            }
        }
    }

    /* ---------------------------------------------------------------------------------------------------------------- */
    /* ------------------------------------------- HELPER CLASSES AND RECORDS ----------------------------------------- */
    /* ---------------------------------------------------------------------------------------------------------------- */

    /**
     * Class to store the logging level and pattern for a log category.
     */
    private static final class CategoryConfig
    {
        /** the logging level for a category. */
        private Level level;

        /** the String pattern to use for a category. */
        private String pattern;

        /**
         * Create a record for storing the logging level and pattern for a log category.
         * @param level the logging level for a category
         * @param pattern the pattern for a category
         */
        private CategoryConfig(final Level level, final String pattern)
        {
            this.level = Objects.requireNonNull(level);
            this.pattern = Objects.requireNonNull(pattern);
        }
    }

    /**
     * Class to store the logger and the appenders for a log category.
     */
    private static final class CategoryState
    {
        /** The logger to use. */
        private final Logger logger;

        /** The appenders for this log category. */
        private final Map<String, Appender<ILoggingEvent>> appendersByFactoryId = new HashMap<>();

        /**
         * Instantiate a category state.
         * @param logger the logger to use for the category that is connected to this state
         */
        CategoryState(final Logger logger)
        {
            this.logger = logger;
        }
    }

    /* ---------------------------------------------------------------------------------------------------------------- */
    /* ----------------------------------------------- APPENDER FACTORIES --------------------------------------------- */
    /* ---------------------------------------------------------------------------------------------------------------- */

    /**
     * The interface for the appender instance per category. The id is used for later removal.
     */
    public interface CategoryAppenderFactory
    {
        /**
         * Return the id to be used for later removal.
         * @return the id to be used for later removal
         */
        String id();

        /**
         * Create an appender instance for a category.
         * @param id the id to be used for later removal
         * @param category the logging category
         * @param messageFormat the pattern to use for printing the log message
         * @param ctx the context to use
         * @return an appender with the above features
         */
        Appender<ILoggingEvent> create(String id, LogCategory category, String messageFormat, LoggerContext ctx);
    }

    /** Console appender factory (uses the category's pattern). */
    public static final class ConsoleAppenderFactory implements CategoryAppenderFactory
    {
        /** the id to be used for later removal. */
        private final String id;

        /**
         * Instantiate the factory for the console appender.
         * @param id the id to be used for later removal
         */
        public ConsoleAppenderFactory(final String id)
        {
            this.id = id;
        }

        @Override
        public String id()
        {
            return this.id;
        }

        @Override
        @SuppressWarnings("checkstyle:hiddenfield")
        public Appender<ILoggingEvent> create(final String id, final LogCategory category, final String messageFormat,
                final LoggerContext ctx)
        {
            PatternLayoutEncoder enc = new PatternLayoutEncoder();
            enc.setContext(ctx);
            enc.setPattern(messageFormat);
            enc.start();

            ch.qos.logback.core.ConsoleAppender<ILoggingEvent> app = new ch.qos.logback.core.ConsoleAppender<>();
            app.setName(id + "@" + category.toString());
            app.setContext(ctx);
            app.setEncoder(enc);
            return app;
        }
    }

    /** Rolling file appender factory (per-category file pattern). */
    public static final class RollingFileAppenderFactory implements CategoryAppenderFactory
    {
        /** the id to be used for later removal. */
        private final String id;

        /** The filename pattern, e.g. "logs/%s-%d{yyyy-MM-dd}.log.gz" (use %s for category). */
        private final String fileNamePattern;

        /**
         * Instantiate the factory for the rolling file appender.
         * @param id the id to be used for later removal
         * @param fileNamePattern the filename pattern, e.g. "logs/%s-%d{yyyy-MM-dd}.log.gz" (use %s for category)
         */
        public RollingFileAppenderFactory(final String id, final String fileNamePattern)
        {
            this.id = id;
            this.fileNamePattern = fileNamePattern;
        }

        @Override
        public String id()
        {
            return this.id;
        }

        @Override
        @SuppressWarnings("checkstyle:hiddenfield")
        public Appender<ILoggingEvent> create(final String id, final LogCategory category, final String messageFormat,
                final LoggerContext ctx)
        {
            PatternLayoutEncoder enc = new PatternLayoutEncoder();
            enc.setContext(ctx);
            enc.setPattern(messageFormat);
            enc.start();

            RollingFileAppender<ILoggingEvent> file = new RollingFileAppender<>();
            file.setName(id + "@" + category.toString());
            file.setContext(ctx);
            file.setEncoder(enc);

            final TimeBasedRollingPolicy<ILoggingEvent> policy = new TimeBasedRollingPolicy<>();
            policy.setContext(ctx);
            policy.setParent(file);

            // IMPORTANT: replace only the category placeholder; keep %d{...} intact for Logback
            final String effectivePattern = this.fileNamePattern.replace("%s", category.toString());
            policy.setFileNamePattern(effectivePattern);

            policy.start();
            file.setRollingPolicy(policy);

            return file; // caller will start() it
        }
    }

    /* ---------------------------------------------------------------------------------------------------------------- */
    /* ------------------------------------------------ DELEGATE LOGGER ----------------------------------------------- */
    /* ---------------------------------------------------------------------------------------------------------------- */

    /**
     * DelegateLogger class that takes care of actually logging the message and/or exception. <br>
     * <p>
     * Copyright (c) 2003-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights
     * reserved.<br>
     * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
     * </p>
     * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
     */
    public static final class DelegateLogger
    {
        /** The logger facade from slf4j. */
        private final org.slf4j.Logger logger;

        /** The fully-qualified class name that defines the class to hide in the call stack. */
        private final String boundaryFqcn;

        /** Whether we should log or not (the NO_LOGGER does not log). */
        private final boolean log;

        /**
         * Create a DelegateLogger with a class that indicates what to hide in the call stack.
         * @param slf4jLogger the logger facade from slf4j, can be null in case no logging is done
         * @param callerBoundary class that defines what to hide in the call stack
         * @param log whether we should log or not (the NO_LOGGER does not log)
         */
        private DelegateLogger(final org.slf4j.Logger slf4jLogger, final Class<?> callerBoundary, final boolean log)
        {
            this.logger = slf4jLogger;
            this.boundaryFqcn = Objects.requireNonNull(callerBoundary).getName();
            this.log = log;
        }

        /**
         * Create a DelegateLogger with DelegateLogger as the only class to hide from the call stack.
         * @param slf4jLogger the logger facade from slf4j, can be null in case no logging is done
         */
        private DelegateLogger(final org.slf4j.Logger slf4jLogger)
        {
            this(slf4jLogger, CategoryLogger.DelegateLogger.class, true);
        }

        /**
         * The conditional filter that will result in the usage of a DelegateLogger.
         * @param condition the condition that should be evaluated
         * @return the logger that further processes logging (DelegateLogger)
         */
        public DelegateLogger when(final boolean condition)
        {
            if (condition)
                return this;
            return CategoryLogger.NO_LOGGER;
        }

        /**
         * The conditional filter that will result in the usage of a DelegateLogger.
         * @param supplier the function evaluating the condition
         * @return the logger that further processes logging (DelegateLogger)
         */
        public DelegateLogger when(final BooleanSupplier supplier)
        {
            if (supplier.getAsBoolean())
                return this;
            return CategoryLogger.NO_LOGGER;
        }

        /**
         * helper method to return the LoggingEventBuilder WITH a boundary to leave out part of the call stack.
         * @param leb The original LoggingEventBuilder
         * @return the boundary-based LoggerEventBuilder
         */
        private LoggingEventBuilder withBoundary(final LoggingEventBuilder leb)
        {
            if (leb instanceof CallerBoundaryAware cba)
                cba.setCallerBoundary(this.boundaryFqcn);
            return leb;
        }

        /* ---------------------------------------------------------------------------------------------------------------- */
        /* ----------------------------------------------------- TRACE ---------------------------------------------------- */
        /* ---------------------------------------------------------------------------------------------------------------- */

        /**
         * Create a debug log entry that will be output if TRACE is enabled for this DelegateLogger.
         * @param object the result of the <code>toString()</code> method of <code>object</code> will be logged
         */
        public void trace(final Object object)
        {
            if (!this.log || !this.logger.isTraceEnabled())
                return;
            withBoundary(this.logger.atTrace()).log(object.toString());
        }

        /**
         * Create a trace log entry that will be output if TRACE is enabled for this DelegateLogger.
         * @param message the message to log
         */
        public void trace(final String message)
        {
            if (!this.log || !this.logger.isTraceEnabled())
                return;
            withBoundary(this.logger.atTrace()).log(message);
        }

        /**
         * Create a trace log entry that will be output if TRACE is enabled for this DelegateLogger.
         * @param message the message to be logged, where {} entries will be replaced by arguments
         * @param arguments the arguments to substitute for the {} entries in the message string
         */
        public void trace(final String message, final Object... arguments)
        {
            if (!this.log || !this.logger.isTraceEnabled())
                return;
            withBoundary(this.logger.atTrace()).log(message, arguments);
        }

        /**
         * Create a trace log entry that will be output if TRACE is enabled for this DelegateLogger.
         * @param throwable the throwable to log
         */
        public void trace(final Throwable throwable)
        {
            if (!this.log || !this.logger.isTraceEnabled())
                return;
            withBoundary(this.logger.atTrace()).setCause(throwable)
                .log((() -> throwable.getClass().getSimpleName() + "(" + Objects.requireNonNullElse(throwable.getMessage(), "")
                        + ")"));
        }

        /**
         * Create a trace log entry that will be output if TRACE is enabled for this DelegateLogger.
         * @param throwable the throwable to log
         * @param message the message to log
         */
        public void trace(final Throwable throwable, final String message)
        {
            if (!this.log || !this.logger.isTraceEnabled())
                return;
            withBoundary(this.logger.atTrace()).setCause(throwable).log(message);
        }

        /**
         * Create a trace log entry that will be output if TRACE is enabled for this DelegateLogger.
         * @param throwable the exception to log
         * @param message the message to log, where {} entries will be replaced by arguments
         * @param arguments the arguments to substitute for the {} entries in the message string
         */
        public void trace(final Throwable throwable, final String message, final Object... arguments)
        {
            if (!this.log || !this.logger.isTraceEnabled())
                return;
            withBoundary(this.logger.atTrace()).setCause(throwable).log(message, arguments);
        }

        /* ---------------------------------------------------------------------------------------------------------------- */
        /* ----------------------------------------------------- DEBUG ---------------------------------------------------- */
        /* ---------------------------------------------------------------------------------------------------------------- */

        /**
         * Create a debug log entry that will be output if DEBUG is enabled for this DelegateLogger.
         * @param object the result of the <code>toString()</code> method of <code>object</code> will be logged
         */
        public void debug(final Object object)
        {
            if (!this.log || !this.logger.isDebugEnabled())
                return;
            withBoundary(this.logger.atDebug()).log(object.toString());
        }

        /**
         * Create a debug log entry that will be output if DEBUG is enabled for this DelegateLogger.
         * @param message the message to log
         */
        public void debug(final String message)
        {
            if (!this.log || !this.logger.isDebugEnabled())
                return;
            withBoundary(this.logger.atDebug()).log(message);
        }

        /**
         * Create a debug log entry that will be output if DEBUG is enabled for this DelegateLogger.
         * @param message the message to be logged, where {} entries will be replaced by arguments
         * @param arguments the arguments to substitute for the {} entries in the message string
         */
        public void debug(final String message, final Object... arguments)
        {
            if (!this.log || !this.logger.isDebugEnabled())
                return;
            withBoundary(this.logger.atDebug()).log(message, arguments);
        }

        /**
         * Create a debug log entry that will be output if DEBUG is enabled for this DelegateLogger.
         * @param throwable the throwable to log
         */
        public void debug(final Throwable throwable)
        {
            if (!this.log || !this.logger.isDebugEnabled())
                return;
            withBoundary(this.logger.atDebug()).setCause(throwable)
                .log((() -> throwable.getClass().getSimpleName() + "(" + Objects.requireNonNullElse(throwable.getMessage(), "")
                        + ")"));
        }

        /**
         * Create a debug log entry that will be output if DEBUG is enabled for this DelegateLogger.
         * @param throwable the throwable to log
         * @param message the message to log
         */
        public void debug(final Throwable throwable, final String message)
        {
            if (!this.log || !this.logger.isDebugEnabled())
                return;
            withBoundary(this.logger.atDebug()).setCause(throwable).log(message);
        }

        /**
         * Create a debug log entry that will be output if DEBUG is enabled for this DelegateLogger.
         * @param throwable the exception to log
         * @param message the message to log, where {} entries will be replaced by arguments
         * @param arguments the arguments to substitute for the {} entries in the message string
         */
        public void debug(final Throwable throwable, final String message, final Object... arguments)
        {
            if (!this.log || !this.logger.isDebugEnabled())
                return;
            withBoundary(this.logger.atDebug()).setCause(throwable).log(message, arguments);
        }

        /* ---------------------------------------------------------------------------------------------------------------- */
        /* ----------------------------------------------------- INFO ----------------------------------------------------- */
        /* ---------------------------------------------------------------------------------------------------------------- */

        /**
         * Create a info log entry that will be output if INFO is enabled for this DelegateLogger.
         * @param object the result of the <code>toString()</code> method of <code>object</code> will be logged
         */
        public void info(final Object object)
        {
            if (!this.log || !this.logger.isInfoEnabled())
                return;
            withBoundary(this.logger.atInfo()).log(object.toString());
        }

        /**
         * Create a info log entry that will be output if INFO is enabled for this DelegateLogger.
         * @param message the message to log
         */
        public void info(final String message)
        {
            if (!this.log || !this.logger.isInfoEnabled())
                return;
            withBoundary(this.logger.atInfo()).log(message);
        }

        /**
         * Create a info log entry that will be output if INFO is enabled for this DelegateLogger.
         * @param message the message to be logged, where {} entries will be replaced by arguments
         * @param arguments the arguments to substitute for the {} entries in the message string
         */
        public void info(final String message, final Object... arguments)
        {
            if (!this.log || !this.logger.isInfoEnabled())
                return;
            withBoundary(this.logger.atInfo()).log(message, arguments);
        }

        /**
         * Create a info log entry that will be output if INFO is enabled for this DelegateLogger.
         * @param throwable the throwable to log
         */
        public void info(final Throwable throwable)
        {
            if (!this.log || !this.logger.isInfoEnabled())
                return;
            withBoundary(this.logger.atInfo()).setCause(throwable)
                .log((() -> throwable.getClass().getSimpleName() + "(" + Objects.requireNonNullElse(throwable.getMessage(), "")
                        + ")"));
        }

        /**
         * Create a info log entry that will be output if INFO is enabled for this DelegateLogger.
         * @param throwable the throwable to log
         * @param message the message to log
         */
        public void info(final Throwable throwable, final String message)
        {
            if (!this.log || !this.logger.isInfoEnabled())
                return;
            withBoundary(this.logger.atInfo()).setCause(throwable).log(message);
        }

        /**
         * Create a info log entry that will be output if INFO is enabled for this DelegateLogger.
         * @param throwable the exception to log
         * @param message the message to log, where {} entries will be replaced by arguments
         * @param arguments the arguments to substitute for the {} entries in the message string
         */
        public void info(final Throwable throwable, final String message, final Object... arguments)
        {
            if (!this.log || !this.logger.isInfoEnabled())
                return;
            withBoundary(this.logger.atInfo()).setCause(throwable).log(message, arguments);
        }

        /* ---------------------------------------------------------------------------------------------------------------- */
        /* ----------------------------------------------------- WARN ----------------------------------------------------- */
        /* ---------------------------------------------------------------------------------------------------------------- */

        /**
         * Create a warn log entry that will be output if WARN is enabled for this DelegateLogger.
         * @param object the result of the <code>toString()</code> method of <code>object</code> will be logged
         */
        public void warn(final Object object)
        {
            if (!this.log || !this.logger.isWarnEnabled())
                return;
            withBoundary(this.logger.atWarn()).log(object.toString());
        }

        /**
         * Create a warn log entry that will be output if WARN is enabled for this DelegateLogger.
         * @param message the message to log
         */
        public void warn(final String message)
        {
            if (!this.log || !this.logger.isWarnEnabled())
                return;
            withBoundary(this.logger.atWarn()).log(message);
        }

        /**
         * Create a warn log entry that will be output if WARN is enabled for this DelegateLogger.
         * @param message the message to be logged, where {} entries will be replaced by arguments
         * @param arguments the arguments to substitute for the {} entries in the message string
         */
        public void warn(final String message, final Object... arguments)
        {
            if (!this.log || !this.logger.isWarnEnabled())
                return;
            withBoundary(this.logger.atWarn()).log(message, arguments);
        }

        /**
         * Create a warn log entry that will be output if WARN is enabled for this DelegateLogger.
         * @param throwable the throwable to log
         */
        public void warn(final Throwable throwable)
        {
            if (!this.log || !this.logger.isWarnEnabled())
                return;
            withBoundary(this.logger.atWarn()).setCause(throwable)
                .log((() -> throwable.getClass().getSimpleName() + "(" + Objects.requireNonNullElse(throwable.getMessage(), "")
                        + ")"));
        }

        /**
         * Create a warn log entry that will be output if WARN is enabled for this DelegateLogger.
         * @param throwable the throwable to log
         * @param message the message to log
         */
        public void warn(final Throwable throwable, final String message)
        {
            if (!this.log || !this.logger.isWarnEnabled())
                return;
            withBoundary(this.logger.atWarn()).setCause(throwable).log(message);
        }

        /**
         * Create a warn log entry that will be output if WARN is enabled for this DelegateLogger.
         * @param throwable the exception to log
         * @param message the message to log, where {} entries will be replaced by arguments
         * @param arguments the arguments to substitute for the {} entries in the message string
         */
        public void warn(final Throwable throwable, final String message, final Object... arguments)
        {
            if (!this.log || !this.logger.isWarnEnabled())
                return;
            withBoundary(this.logger.atWarn()).setCause(throwable).log(message, arguments);
        }

        /* ---------------------------------------------------------------------------------------------------------------- */
        /* ----------------------------------------------------- ERROR ---------------------------------------------------- */
        /* ---------------------------------------------------------------------------------------------------------------- */

        /**
         * Create a error log entry that will be output if ERROR is enabled for this DelegateLogger.
         * @param object the result of the <code>toString()</code> method of <code>object</code> will be logged
         */
        public void error(final Object object)
        {
            if (!this.log || !this.logger.isErrorEnabled())
                return;
            withBoundary(this.logger.atError()).log(object.toString());
        }

        /**
         * Create a error log entry that will be output if ERROR is enabled for this DelegateLogger.
         * @param message the message to log
         */
        public void error(final String message)
        {
            if (!this.log || !this.logger.isErrorEnabled())
                return;
            withBoundary(this.logger.atError()).log(message);
        }

        /**
         * Create a error log entry that will be output if ERROR is enabled for this DelegateLogger.
         * @param message the message to be logged, where {} entries will be replaced by arguments
         * @param arguments the arguments to substitute for the {} entries in the message string
         */
        public void error(final String message, final Object... arguments)
        {
            if (!this.log || !this.logger.isErrorEnabled())
                return;
            withBoundary(this.logger.atError()).log(message, arguments);
        }

        /**
         * Create a error log entry that will be output if ERROR is enabled for this DelegateLogger.
         * @param throwable the throwable to log
         */
        public void error(final Throwable throwable)
        {
            if (!this.log || !this.logger.isErrorEnabled())
                return;
            withBoundary(this.logger.atError()).setCause(throwable)
                .log((() -> throwable.getClass().getSimpleName() + "(" + Objects.requireNonNullElse(throwable.getMessage(), "")
                        + ")"));
        }

        /**
         * Create a error log entry that will be output if ERROR is enabled for this DelegateLogger.
         * @param throwable the throwable to log
         * @param message the message to log
         */
        public void error(final Throwable throwable, final String message)
        {
            if (!this.log || !this.logger.isErrorEnabled())
                return;
            withBoundary(this.logger.atError()).setCause(throwable).log(message);
        }

        /**
         * Create a error log entry that will be output if ERROR is enabled for this DelegateLogger.
         * @param throwable the exception to log
         * @param message the message to log, where {} entries will be replaced by arguments
         * @param arguments the arguments to substitute for the {} entries in the message string
         */
        public void error(final Throwable throwable, final String message, final Object... arguments)
        {
            if (!this.log || !this.logger.isErrorEnabled())
                return;
            withBoundary(this.logger.atError()).setCause(throwable).log(message, arguments);
        }
    }

}