# CentralLogger.java

    /*
    ** Module   : CentralLogger.java
    ** public abstract : Central Logger
    **
    ** Copyright (c) 2023-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ----------------------------Description-----------------------------
    ** 001 GBB 20230207 Initial setup
    ** 002 GBB 20230517 Default server and client log file patterns separated.
    ** 003 GBB 20230519 Support for %as placeholder in log file names with serverSide.
    **                  Resolve logger mode after server params are parsed and server mode is determined.
    **     GBB 20230520 Add the method getExplicitLevel to get the explicitly set level for the logger instead
    *                   of the one resolved from parent levels.
    ** 004 GBB 20230530 DEFAULT_CLIENT_LOG_FILE_NAME made package-private.
    ** 005 GBB 20230608 Syncing shut down. Replace atomicGetAndClearPreInit with the Utils method pollAll.
    ** 006 GBB 20230613 convertToLevel methods moved to LoggingUtils.
    ** 007 GBB 20230619 Adding field level to each logger instance to boost isLoggable performance.
    ** 008 GBB 20230620 createThreadName method moved to Utils.
    ** 009 GBB 20230623 Stop spawner logger when the client has been launched by parsing a cmd.
    ** 010 CA  20230704 Added a name for the timer thread used to stop the spawner logger.
    ** 011 GBB 20230825 The collection LOGGERS made thread-safe.
    ** 012 GBB 20231011 findLevel behavior, inheriting parent logger level works now even if the name is not
    **                  valid package.
    ** 013 GBB 20240311 Stream / Spawner logger to quickly exit based on isProcessRunning.
    ** 014 GBB 20240329 Lower the default spawner log level to FINE.
    ** 015 TJD 20240126 Java 17 fixes, c.g.p2j.net.Queue must be public
    **     TJD 20240117 Only allow to infer caller in case LoggingLevel is ALL
    ** 016 GBB 20240729 Support writing to slf4j-api.
    ** 017 ICP 20240719 Modified CentralLogger to wait for threads to complete execution before shutting down.
    ** 018 ICP 20240722 Modified shutdown hook registration.
    ** 019 ICP 20240813 Modified handleShutdown to close all files used by CentralLoggerFile
    */

    /*
     ** This program is free software: you can redistribute it and/or modify
     ** it under the terms of the GNU Affero General Public License as
     ** published by the Free Software Foundation, either version 3 of the
     ** License, or (at your option) any later version.
     **
     ** This program is distributed in the hope that it will be useful,
     ** but WITHOUT ANY WARRANTY; without even the implied warranty of
     ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     ** GNU Affero General Public License for more details.
     **
     ** You may find a copy of the GNU Affero GPL version 3 at the following
     ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
     **
     ** Additional terms under GNU Affero GPL version 3 section 7:
     **
     **   Under Section 7 of the GNU Affero GPL version 3, the following additional
     **   terms apply to the works covered under the License.  These additional terms
     **   are non-permissive additional terms allowed under Section 7 of the GNU
     **   Affero GPL version 3 and may not be removed by you.
     **
     **   0. Attribution Requirement.
     **
     **     You must preserve all legal notices or author attributions in the covered
     **     work or Appropriate Legal Notices displayed by works containing the covered
     **     work.  You may not remove from the covered work any author or developer
     **     credit already included within the covered work.
     **
     **   1. No License To Use Trademarks.
     **
     **     This license does not grant any license or rights to use the trademarks
     **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
     **     of Golden Code Development Corporation. You are not authorized to use the
     **     name Golden Code, FWD, or the names of any author or contributor, for
     **     publicity purposes without written authorization.
     **
     **   2. No Misrepresentation of Affiliation.
     **
     **     You may not represent yourself as Golden Code Development Corporation or FWD.
     **
     **     You may not represent yourself for publicity purposes as associated with
     **     Golden Code Development Corporation, FWD, or any author or contributor to
     **     the covered work, without written authorization.
     **
     **   3. No Misrepresentation of Source or Origin.
     **
     **     You may not represent the covered work as solely your work.  All modified
     **     versions of the covered work must be marked in a reasonable way to make it
     **     clear that the modified work is not originating from Golden Code Development
     **     Corporation or FWD.  All modified versions must contain the notices of
     **     attribution required in this license.
     */
    package com.goldencode.p2j.util.logging;

    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.main.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.ui.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.util.*;
    import org.slf4j.*;
    import org.slf4j.Logger;
    import org.slf4j.spi.*;

    import java.io.*;
    import java.lang.ref.*;
    import java.util.*;
    import java.util.Queue;
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.*;
    import java.util.function.*;
    import java.util.logging.*;
    import java.util.logging.Formatter;

    import static com.goldencode.p2j.util.logging.CentralLogger.Mode.*;
    import static com.goldencode.p2j.util.logging.LoggingUtil.*;

    /**
     * Base class for client, server, spawner jvm, class loader and default loggers. Acts as a factory for
     * logger instances. Loggers should not be instantiated directly from their subclasses, because they can be
     * used in classes part of multiple deployments, e.g. client, server, standalone tool, Harness tests. The
     * decision what type of logger is needed should be made runtime.
     * <p>
     * There is a specific order of invoking the logger methods in the app lifecycle.
     * <p>
     * First line in each driver should be {@link #setMode(Mode)} to enable the proper type instantiation in
     * {@link #get(String, boolean, boolean)}, proper actions on shutdown and context description in messages.
     * Both server and client loggers use the same formatter. The static method
     * {@link CentralLogFormatter#describeContext(Integer, String, boolean)} enhances each message before it's
     * stored as a LogRecord, marking where it comes from (the server or the client).
     * <p>
     * Initialization is the second necessary step in the logger lifecycle. It sets the configs for the operation
     * of the loggers and is different on the client and the server. It sets the root logger's level, based on
     * which log messages are filtered out or saved.
     * <p>
     * Before configs are read from directory and client bootstrap and initialization kicks in, all logs are
     * stored in-memory in a pre-init buffer. On initialization they can be filtered out based on the logger
     * level and further handled.
     * <p>
     * The base functionality of logging and the interface is shared between the server, client and default
     * fallback subclasses. The difference is in the implementation of the abstract method
     * {@link #publish(ContextLogRecord)}.
     * <p>
     * The client can operate in two modes - {@link Mode#CLIENT} and {@link Mode#CLIENT_STANDALONE}, where the
     * first one is sending logs to the server to be saved to files, while the second one writes log files by
     * itself.
     * <p>
     * With mode {@link Mode#CLIENT} all log records are stored in an in-memory data structure, pulled from the
     * client state synchronizer and added to the sync messages to the server. Before the session is
     * terminated, one last rpc call is made to the server through the service {@link CentralLogService} to
     * flush the leftover logs, see {@link CentralLoggerClient#finish()}.
     * <p>
     * On the server the log records are stored in an in-memory data structure before initialization and after
     * that saved to a file using FileHandler. The file gets rotated based on directory configs for size and
     * count. Its location and name prefix can also be configured.
     * <p>
     * The client and server loggers lifecycle ends with a call to {@link #handleShutDown()}. If an unexpected
     * event occurs during the execution and the loggers don't complete work normally, on JVM shutdown any
     * leftover logs are saved to a crash log file in the client / server / spawner launch directory.
     * <p>
     * CentralLogger provides also some backwards compatibility methods (e.g.
     * {@link #generate(String, Object...)}), originally from LogHelper, and methods providing
     * additional logging, that do parsing from System.err streams.
     */
    public abstract class CentralLogger
    {
       /**
        * The name of the JVM arg used to disable FWD's implementation of slf4j and make the logger switch
        * writing from files to slf4j api.
        */
       public static final String JVM_ARG_ENABLE_SLF4j_API = "enableSlf4jApi";

       /** Indicates if the loggers should use slf4j api instead of writing to FileHandler. */
       public static final boolean WRITE_TO_SLF4J_API =
          Boolean.parseBoolean(System.getProperty(JVM_ARG_ENABLE_SLF4j_API, "false"));

       /** The default name for server log files. */
       static final String DEFAULT_SERVER_LOG_FILE_NAME =
          "fwd_%m_" + SORTABLE_DATETIME_FORMAT + "_%uos_%uf_%as_%g.log";

       /** The default name for client log files. */
       static final String DEFAULT_CLIENT_LOG_FILE_NAME =
          "fwd_%m_" + SORTABLE_DATETIME_FORMAT + "_%uos_%pid_%uf_%as_%g.log";

       /** The default root logger level for server processes. */
       static final Level DEFAULT_ROOT_LEVEL = Level.INFO;

       /**
        * The mode of logger operation. There is one more temporary mode designed for ClassLoaders, not included
        *  in the enum. It gets replaced on initialization or first log attempt.
        */
       public enum Mode
       {
          /** Mode for ClientDriver as the app entry point. Still not initialized with server configs. */
          CLIENT_BASE,
          /** Mode for client that sends logs to the server. */
          CLIENT,
          /** Mode for client that writes logs to files by itself. */
          CLIENT_STANDALONE,
          /**
           * Mode for ServerDriver as the app entry point. Concrete type still not determined based on startup
           * args and run mode. Can be resolved as SERVER or DEFAULT.
           */
          SERVER_BASE,
          /** Mode for the server. */
          SERVER,
          /** Fallback mode for all unspecified entry points, like standalone tools or tests. */
          DEFAULT,
          /** Mode for the NativeSecureConnection JVM launched by the spawner. */
          SPAWNER_JVM
       }

       /** The slf4j MDC keys. */
       public enum MdcVar
       {
          /** The process id. */
          PID("pid"),

          /** The OS user. */
          OS_USER("osUser"),

          /** The thread name. */
          THREAD_NAME("threadName"),

          /** The thread ID. */
          THREAD_ID("threadId"),

          /** The session ID. */
          SESSION_ID("sessionId"),

          /** The FWD account. */
          FWD_ACCOUNT("fwdAccount");

          /** The name of the key. */
          private final String key;

          /**
           * Enum constructor to set the key name.
           *
           * @param    key
           *           The key name.
           */
          MdcVar(String key)
          {
             this.key = key;
          }

          /**
           * Returns the key name.
           *
           * @return   The key name.
           */
          public String getKey()
          {
             return key;
          }

          /**
           * Returns the string representation of the enum entry, that is the key name.
           *
           * @return   See above.
           */
          @Override
          public String toString()
          {
             return key;
          }
       }

       /** The slf4j keys. */
       public enum Slf4jKey
       {
          /** The logger name. */
          LOGGER_NAME("loggerName"),

          /** The original event time. */
          ORIGINAL_TIME("originalTime"),

          /** The name of the source class. */
          SOURCE_CLASS("sourceClassName"),

          /** The name of the source method. */
          SOURCE_METHOD("sourceMethodName");

          /** The name of the key. */
          private final String key;

          /**
           * Enum constructor to set the key name.
           *
           * @param    key
           *           The key name.
           */
          Slf4jKey(String key)
          {
             this.key = key;
          }

          /**
           * Returns the key name.
           *
           * @return   The key name.
           */
          public String getKey()
          {
             return key;
          }

          /**
           * Returns the string representation of the enum entry, that is the key name.
           *
           * @return   See above.
           */
          @Override
          public String toString()
          {
             return key;
          }
       }

       /** The default log file size in bytes. */
       public static final int DEFAULT_FILE_LIMIT_BYTES = 500_000;

       /** The minimum allowed log file size in bytes. */
       public static final int MIN_FILE_LIMIT_BYTES = 500_000;

       /** The default max number of log files before overwrite. */
       public static final int DEFAULT_FILE_COUNT = 120;

       /** The minimum allowed number of log files before overwrite. */
       public static final int MIN_FILE_COUNT = 2;

       /** Flag to indicate if the logger has flushed all logs on client shutdown / termination. */
       static final AtomicBoolean IS_LOGGER_WORK_FINISHED = new AtomicBoolean(false);

       /** The name of the root logger. */
       private static final String ROOT_LOGGER_NAME = "";

       /**
        * Collection of WeakReferences to logger threads for parsing streams. Used to get them notified, when
        * logger work should be finished.
        */
       private static final Set<WeakReference<Thread>> STREAM_LOGGING_THREADS = new HashSet<>();

       /**
        * Thread-safe queue to store logs before logger initialization. Needed to store them before the logger
        * level is determined, that could potentially filter some of them out.
        */
       private static final Queue<ContextLogRecord> PRE_INIT_LOG_BUFFER = new ConcurrentLinkedQueue<>();

       /**
        * Thread-safe map to store explicitly set log levels as logger name : log level pairs.
        * Contains the root ("") level log after initialization.
        */
       private static final Map<String, Level> LOGGER_LEVEL_MAP = new ConcurrentHashMap<>();

       /** Map between the java log levels and their corresponding sfl4j api counterparts. */
       private static final Map<java.util.logging.Level, org.slf4j.event.Level> JUL_TO_SLF4J_LEVEL_MAP =
          new HashMap<java.util.logging.Level, org.slf4j.event.Level>() {{
             put(Level.OFF, org.slf4j.event.Level.TRACE);
             put(Level.ALL, org.slf4j.event.Level.TRACE);
             put(Level.FINEST, org.slf4j.event.Level.TRACE);
             put(Level.FINER, org.slf4j.event.Level.DEBUG);
             put(Level.FINE, org.slf4j.event.Level.DEBUG);
             put(Level.CONFIG, org.slf4j.event.Level.INFO);
             put(Level.INFO, org.slf4j.event.Level.INFO);
             put(Level.WARNING, org.slf4j.event.Level.WARN);
             put(Level.SEVERE, org.slf4j.event.Level.ERROR);
          }};

       /** Lock for synchronizing setting and getting the logger mode. */
       private static final Object MODE_LOCK = new Object();

       /** The logger mode. */
       private static volatile Mode mode = null;

       /** Thread local field to mark if the slf4j MDC context has been initialized. */
       private static final ThreadLocal<Boolean> mdcInitialized = new ThreadLocal<>();

       /** Volatile map with the slf4j MDC base context for the process. */
       private static volatile Map<String, String> mdcBaseContextMap;

       /** Set of references to all instantiated loggers. */
       private static final Set<WeakReference<CentralLogger>> LOGGERS = ConcurrentHashMap.newKeySet();

       /** The bootstrap config reader. */
       private static BootstrapConfig bc;

       /** The instanced logger name. */
       private final String loggerName;

       /** The instanced logger level. Should never be set directly, but only through {@link #setLevel(Level)}. */
       private volatile Level level;

       /** Flag for the instanced logger to prevent recursion in class loaders. */
       private final boolean checkParentLevels;

       /** Flag to prevent deadlock on SecurityManager. */
       private final boolean excludeSMContext;

       /** Slf4j API logger. Enabled by jvm arg {@link #WRITE_TO_SLF4J_API}. */
       private volatile org.slf4j.Logger slf4jLogger;

       /**
        * Package-private constructor for CentralLogger.
        *
        * @param    loggerName
        *           The name of the logger.
        * @param    level
        *           The level of the logger.
        * @param    checkParentLevels
        *           Flag to prevent recursion for class loaders.
        * @param    excludeSMContext
        *           Flag to prevent deadlock on SecurityManager.
        */
       CentralLogger(String loggerName, Level level, boolean checkParentLevels, boolean excludeSMContext)
       {
          this.loggerName = loggerName;
          this.level = level;
          this.checkParentLevels = checkParentLevels;
          this.excludeSMContext = excludeSMContext;
       }

       /**
        * Setter for the logger mode.
        *
        * @param    mode
        *           The mode of the logger.
        */
       public static void setMode(Mode mode)
       {
          synchronized (MODE_LOCK)
          {
             if (CentralLogger.mode != null && CentralLogger.mode != CLIENT_BASE && CentralLogger.mode != SERVER_BASE)
             {
                // in case a client entry point is called on the server, is it possible though???
                return;
             }
             Mode originalMode = CentralLogger.mode;
             CentralLogger.mode = mode;

             if (originalMode == SERVER_BASE && mode == DEFAULT)
             {
                publishPreInitBuffer();
             }
          }
          if (mode == CLIENT_BASE || mode == CLIENT || mode == CLIENT_STANDALONE || mode == SERVER_BASE || mode == SERVER)
          {
             listenForShutDown();
          }
       }

       /**
        * Return if running in client
        *
        * @return   <code>true</code> if running in client mode, <code>false</code> otherwise.
        */
       public static boolean isClient()
       {
          Mode mode = getMode();
          return mode == CLIENT_BASE || mode == CLIENT_STANDALONE || mode == CLIENT;
       }

       /**
        * A generic method for logging streams (e.g. System.err, System.out).
        * <p>
        * A thread is created to parse the stream in background. A name for the assigned logger is provided in
        * args, as well as a supplier to indicate when the work of the thread should stop. The thread is weakly
        * referenced by a map (STREAM_LOGGING_THREADS) to be later woken up by a shutdown / termination app
        * event. The thread is assigned a name formed by the logger name and "-Logger" postfix and starts
        * execution. Its internal loop checks for new messages every one second.
        * <p>
        * The method usually receives either loggerLevel, or recordLevel arg to set the level of logging. On
        * scanning each line of the stream is attempted to be parsed by the LogFormatter used by CentralLogger,
        * which works for spawner JVM messages. If the message is not parsable and the record level is not
        * provided in the args, there is an attempt to parse it from the msg, which works for spawn messages.
        *
        * @param    inputStream
        *           The stream to be parsed to log messages.
        * @param    loggerName
        *           The name of the logger to log the parsed messages.
        * @param    loggerLevel
        *           The level of the logger. Overwrites the root / parents levels and ensures min allowed level
        *           for log messages.
        * @param    recordLevel
        *           The level of the log messages.
        * @param    isProcessRunning
        *           Supplier of a flag to indicate when the stream should stop being parsed.
        */
       public static void logStream(InputStream inputStream, String loggerName, Level loggerLevel,
                                    Level recordLevel, AtomicBoolean isProcessRunning)
       {
          Thread streamLoggingThread = new Thread(new Runnable()
          {
             private Scanner scanner = new Scanner(inputStream);

             @Override
             public void run()
             {
                CentralLogger streamLogger = get(loggerName);
                streamLogger.level = loggerLevel;

                while (hasNext())
                {
                   String line = scanner.nextLine();

                   CentralLogRecord clr = CentralLogFormatter.parse(line);
                   if (clr != null)
                   {
                      streamLogger.log(clr.toLogRecord());
                      continue;
                   }
                   Level level = recordLevel;
                   if (level == null)
                   {
                      // set default to WARNING just in case an important msg hasn't been
                      // transformed to the format
                      level = parseRecordLevel(line).orElse(Level.FINE);
                      line = line.replaceFirst(level.getName() + ": ", "");
                   }
                   streamLogger.log(level, line);
                }
             }

             private boolean hasNext()
             {
                try
                {
                   while (isProcessRunning.get() && inputStream.available() == 0)
                   {
                      Thread.sleep(10);
                   }
                }
                catch (IOException | InterruptedException e)
                {
                   return false;
                }
                return isProcessRunning.get() && scanner.hasNextLine();
             }
          }, loggerName + "-Logger");
          STREAM_LOGGING_THREADS.add(new WeakReference<>(streamLoggingThread));
          streamLoggingThread.setDaemon(true);
          streamLoggingThread.start();
       }

       /**
        * Returns a new instance of the CentralLogger for the source class, while preserving its attributes,
        * i.e. its log level. All log instances for client and server are secure, they don't have console
        * handlers and don't use parent handlers.
        *
        * @param    clazz
        *           The class of the source of log messages.
        *
        * @return   The instance of the CentralLogger.
        */
       public static CentralLogger get(Class<?> clazz)
       {
          return get(clazz.getName());
       }

       /**
        * Returns a new instance of the CentralLogger for the name, while preserving the corresponding logger
        * attributes, i.e. its log level. All log instances for client and server are secure, they don't have
        * console handlers and don't use parent handlers.
        *
        * @param    loggerName
        *           The name of the logger.
        *
        * @return   The instance of the CentralLogger.
        */
       public static CentralLogger get(String loggerName)
       {
          return get(loggerName, true, false);
       }

       /**
        * Returns a new instance of the CentralLogger for the name, while preserving the corresponding logger
        * attributes, i.e. its log level. All log instances for client and server are secure, they don't have
        * console handlers and don't use parent handlers.
        * <p>
        * First it tries to restore the logger level, if it has already been set explicitly before, because
        * logger levels are global for the app (a Java behavior of its LogManager, although not consistent
        * due to GC intervention). CentralLogger has a consistent behavior of global levels.
        * <p>
        * Then it checks if the logger mode has already been determined. It's set once for all logger instances
        * in the JVM. Setting the server / client mode should be the first statement in an app entry point. If
        * it's not set, when the first logger instance is requested, there are two possibilities:
        * a. The current Thread stacktrace indicates the logger is instantiated by a ClassLoader. Then a
        * temporary mode is activated that creates wrapper instances for the actual logger determined later, i.e.
        * CentralLoggerForClassLoaders;
        * b. The app is running in conversion mode, as a standalone tool or a test and a default / fallback
        * logger mode should be activated.
        *
        * @param    loggerName
        *           The name of the logger.
        * @param    checkParentLevels
        *           Flag to prevent recursion in class loaders, when determining the parent level.
        * @param    excludeSMContext
        *           Flag to prevent deadlock on SecurityManager.
        *
        * @return   The instance of the CentralLogger.
        */
       public static CentralLogger get(String loggerName, boolean checkParentLevels, boolean excludeSMContext)
       {
          loggerName = loggerName == null || loggerName.trim().isEmpty() ? ROOT_LOGGER_NAME : loggerName;
          Level level = findLevel(loggerName, checkParentLevels);

          synchronized (MODE_LOCK)
          {
             if (mode == null)
             {
                if (hasParentClassLoader())
                {
                   // special case for custom class loaders, logger mode is still not set
                   CentralLoggerNoModeWrapper logger = new CentralLoggerNoModeWrapper(loggerName,
                                                                                      level,
                                                                                      checkParentLevels,
                                                                                      excludeSMContext);
                   LOGGERS.add(new WeakReference<>(logger));
                   return logger;
                }
                // The deployment hasn't been explicitly marked as server or client. It's not clear what
                // configurations are present, so using the default fallback to stdout. No shut down hook here, no
                // crash log files generated.
                mode = DEFAULT;
             }
          }

          CentralLogger logger;
          switch (getMode())
          {
             case CLIENT_BASE:
             case SERVER_BASE:
                logger = new CentralLoggerNoModeWrapper(loggerName, level, checkParentLevels, excludeSMContext);
                break;
             case CLIENT:
                logger = new CentralLoggerClient(loggerName, level, checkParentLevels, excludeSMContext);
                break;
             case CLIENT_STANDALONE:
                logger = new CentralLoggerClientStandalone(loggerName, level, checkParentLevels, excludeSMContext);
                break;
             case SERVER:
                logger = new CentralLoggerServer(loggerName, level, checkParentLevels, excludeSMContext);
                break;
             case SPAWNER_JVM:
                logger = new CentralLoggerSpawnerJvm(loggerName, level, checkParentLevels, excludeSMContext);
                break;
             default:
                logger = new CentralLoggerFallback(loggerName, level, checkParentLevels, excludeSMContext);
          }
          LOGGERS.add(new WeakReference<>(logger));
          return logger;
       }

       /**
        * Invalidates the current logger levels and sets the new values. Called client-side.
        *
        * @param    loggerLevels
        *           The new logger levels.
        */
       public static void setLevels(CentralLoggerClientConfig loggerLevels)
       {
          LOGGER_LEVEL_MAP.clear();
          LOGGER_LEVEL_MAP.putAll(loggerLevels.getLoggerLevels());
          for (WeakReference<CentralLogger> loggerRef : LOGGERS)
          {
             CentralLogger logger = loggerRef.get();
             if (logger != null)
             {
                logger.level = findLevel(logger.loggerName, logger.checkParentLevels);
             }
          }
       }

       /**
        * Return the root logger level.
        *
        * @return   The root logger level.
        */
       static Level getRootLevel()
       {
          return LOGGER_LEVEL_MAP.getOrDefault(ROOT_LOGGER_NAME, null);
       }

       /**
        * Return the unmodifiable map of the logger name : logger level.
        *
        * @return   See above.
        */
       static Map<String, Level> getLoggerLevelMap()
       {
          return Collections.unmodifiableMap(LOGGER_LEVEL_MAP);
       }

       /**
        * Consider deprecated and don't use in future development. Ported from LogHelper for backwards
        * compatibility. Currently it simply does String.format(), while originally it also added context to
        * the message, which is now performed by the logger on creating a log record.
        * <p>
        * To be removed.
        *
        * @param    spec
        *           The sprintf specification string.
        * @param    params
        *           The substitution parameters.
        *
        * @return   The formatted message.
        */
       public static String generate(String spec, Object... params)
       {
          return String.format(spec, params);
       }

       /**
        * The method is called as a last resort from different points in the app execution to notify
        * CentralLogger to complete work:
        * a. On server process completed.
        * b. On shutdown hook called.
        * c. Before client process terminates server session.
        * d. On client process exception thrown.
        * <p>
        * The method sets the flag IS_PROCESS_RUNNING to false to make logger threads stop scanning for new
        * messages and then interrupts all live logger threads. Then exits if all messages have already been
        * processed as marked by the flag IS_LOGGER_WORK_FINISHED, which could be true in client mode.
        * <p>
        * Otherwise it gets the remaining log records from the pre-init buffer and the client log supplier
        * (server loggers don't keep logs in buffers after initialization, but directly record them to files)
        * and writes them to a file in the JVM launch dir, named after the logger mode, e.g.
        * `server_crash_%datetime.log`, `client_crash_%datetime.log`.
        */
       public static synchronized void handleShutDown()
       {
          STREAM_LOGGING_THREADS.stream()
                                .map(WeakReference::get)
                                .filter(Objects::nonNull)
                                .forEach(Thread::interrupt);
          STREAM_LOGGING_THREADS.clear();

          if (IS_LOGGER_WORK_FINISHED.getAndSet(true))
          {
             return;
          }

          Mode loggerMode = getMode();
          if (loggerMode == SERVER)
          {
             CentralLoggerServer.closeFiles();
          }
          else if (loggerMode == CLIENT_STANDALONE)
          {
             CentralLoggerClientStandalone.closeFiles();
          }

          CentralLoggerFile.closeAllFiles();

          List<LogRecord> leftoverLogs = loggerMode == CLIENT ? CentralLoggerClient.getLeftoverLogs() : null;
          List<ContextLogRecord> preInitLogs = Utils.pollAll(PRE_INIT_LOG_BUFFER).orElse(Collections.emptyList());
          if (preInitLogs.size() == 0 && (leftoverLogs == null || leftoverLogs.size() == 0))
          {
             return;
          }

          if (CentralLogger.WRITE_TO_SLF4J_API)
          {
             Logger crashLogger = LoggerFactory.getLogger("CrashLogger");
             for (LogRecord logRecord : preInitLogs)
             {
                LoggingEventBuilder slf4jLogBuilder = getSlf4jLogBuilder(crashLogger, logRecord);
                slf4jLogBuilder.addKeyValue(Slf4jKey.LOGGER_NAME.key, logRecord.getLoggerName())
                               .log();
             }
             if (leftoverLogs != null)
             {
                for (LogRecord logRecord : leftoverLogs)
                {
                   LoggingEventBuilder slf4jLogBuilder = getSlf4jLogBuilder(crashLogger, logRecord);
                   slf4jLogBuilder.addKeyValue(Slf4jKey.LOGGER_NAME.key, logRecord.getLoggerName())
                                  .log();
                }
             }
             return;
          }

          createCrashLogFile(loggerMode, preInitLogs, leftoverLogs);
       }

       /**
        * Sets the config reader.
        *
        * @param    bc
        *           The bootstrap config reader.
        */
       public static void setBootstrapConfig(BootstrapConfig bc)
       {
          CentralLogger.bc = bc;
       }

       /**
        * Initializer for clients. It's a call from {@link com.goldencode.p2j.ui.chui.ThinClient}, received
        * when the server sends the logging configs.
        *
        * @param    config
        *           The logger levels as coming from server configs.
        */
       public static void initializeClient(CentralLoggerClientConfig config)
       {
          if (mode != CLIENT_BASE)
          {
             return;
          }

          String filePath = bc.getString("client", "logging", "path", config.getFilePath());
          if (!StringHelper.hasContent(filePath))
          {
             filePath = DEFAULT_CLIENT_LOG_FILE_NAME;
          }
          int rotationLimit = bc.getInt("client", "logging", "rotationLimit", config.getFileLimit());
          int rotationCount = bc.getInt("client", "logging", "rotationCount", config.getFileCount());

          SessionManager sessMgr = SessionManager.get();
          Session session = sessMgr.getSession();
          String appServerName = sessMgr.getAppServer(session);

          if (config.isServerSideLoggingEnabled())
          {
             setMode(CLIENT);
             CentralLoggerClient.setupFileHandler(filePath, rotationLimit, rotationCount, appServerName);
          }
          else
          {
             setMode(CLIENT_STANDALONE);
             CentralLoggerClientStandalone.setupFileHandler(filePath, rotationLimit, rotationCount, appServerName);
          }
          // Root logger levels is part of the map, no need to be passed in explicitly.
          completePreInitWork(config.getLoggerLevels(), null);
       }

       /**
        * Called on client / server logger initialization after the global log level is resolved from the
        * configs. It sets the root logger level, which will help determine the parent logger levels for each
        * log. Then the pre-init buffer is emptied and logs are sent to the corresponding loggers.
        *
        * @param    nameLevelMap
        *           Logger name, level pairs.
        * @param    rootLevel
        *           The root logger level.
        */
       static void completePreInitWork(Map<String, Level> nameLevelMap, Level rootLevel)
       {
          Level rootLoggerLevel = nameLevelMap.remove(ROOT_LOGGER_NAME);
          rootLoggerLevel = rootLoggerLevel != null ? rootLoggerLevel : rootLevel;

          LOGGER_LEVEL_MAP.putAll(nameLevelMap);
          LOGGER_LEVEL_MAP.put(ROOT_LOGGER_NAME, rootLoggerLevel);

          // should replace all loggers' null levels
          invalidateLoggerTreeLevelCache(ROOT_LOGGER_NAME);

          publishPreInitBuffer();
       }

       /**
        * Static getter for the logger mode in the JVM.
        *
        * @return   The logger
        */
       static Mode getMode()
       {
          synchronized (MODE_LOCK)
          {
             return mode;
          }
       }

       /**
        * Adds a new log record to the pre-init buffer.
        *
        * @param    lr
        *           The log record.
        */
       static void addToPreInitBuffer(ContextLogRecord lr)
       {
          PRE_INIT_LOG_BUFFER.add(lr);
       }

       /**
        * Executed once in the process on initialization. A good place to set the MDC values, when the logger
        * is configured to write to slf4j api.
        *
        * @param    isRotationDisabled
        *           Flag to disable log file rotation.
        * @param    filePath
        *           The log file path.
        * @param    rotationLimit
        *           The file rotation limit.
        * @param    rotationCount
        *           The file rotation count.
        * @param    formatter
        *           The log formatter.
        *
        * @return   The java log file handler, used for writing logs to files.
        *
        * @throws   IOException
        *           Exception caused by file I/O operations.
        */
       static FileHandler setFileHandler(boolean isRotationDisabled,
                                         String filePath,
                                         int rotationLimit,
                                         int rotationCount,
                                         Formatter formatter)
       throws IOException
       {
          if (WRITE_TO_SLF4J_API)
          {
             try
             {
                // load the native library to find the pid and os user.
                if (!ClientCore.isNativeLayerInitialized())
                {
                   System.loadLibrary("p2j");
                }
                MDC.put(MdcVar.PID.getKey(), String.valueOf(ClientCore.getPid()));
                MDC.put(MdcVar.OS_USER.getKey(), String.valueOf(ClientCore.getUserName()));
             }
             catch (Throwable t)
             {
                System.err.println("The process id can't be determined at this time, which will lead to slf4j " +
                                      "logs having no value for the MDC vars `pid` and `osUser`. Loading the " +
                                      "native library `p2j` loading method is likely the cause of the issue.");
             }

             mdcBaseContextMap = MDC.getCopyOfContextMap();
             return null;
          }

          FileHandler fh = isRotationDisabled ?
             new FileHandler(filePath) :
             new FileHandler(filePath, rotationLimit, rotationCount);
          // we don't want a min logger level, so setting it to ALL
          fh.setLevel(Level.ALL);
          fh.setFormatter(formatter);
          return fh;
       }

       /**
        * Returns the level applying to the logger. If the level has been set for the logger explicitly, it is
        * returned. If the logger is for a class loader and the flag {@link CentralLogger#checkParentLevels} is
        * true, to avoid recursion the global level is returned. If the logger name can be parsed to a class
        * name, the level of the closest parent package is returned. If no parent packages have log levels set,
        * the root logger, i.e. "", level is returned.
        *
        * @param    loggerName
        *           The name of the logger.
        * @param    checkParentLevels
        *           Flag to prevent recursion for class loaders.
        *
        * @return   The logging level.
        */
       static Level findLevel(String loggerName, boolean checkParentLevels)
       {
          if (LOGGER_LEVEL_MAP.containsKey(loggerName))
          {
             return LOGGER_LEVEL_MAP.get(loggerName);
          }
          if (!checkParentLevels)
          {
             return getRootLevel();
          }
          String closestFind = ROOT_LOGGER_NAME;
          for (String existingLoggerName : LOGGER_LEVEL_MAP.keySet())
          {
             if (loggerName.startsWith(existingLoggerName) && existingLoggerName.length() > closestFind.length())
             {
                closestFind = existingLoggerName;
             }
          }
          return LOGGER_LEVEL_MAP.get(closestFind);
       }

       /** Writes all messages from the pre-init buffer with their respective loggers. */
       private static void publishPreInitBuffer()
       {
          // writes all pre-init log records to their actual destinations
          Map<String, CentralLogger> tempLoggerMap = new HashMap<>();
          List<ContextLogRecord> preInitLogRecords = Utils.pollAll(PRE_INIT_LOG_BUFFER)
                                                          .orElse(Collections.emptyList());
          for (ContextLogRecord logRecord : preInitLogRecords)
          {
             String loggerName = logRecord.getLoggerName();
             if (!tempLoggerMap.containsKey(loggerName))
             {
                tempLoggerMap.put(loggerName, CentralLogger.get(loggerName));
             }
             tempLoggerMap.get(loggerName).log(logRecord);
          }
       }

       /** Creates a new thread to listen for the JVM shutdown event and attaches it to the Runtime hook. */
       private static void listenForShutDown()
       {
          OrderedShutdownHooks.registerShutdownHook(2, new Thread(CentralLogger::handleShutDown, "ShutdownListener"));
       }

       /**
        * Creates a crash log file in the working directory, dumping all remaining logs.
        *
        * @param    loggerMode
        *           The logger mode.
        * @param    preInitLogBuffer
        *           The list of logs held in the pre-init buffer.
        * @param    leftoverLogs
        *           The list of logs held on the client with server-side logging.
        *
        */
       private static void createCrashLogFile(Mode loggerMode,
                                              List<ContextLogRecord> preInitLogBuffer,
                                              List<LogRecord> leftoverLogs)
       {
          String datetime = LoggingUtil.replaceFormatterDateTime(LoggingUtil.SORTABLE_DATETIME_FORMAT);
          String logFile = "fwd_" + loggerMode.name().toLowerCase() + "_crash_" + datetime + ".log";
          try (FileWriter fileWriter = new FileWriter(logFile))
          {
             CentralLogFormatter logFormatter = new CentralLogFormatter();
             Function<LogRecord, Void> write = logRecord ->
             {
                String logLine = logFormatter.format(logRecord);
                try
                {
                   fileWriter.write(logLine);
                   fileWriter.flush();
                }
                catch (IOException e)
                {
                   e.printStackTrace();
                }
                return null;
             };
             for (LogRecord logRecord : preInitLogBuffer)
             {
                write.apply(logRecord);
             }
             if (leftoverLogs != null)
             {
                for (LogRecord logRecord : leftoverLogs)
                {
                   write.apply(logRecord);
                }
             }
          }
          catch (Exception e)
          {
             e.printStackTrace();
          }
       }

       /**
        * Finds the log level in texts of the format `LEVEL: msg`. Used for parsing spawner process logs.
        *
        * @param    msg
        *           The log message.
        *
        * @return   Optional for the log level.
        */
       private static Optional<Level> parseRecordLevel(String msg)
       {
          String[] msgParts = msg.split(":");
          if (msgParts.length > 1)
          {
             return convertToLevel(msgParts[0]);
          }
          return Optional.empty();
       }

       /** Checks if there is a ClassLoader instance in the current thread stacktrace. */
       private static boolean hasParentClassLoader()
       {
          for (StackTraceElement ste : Thread.currentThread().getStackTrace())
          {
             String className = ste.getClassName();
             if (className.equals(Thread.class.getName()) || className.equals(CentralLogger.class.getName()))
             {
                continue;
             }
             try
             {
                Class<?> callerClass = Class.forName(className);
                if (callerClass != null && ClassLoader.class.isAssignableFrom(callerClass))
                {
                   return true;
                }
             }
             catch (ClassNotFoundException ignored)
             {
             }
          }
          return false;
       }

       /**
        * Invalidates the local cache of the current logger and all its children. If running on the server,
        * also sends a call to the client to update logger levels.
        *
        * @param    loggerName
        *           The name of the logger with a new level.
        */
       private static void invalidateLoggerTreeLevelCache(String loggerName)
       {
          for (WeakReference<CentralLogger> loggerRef : LOGGERS)
          {
             CentralLogger logger = loggerRef.get();
             if (logger != null && logger.loggerName.startsWith(loggerName))
             {
                logger.level = findLevel(logger.loggerName, logger.checkParentLevels);
             }
          }
          if (CentralLoggerServer.isLogicalTerminalInitialized())
          {
             LogicalTerminal.getInstance()
                            .invalidateLoggerLevels(new CentralLoggerClientConfig(LOGGER_LEVEL_MAP));
          }
       }

       /**
        * Publishes a log record, different for each version of CentralLogger.
        *
        * @param    logRecord
        *           The log record.
        */
       protected abstract void publish(ContextLogRecord logRecord);

       /**
        * Checks if the argument object represents the same logger, identified by its name and level.
        *
        * @param    o
        *           The object to compare to.
        *
        * @return   <code>true</code> if it's the same instance or name and level are the same.
        *           <code>false</code> otherwise.
        */
       @Override
       public boolean equals(Object o)
       {
          if (this == o)
          {
             return true;
          }
          if (o == null || getClass() != o.getClass())
          {
             return false;
          }
          CentralLogger centralLogger = (CentralLogger) o;
          return loggerName.equals(centralLogger.loggerName);
       }

       /**
        * Returns the hash of the fields (loggerName and level) uniquely identifying a logger.
        *
        * @return   The hash of the fields (loggerName and level) uniquely identifying a logger.
        */
       @Override
       public int hashCode()
       {
          return Objects.hash(loggerName);
       }

       /**
        * Getter for the logger name.
        *
        * @return   The logger name.
        */
       public String getLoggerName()
       {
          return loggerName;
       }

       /**
        * Getter for the logger level.
        *
        * @return   The logger level.
        */
       public Level getLevel()
       {
          return level;
       }

       /**
        * Returns the level explicitly set for this logger.
        *
        * @return   The logging level explicitly set for this logger.
        */
       public Level getExplicitLevel()
       {
          return LOGGER_LEVEL_MAP.getOrDefault(loggerName, null);
       }

       /**
        * Setter for the logger level. Updates the map with all logger levels. If on the server, invalidates
        * client log levels.
        *
        * @param    level
        *           The level to set to the logger.
        */
       public void setLevel(Level level)
       {
          if (isClient())
          {
             // Setting logger level in client mode is not supported.
             return;
          }

          if ((LOGGER_LEVEL_MAP.containsKey(loggerName) && LOGGER_LEVEL_MAP.get(loggerName) == level) ||
             (!LOGGER_LEVEL_MAP.containsKey(loggerName) && level == null))
          {
             // no change
             return;
          }

          if (level != null)
          {
             LOGGER_LEVEL_MAP.put(loggerName, level);
          }
          else
          {
             LOGGER_LEVEL_MAP.remove(loggerName);
          }

          invalidateLoggerTreeLevelCache(loggerName);
       }

       /**
        * Getter for the flag indicating if the log level can be determined by checking the parent loggers.
        *
        * @return   <code>true</code> if the log level can be determined by checking the parent loggers.
        */
       public boolean isCheckParentLevels()
       {
          return checkParentLevels;
       }

       /**
        * Getter for the excludeSMContext preventing deadlock on SecurityManager.
        *
        * @return   <code>true</code> if context should not be fetched from SecurityManager,
        *           <code>false</code> otherwise.
        */
       public boolean isExcludeSMContext()
       {
          return excludeSMContext;
       }

       /**
        * Returns if the msg level is loggable by the logger, that is true when the log msg level is the same
        * or bigger than the logger level.
        *
        * @param    logLevel
        *           The level of the log msg.
        *
        * @return   <code>true</code> if loggable, <code>false</code> otherwise.
        */
       public boolean isLoggable(Level logLevel)
       {
          return level == null || logLevel.intValue() >= level.intValue();
       }

       /**
        * Logs a new message. The message is evaluated after determined if it can be logged. The supplier
        * role is to delay costly evaluation.
        *
        * @param    level
        *           The level of the log msg.
        * @param    msgSupplier
        *           Supplier for the log msg.
        */
       public void log(Level level, Supplier<String> msgSupplier)
       {
          if (!canLog(level, null))
          {
             return;
          }
          log(level, msgSupplier.get());
       }

       /**
        * Logs a new message.
        *
        * @param    level
        *           The level of the log msg.
        * @param    msg
        *           The log msg.
        */
       public void log(Level level, String msg)
       {
          ultimateLog(level, (Throwable) null, null, null, null, msg);
       }

       /**
        * Logs a new message.
        *
        * @param    level
        *           The level of the log msg.
        * @param    msg
        *           The log msg.
        * @param    t
        *           Throwable.
        */
       public void log(Level level, String msg, Throwable t)
       {
          ultimateLog(level, t, null, null, null, msg);
       }

       /**
        * Logs a new message. The message is formed by message format and params. The only supported
        * placeholders are of type <code>%s</code>, because the client logger sends the parameters as part of
        * {@link CentralLogRecord} to the server and parsing Externalizables supports limited types.
        * <p>
        * This method overload can be easily misused by passing in a Throwable as the last argument and the
        * Throwable will not be logged, so there is an argument verification.
        *
        * @param    level
        *           The level of the log msg.
        * @param    msgFormat
        *           The log msg format.
        * @param    params
        *           The format params.
        */
       public void log(Level level, String msgFormat, Object... params)
       {
          if (params != null && params.length > 1 && params[params.length - 1] instanceof Throwable)
          {
             warning("", new IllegalArgumentException("CentralLogger log method overload used by mistake." +
                                                         "Check the source of the call in the stacktrace."));
             return;
          }
          ultimateLog(level, (Throwable) null, null, null, null, msgFormat, params);
       }

       /**
        * Logs a new message. The message is formed by message format and params. The only supported
        * placeholders are of type <code>%s</code>, because the client logger sends the parameters as part of
        * {@link CentralLogRecord} to the server and parsing Externalizables supports limited types.
        *
        * @param    level
        *           The level of the log msg.
        * @param    t
        *           Throwable.
        * @param    msgFormat
        *           The log msg format.
        * @param    params
        *           The format params.
        */
       public void log(Level level, Throwable t, String msgFormat, Object... params)
       {
          ultimateLog(level, t, null, null, null, msgFormat, params);
       }

       /**
        * Logs a new message.
        *
        * @param    level
        *           The level of the log msg.
        * @param    sourceClass
        *           The class where the msg originates from.
        * @param    sourceMethod
        *           The method where the msg originates from.
        * @param    msg
        *           The log msg.
        */
       public void logp(Level level, String sourceClass, String sourceMethod, String msg)
       {
          ultimateLog(level, (Throwable) null, sourceClass, sourceMethod, null, msg);
       }

       /**
        * Logs a new message.
        *
        * @param    level
        *           The level of the log msg.
        * @param    sourceClass
        *           The class where the msg originates from.
        * @param    sourceMethod
        *           The method where the msg originates from.
        * @param    msg
        *           The log msg.
        * @param    t
        *           Throwable.
        */
       public void logp(Level level, String sourceClass, String sourceMethod, String msg, Throwable t)
       {
          ultimateLog(level, t, sourceClass, sourceMethod, null, msg);
       }

       /**
        * Logs a new message with log level {@link Level#SEVERE}.
        *
        * @param    msg
        *           The log msg.
        */
       public void severe(String msg)
       {
          log(Level.SEVERE, msg);
       }

       /**
        * Logs a new message with log level {@link Level#SEVERE}.
        *
        * @param    msg
        *           The log msg.
        * @param    t
        *           Throwable.
        */
       public void severe(String msg, Throwable t)
       {
          log(Level.SEVERE, msg, t);
       }

       /**
        * Logs a new message with log level {@link Level#WARNING}.
        *
        * @param    msg
        *           The log msg.
        */
       public void warning(String msg)
       {
          log(Level.WARNING, msg);
       }

       /**
        * Logs a new message with log level {@link Level#WARNING}.
        *
        * @param    msg
        *           The log msg.
        * @param    t
        *           Throwable.
        */
       public void warning(String msg, Throwable t)
       {
          log(Level.WARNING, msg, t);
       }

       /**
        * Logs a new message with log level {@link Level#WARNING}. The message is evaluated after determined if
        * it can be logged. The supplier role is to delay costly evaluation.
        *
        * @param    msgSupplier
        *           The log msg.
        */
       public void warning(Supplier<String> msgSupplier)
       {
          if (!canLog(Level.WARNING, null))
          {
             return;
          }
          log(Level.WARNING, msgSupplier.get());
       }

       /**
        * Logs a new message with log level {@link Level#INFO}.
        *
        * @param    msg
        *           The log msg.
        */
       public void info(String msg)
       {
          log(Level.INFO, msg);
       }

       /**
        * Logs a new message with log level {@link Level#INFO}.
        *
        * @param    msg
        *           The log msg.
        * @param    t
        *           Throwable.
        */
       public void info(String msg, Throwable t)
       {
          log(Level.INFO, msg, t);
       }

       /**
        * Logs a new message with log level {@link Level#CONFIG}.
        *
        * @param    msg
        *           The log msg.
        */
       public void config(String msg)
       {
          log(Level.CONFIG, msg);
       }

       /**
        * Logs a new message with log level {@link Level#FINE}.
        *
        * @param    msg
        *           The log msg.
        */
       public void fine(String msg)
       {
          log(Level.FINE, msg);
       }

       /**
        * Logs a new message with log level {@link Level#FINE}. The message is evaluated after determined if
        * it can be logged. The supplier role is to delay costly evaluation.
        *
        * @param    msgSupplier
        *           The log msg.
        */
       public void fine(Supplier<String> msgSupplier)
       {
          if (!canLog(Level.FINE, null))
          {
             return;
          }
          log(Level.FINE, msgSupplier.get());
       }

       /**
        * Logs a new message with log level {@link Level#FINER}.
        *
        * @param    msg
        *           The log msg.
        */
       public void finer(String msg)
       {
          log(Level.FINER, msg);
       }

       /**
        * Logs a new message with log level {@link Level#FINEST}.
        *
        * @param    msg
        *           The log msg.
        */
       public void finest(String msg)
       {
          log(Level.FINEST, msg);
       }

       /**
        * Logs a new message with log level {@link Level#ALL}.
        *
        * @param    msg
        *           The log msg.
        */
       public void all(String msg)
       {
          log(Level.ALL, msg);
       }

       /**
        * Log the java log record.
        *
        * @param    logRecord
        *           The log record.
        * @param    isServer
        *           <code>true</code> if in a server process.
        * @param    isServerSideLogging
        *           <code>true</code> if server-side logging is enabled.
        */
       void logToSlf4j(ContextLogRecord logRecord, boolean isServer, boolean isServerSideLogging)
       {
          if (slf4jLogger == null)
          {
             slf4jLogger = LoggerFactory.getLogger(loggerName);
          }

          if ((isServer && isServerSideLogging) || Boolean.TRUE != mdcInitialized.get())
          {
             // associate the stored copy of the original MDC values with the new thread
             MDC.setContextMap(mdcBaseContextMap);

             ContextLogRecord.LogContext logContext = logRecord.getLogContext();

             MDC.put(MdcVar.THREAD_NAME.getKey(),
                     logContext != null ? logContext.getThreadName() : Thread.currentThread().getName());

             if (logContext != null)
             {
                String sessionId = logContext.getSessionId();

                if (isServer)
                {
                   MDC.put(MdcVar.THREAD_ID.getKey(), logContext.getThreadId());
                }

                if (sessionId != null)
                {
                   MDC.put(MdcVar.SESSION_ID.getKey(), sessionId);
                   MDC.put(MdcVar.FWD_ACCOUNT.getKey(), logContext.getFwdUser());
                   mdcInitialized.set(true);
                }
             }
          }

          getSlf4jLogBuilder(slf4jLogger, logRecord).log();
       }

       /**
        * Creates a slf4j event builder, reads the java log record and sets the necessary properties.
        *
        * @param    slf4jLogger
        *           The slf4j logger that is to log the record.
        * @param    logRecord
        *           The java log record to be used for the slf4j event.
        *
        * @return   The slf4j event builder.
        */
       static LoggingEventBuilder getSlf4jLogBuilder(Logger slf4jLogger, LogRecord logRecord)
       {
          // can't directly move to slf4j due to difference in parameterized formats
          Object[] parameters = logRecord.getParameters();
          String msg = parameters != null && parameters.length > 0 ?
             String.format(logRecord.getMessage(), parameters) :
             logRecord.getMessage();

          LoggingEventBuilder eventBuilder = slf4jLogger.atLevel(JUL_TO_SLF4J_LEVEL_MAP.get(logRecord.getLevel()))
                                                        .setMessage(msg)
                                                        .setCause(logRecord.getThrown())
                                                        .addKeyValue(Slf4jKey.ORIGINAL_TIME.key,
                                                                     String.valueOf(logRecord.getMillis()));

          String sourceClassName = logRecord.getSourceClassName();
          if (sourceClassName != null)
          {
             eventBuilder.addKeyValue(Slf4jKey.SOURCE_CLASS.key, sourceClassName);
          }

          String sourceMethodName = logRecord.getSourceMethodName();
          if (sourceMethodName != null)
          {
             eventBuilder.addKeyValue(Slf4jKey.SOURCE_METHOD.key, sourceMethodName);
          }

          return eventBuilder;
       }

       /**
        * Checks if a message with the provided level can be logged. The level cannot be null, the logger
        * should not have indicated that work is finished and the msg level should be same or bigger than the
        * logger level.
        *
        * @param    level
        *           The log level.
        * @param    msg
        *           The log msg.
        *
        * @return   <code>true</code> if the msg can be logged, <code>false</code> otherwise.
        *
        * @throws   IllegalArgumentException
        *           If log level is null.
        */
       private boolean canLog(Level level, String msg)
       {
          if (level == null)
          {
             throw new IllegalArgumentException(
                "Log record level cannot be null" +
                   (msg == null ? "." : " for msg: " + msg));
          }
          return !IS_LOGGER_WORK_FINISHED.get() && isLoggable(level);
       }

       /**
        * Logs a new log record. If the included message already has context details attached, the passed-in
        * flag addContext would be false.
        *
        * @param    logRecord
        *           The log record.
        */
       private void log(ContextLogRecord logRecord)
       {
          ultimateLog(logRecord.getLevel(), logRecord.getThrown(), logRecord.getSourceClassName(),
                      logRecord.getSourceMethodName(), logRecord.getLogContext(), logRecord.getMessage(),
                      logRecord.getParameters());
       }

       /**
        * Logs a new message. The message is formed by message format and params. The only supported
        * placeholders are of type <code>%s</code>, because the client logger sends the parameters as part of
        * {@link CentralLogRecord} to the server and parsing Externalizables supports limited types.
        *
        * @param    level
        *           The level of the log msg.
        * @param    sourceClass
        *           The class where the msg originates from.
        * @param    sourceMethod
        *           The method where the msg originates from.
        * @param    t
        *           Throwable.
        * @param    context
        *           The original context of the log record.
        * @param    msg
        *           The log msg.
        * @param    params
        *           The format params.
        */
       private void ultimateLog(Level level, Throwable t, String sourceClass, String sourceMethod,
                                ContextLogRecord.LogContext context, String msg, Object... params)
       {
          if (!canLog(level, msg))
          {
             return;
          }

          ContextLogRecord lr = new ContextLogRecord(level, msg);
          if (context == null && getMode() != DEFAULT)
          {
             context = CentralLogFormatter.describeContext(CentralLoggerClient.getSessionId(),
                                                           CentralLoggerClient.getUserId(),
                                                           excludeSMContext);
          }
          lr.setLogContext(context);
          lr.setLoggerName(loggerName);
          // If log Level is ALL allow LogRecord to infer for caller source class and source method
          if (sourceClass != null || !level.equals(Level.ALL))
          {
             lr.setSourceClassName(sourceClass);
          }
          if (sourceMethod != null || !level.equals(Level.ALL))
          {
             lr.setSourceMethodName(sourceMethod);
          }
          if (t != null)
          {
             lr.setThrown(t);
          }
          if (params != null)
          {
             lr.setParameters(params);
          }

          publish(lr);
       }

    }