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);
   }

}