# CentralLoggerServer.java

    /*
    ** Module   : CentralLoggerServer.java
    ** Abstract : CentralLogger concrete version for servers.
    **
    ** Copyright (c) 2023-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ----------------------------Description-----------------------------
    ** 001 GBB 20230207 Initial setup
    ** 002 GBB 20230517 Native lib not being loaded exception now logged on a lower level.
    ** 003 GBB 20230519 Support for %as placeholder in log file names with serverSide.
    **                  Support for disabling rotation.
    **                  Support for console logging.
    ** 004 GBB 20230530 Use default log file name on issues with provided path.
    ** 005 RAA 20230608 Added null parameter to resolveLogFile.
    ** 006 GBB 20230613 convertToLevel moved to LoggingUtils.
    ** 007 GBB 20230619 Adding field level to each logger instance to boost isLoggable performance.
    ** 008 GBB 20230623 Pass in AtomicBoolean instead of Supplier<Boolean> to logStream.
    ** 009 GBB 20230626 Sets SESSION:FWD-LOGFILE on client process file handler initialized.
    ** 010 GBB 20240729 Support for writing to slf4j api.
    */
    /*
     ** 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.directory.*;
    import com.goldencode.p2j.main.*;
    import com.goldencode.p2j.security.SecurityManager;
    import com.goldencode.p2j.util.*;
    import com.goldencode.util.*;
    import org.slf4j.*;
    import org.slf4j.Logger;

    import java.io.*;
    import java.util.*;
    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.LoggingUtil.*;

    /** A concrete version of CentralLogger for apps launched by {@link com.goldencode.p2j.main.ServerDriver#main}. */
    public class CentralLoggerServer
    extends CentralLogger
    {
       /** Logger. */
       private static final CentralLogger LOG = CentralLogger.get(CentralLoggerServer.class);

       /** The log formatter. */
       private static final Formatter LOG_FORMATTER = new CentralLogFormatter();

       /** The logger name for the redirected System.err from the spawn process. */
       private static final String SPAWNER_LOGGER_NAME = "Spawner";

       /** Counter for unique name for the spawner logger Thread. */
       private static final AtomicInteger SPAWNER_THREAD_COUNTER = new AtomicInteger(1);

       /** The default logger level for spawner processes. */
       private static final Level DEFAULT_SPAWNER_LEVEL = Level.WARNING;

       /** Placeholder for log files count, when no config value. */
       private static final int MISSING_COUNT_CONFIG_PLACEHOLDER = -1;

       /** Placeholder for log files size, when no config value. */
       private static final int MISSING_LIMIT_CONFIG_PLACEHOLDER = -1;

       /** Map of client process id : FileHandler pairs */
       private static final Map<Long, FileHandler> clientPidFileHandlerPairs = new HashMap<>();

       /**
        * Map of client logger name : slf4j logger instance pairs for clients sending logs server-side. The
        * names can be the same between clients and between the client and teh server loggers. The
        * differentiation comes from the context (MDC and other key-values).
        */
       private static final Map<String, org.slf4j.Logger> clientLoggerNameSlf4jPairs = new ConcurrentHashMap<>();

       /** Flag to indicate if the LogicalTerminal has been initialized. */
       private static final AtomicBoolean isLogicalTerminalInitialized = new AtomicBoolean(false);

       /** Configs for all instances of the logger. Effectively final after initialization. */
       private static volatile Configs configs;

       /** The file handler, responsible for writing server logs to files. */
       private static volatile FileHandler serverFileHandler;

       /**
        * Package-private constructor for CentralLoggerServer.
        *
        * @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.
        */
       CentralLoggerServer(String loggerName, Level level, boolean checkParentLevels, boolean excludeSMContext)
       {
          super(loggerName, level, checkParentLevels, excludeSMContext);
       }

       /**
        * Initializer. Sets the root logger level. Creates a file handler and passes in the log file path and
        * rotation configs. Then handles the pre-init log messages.
        *
        * @param    configs
        *           All configs needed to create log files and set the root logger level.
        */
       public static void initialize(Configs configs)
       {
          if (CentralLoggerServer.configs != null)
          {
             return;
          }
          if (configs == null)
          {
             throw new IllegalArgumentException("Cannot initialize CentralLoggerServer with null Configs.");
          }
          synchronized (CentralLoggerServer.class)
          {
             if (CentralLoggerServer.configs != null)
             {
                return;
             }
             boolean isRotationDisabled = configs.fileLimit == 0 || configs.fileCount == 0;

             Supplier<Long> processIdSupplier = () -> {
                try
                {
                   System.loadLibrary("p2j");
                   return ClientCore.getPid();
                }
                catch (Throwable t)
                {
                   LOG.warning("Server process id can't be determined at this time, which will lead to a " +
                                  "corrupt log file name for the server process. The native library `p2j` " +
                                  "loading method is likely the cause of the issue.");
                   LOG.log(Level.FINE, "", t);
                   return 0L;
                }
             };
             String formattedFilePath = resolveLogFile(configs.filePath,
                                                       processIdSupplier,
                                                       "server",
                                                       isRotationDisabled);

             CentralLogger centralLogger = CentralLogger.get(CentralLoggerServer.class);
             centralLogger.info("Configs received: " + configs + ". WRITE_TO_SLF4J_API: " + WRITE_TO_SLF4J_API);

             if (!StringHelper.hasContent(formattedFilePath) || !canWriteToPath(formattedFilePath))
             {
                formattedFilePath = resolveLogFile(DEFAULT_SERVER_LOG_FILE_NAME,
                                                   processIdSupplier,
                                                   "server",
                                                   isRotationDisabled);
                centralLogger.info("Initializing logging with default file path " + formattedFilePath);
                configs.filePath = DEFAULT_SERVER_LOG_FILE_NAME;
             }

             CentralLoggerServer.configs = configs;
             try
             {
                serverFileHandler = setFileHandler(isRotationDisabled,
                                                   formattedFilePath,
                                                   configs.fileLimit,
                                                   configs.fileCount,
                                                   LOG_FORMATTER);
             }
             catch (IOException io)
             {
                centralLogger.warning("FileHandler couldn't be initialized:", io);
             }
          }

          completePreInitWork(configs.loggerLevelPairs, configs.rootLevel);
       }

       /**
        * Return the server configs.
        *
        * @return   The server configs.
        */
       public static Configs getConfigs()
       {
          return configs;
       }

       /**
        * Creates an unique name for the logger Thread, parsing the spawner input stream and passes the spawner
        * logger details to the log method.
        *
        * @param    stream
        *           The input stream with log messages coming from the spawner.
        * @param    isSpawnerRunning
        *           Atomic boolean to flag if the spawner is still running.
        */
       public static void logSpawnerStream(InputStream stream, AtomicBoolean isSpawnerRunning)
       {
          logStream(stream, SPAWNER_LOGGER_NAME + "-" + SPAWNER_THREAD_COUNTER.getAndIncrement(),
                    configs.spawnerLevel, null, isSpawnerRunning);
       }

       /**
        * Returns the configs for initializing logging on the client.
        *
        * @return   The configs for initializing logging on the client.
        */
       public static CentralLoggerClientConfig prepareClientConfig()
       {
          DirectoryService ds = DirectoryService.getInstance();
          CentralLoggerServer.Configs serverConfigs = CentralLoggerServer.getConfigs();
          String filePath = Utils.getDirectoryNodeString(ds,
                                                         "logging/file/path",
                                                         DEFAULT_CLIENT_LOG_FILE_NAME,
                                                         Utils.DirScope.BOTH,
                                                         null);
          if (!StringHelper.hasContent(filePath))
          {
             filePath = DEFAULT_CLIENT_LOG_FILE_NAME;
          }

          int fileLimit = Utils.getDirectoryNodeInt(ds,
                                                    "logging/file/rotationLimit",
                                                    serverConfigs.getFileLimit(),
                                                    true);
          int fileCount = Utils.getDirectoryNodeInt(ds,
                                                    "logging/file/rotationCount",
                                                    serverConfigs.getFileCount(),
                                                    true);

          return new CentralLoggerClientConfig(getLoggerLevelMap(),
                                               filePath,
                                               fileLimit,
                                               fileCount,
                                               serverConfigs.isServerSide());
       }

       /**
        * Creates and configures a file handler for the specified client process. With serverSide enabled.
        *
        * @param    filePath
        *           The path to the log file.
        * @param    rotationLimit
        *           The file limit, or the max bytes in a log file before rotation.
        * @param    rotationCount
        *           The file count, or the max number of files before file overwrite starts.
        * @param    pid
        *           The process id of the client.
        * @param    userOS
        *           The OS user.
        * @param    appServerName
        *           The appServer name.
        */
       static void setupClientHandler(String filePath,
                                      int rotationLimit,
                                      int rotationCount,
                                      long pid,
                                      String userOS,
                                      String appServerName)
       {
          if (WRITE_TO_SLF4J_API || clientPidFileHandlerPairs.containsKey(pid))
          {
             // already initialized file handler
             return;
          }
          boolean isRotationDisabled = rotationLimit == 0 || rotationCount == 0;
          String userFwd = SecurityManager.getInstance().getUserId();
          filePath = resolveLogFile(filePath,
                                    () -> pid,
                                    userOS,
                                    userFwd,
                                    appServerName,
                                    "client",
                                    null,
                                    isRotationDisabled);

          LOG.log(Level.INFO,
                  "Configs received: file path %s, rotationLimit %d, rotationCount %d.",
                  filePath,
                  rotationLimit,
                  rotationCount);

          if (!canWriteToPath(filePath))
          {
             filePath = resolveLogFile(DEFAULT_CLIENT_LOG_FILE_NAME,
                                       () -> pid,
                                       userOS,
                                       userFwd,
                                       appServerName,
                                       "client",
                                       null,
                                       isRotationDisabled);
             LOG.log(Level.INFO,
                     "Initializing server-side logging for client (pid %d) with default file path %s.",
                     pid,
                     filePath);
          }

          try
          {
             FileHandler fileHandler = setFileHandler(isRotationDisabled,
                                                      filePath,
                                                      configs.fileLimit,
                                                      configs.fileCount,
                                                      LOG_FORMATTER);
             if (fileHandler != null)
             {
                clientPidFileHandlerPairs.put(pid, fileHandler);

                String logfileAbsolutePath = new File(filePath.replace("%g", "0")).getAbsolutePath();
                EnvironmentOps.setFwdLogfile(logfileAbsolutePath);
             }
          }
          catch (IOException e)
          {
             LOG.log(Level.WARNING, e, "FileHandler for client (pid %d) can't be created.", pid);
          }
       }

       /**
        * Publishes the log record from a client process to its dedicated file handler. All validations are
        * expected to have already be passed on the client side.
        *
        * @param    clr
        *           The central log record.
        */
       static void publishClientLog(CentralLogRecord clr)
       {
          // it's expected the handler is already created at this point, this is just in case
          setupClientHandler(CentralLoggerServer.getConfigs().getFilePath(),
                             configs.fileLimit,
                             configs.fileCount,
                             clr.getPid(),
                             clr.getUserOS(),
                             "");
          if (WRITE_TO_SLF4J_API)
          {
             MDC.put(MdcVar.PID.getKey(), String.valueOf(clr.getPid()));
             MDC.put(MdcVar.OS_USER.getKey(), clr.getUserOS());
             MDC.put(MdcVar.SESSION_ID.getKey(), clr.getSessionId());
             MDC.put(MdcVar.FWD_ACCOUNT.getKey(), clr.getFwdUser());
             MDC.put(MdcVar.THREAD_NAME.getKey(), clr.getThreadName());
             MDC.put(MdcVar.THREAD_ID.getKey(), LoggingUtil.addLeadingZeros(clr.getThreadId(), 8));

             String loggerName = clr.getLoggerName();
             if (!clientLoggerNameSlf4jPairs.containsKey(loggerName))
             {
                clientLoggerNameSlf4jPairs.put(loggerName, LoggerFactory.getLogger(loggerName));
             }
             Logger slf4jLogger = clientLoggerNameSlf4jPairs.get(loggerName);

             getSlf4jLogBuilder(slf4jLogger, clr.toLogRecord()).log();
             return;
          }
          FileHandler clientFileHandler = clientPidFileHandlerPairs.get(clr.getPid());
          clientFileHandler.publish(clr.toLogRecord());
          clientFileHandler.flush();
       }

       /** On JVM shutdown closes log files. */
       static void closeFiles()
       {
          for (FileHandler fileHandler : clientPidFileHandlerPairs.values())
          {
             fileHandler.close();
          }
          if (serverFileHandler != null)
          {
             serverFileHandler.close();
          }
       }

       /**
        * Removes the client process log file handler.
        *
        * @param    clientPid
        *           The client process ID.
        */
       public static void removeHandler(long clientPid)
       {
          if (!clientPidFileHandlerPairs.containsKey(clientPid))
          {
             return;
          }
          FileHandler fileHandler = clientPidFileHandlerPairs.get(clientPid);
          fileHandler.flush();
          fileHandler.close();
          clientPidFileHandlerPairs.remove(clientPid);
       }

       /**
        * Flag to indicate if the LogicalTerminal has been initialized.
        *
        * @param    isLogicalTerminalInitialized
        *           A flag to indicate if the LogicalTerminal has been initialized.
        */
       public static void setLogicalTerminalInitialized(boolean isLogicalTerminalInitialized)
       {
          // TODO: make context aware with #1848
          CentralLoggerServer.isLogicalTerminalInitialized.set(isLogicalTerminalInitialized);
       }

       /**
        * Returns a flag to indicate if the LogicalTerminal has been initialized.
        *
        * @return   <code>true</code> if the LogicalTerminal has been initialized, <code>false</code> otherwise.
        */
       public static boolean isLogicalTerminalInitialized()
       {
          return isLogicalTerminalInitialized.get();
       }

       /**
        * Publishes a log record. Before initialization the log record is added to the pre-init buffer. After
        * initialization the log record is published to the file handler.
        *
        * @param    logRecord
        *           The log record.
        */
       @Override
       protected void publish(ContextLogRecord logRecord)
       {
          if (getRootLevel() == null)
          {
             addToPreInitBuffer(logRecord);
             return;
          }

          if (WRITE_TO_SLF4J_API)
          {
             logToSlf4j(logRecord, true, configs.isServerSide);
          }
          else if (serverFileHandler != null)
          {
             serverFileHandler.publish(logRecord);
             serverFileHandler.flush();
          }

          if (configs.hasConsole)
          {
             System.err.print(LOG_FORMATTER.format(logRecord));
          }
       }

       /**
        * A class to represent all logger configs for the server. All fields have default values and getters.
        * The class can be instantiated only in {@link CentralLoggerServer}. That is supposed to be done by the
        * related builder {@link ConfigsBuilder}. A convenient static method
        * {@link #fromDirectory(DirectoryService)} is available for reading the configs from the server
        * directory
        * and can be used instead of directly calling the builder.
        */
       public static class Configs
       {
          /** A flag indicating if server-side logging enabled. */
          private boolean isServerSide;

          /** A flag indicating if console logging is enabled. */
          private boolean hasConsole;

          /** The path to the log file. */
          private String filePath;

          /** The root logger level. */
          private Level rootLevel = DEFAULT_ROOT_LEVEL;

          /** The spawner logger level. */
          private Level spawnerLevel = DEFAULT_SPAWNER_LEVEL;

          /** The file limit, or the max bytes in a log file before rotation. */
          private int fileLimit = DEFAULT_FILE_LIMIT_BYTES;

          /** The file count, or the max number of files before file overwrite starts. */
          private int fileCount = DEFAULT_FILE_COUNT;

          /** The logger name : level pairs from configs. */
          private Map<String, Level> loggerLevelPairs = new HashMap<>();

          /** Private constructor for centralLogger.Configs. */
          private Configs()
          {
          }

          /**
           * A convenience static method for reading the configs from the server directory. Can be used instead
           * of directly calling the configs builder.
           *
           * @param    ds
           *           The directory service.
           *
           * @return   The configs.
           */
          public static Configs fromDirectory(DirectoryService ds)
          {
             Map<String, Level> loggerLevelPairs = new HashMap<>();
             String loggerLevelsContainer = "logging/loggers";
             String loggersPath = Utils.findDirectoryNodePath(ds, loggerLevelsContainer, "container", false);
             if (loggersPath != null)
             {
                for (String loggerName : ds.enumerateNodes(loggersPath))
                {
                   String levelString = Utils.getDirectoryNodeString(ds,
                                                                     loggerLevelsContainer + "/" + loggerName,
                                                                     null,
                                                                     false);
                   Optional<Level> levelOptional = convertToLevel(levelString);
                   levelOptional.ifPresent(level -> loggerLevelPairs.put(loggerName, level));
                }
             }

             return new ConfigsBuilder()
                .console(Utils.getDirectoryNodeBoolean(ds, "logging/console", false, false))
                .serverSide(Utils.getDirectoryNodeBoolean(ds, "logging/serverSide", false, false))
                .filePath(Utils.getDirectoryNodeString(ds, "logging/file/path", null, false))
                .fileCount(Utils.getDirectoryNodeInt(ds, "logging/file/rotationCount", MISSING_COUNT_CONFIG_PLACEHOLDER, false))
                .fileLimit(Utils.getDirectoryNodeInt(ds, "logging/file/rotationLimit", MISSING_LIMIT_CONFIG_PLACEHOLDER, false))
                .loggerLevelPairs(loggerLevelPairs)
                .build();
          }

          /**
           * Returns the log file path.
           *
           * @return   The log file path.
           */
          public String getFilePath()
          {
             return filePath;
          }

          /**
           * Returns a flag indicating if server-side logging is enabled.
           *
           * @return   A flag indicating if server-side logging enabled.
           */
          public boolean isServerSide()
          {
             return isServerSide;
          }

          /**
           * Returns a flag indicating if console logging is enabled.
           *
           * @return   A flag indicating if console logging is enabled.
           */
          public boolean hasConsole()
          {
             return hasConsole;
          }

          /**
           * Returns the root logger level.
           *
           * @return   The root logger level.
           */
          public Level getRootLevel()
          {
             return rootLevel;
          }

          /**
           * Returns the spawner logger level.
           *
           * @return   The spawner logger level.
           */
          public Level getSpawnerLevel()
          {
             return spawnerLevel;
          }

          /**
           * Returns the file limit (max bytes in a log file before rotation).
           *
           * @return   The file limit.
           */
          public int getFileLimit()
          {
             return fileLimit;
          }

          /**
           * Returns the file count (max number of files before file overwrite starts).
           *
           * @return   The file count.
           */
          public int getFileCount()
          {
             return fileCount;
          }

          /**
           * Returns the string representation of the Configs.
           *
           * @return   The string representation of the Configs.
           */
          @Override
          public String toString()
          {
             return this.getClass().getName() + " " +
                "isServerSide: " +
                isServerSide +
                ", hasConsole: " +
                hasConsole +
                ", filePath: " +
                filePath +
                ", level: " +
                rootLevel +
                ", fileLimit: " +
                fileLimit +
                ", fileCount: " +
                fileCount;
          }
       }

       /** Builder class for {@link Configs} */
       public static class ConfigsBuilder
       {
          /** The {@link Configs} instance to be built. */
          private final Configs configs;

          /** Public constructor for ConfigsBuilder. */
          public ConfigsBuilder()
          {
             configs = new Configs();
          }

          /**
           * Sets the log file. If the specified directory path doesn't exist on the file system, it gets
           * created. If no directory is specified, the JVM launch dir is used.
           *
           * @param    filePath
           *           The log file path.
           *
           * @return   The builder instance to allow method chaining.
           */
          public ConfigsBuilder filePath(String filePath)
          {
             if (!StringHelper.hasContent(filePath))
             {
                filePath = DEFAULT_SERVER_LOG_FILE_NAME;
             }

             configs.filePath = filePath;
             return this;
          }

          /**
           * Set the flag indicating if server-side logging enabled.
           *
           * @param    isServerSide
           *           The flag indicating if server-side logging enabled.
           *
           * @return   The builder instance to allow method chaining.
           */
          public ConfigsBuilder serverSide(boolean isServerSide)
          {
             configs.isServerSide = isServerSide;
             return this;
          }

          /**
           * Set the flag indicating if console logging is enabled.
           *
           * @param    hasConsole
           *           The flag indicating if console logging is enabled.
           *
           * @return   The builder instance to allow method chaining.
           */
          public ConfigsBuilder console(boolean hasConsole)
          {
             configs.hasConsole = hasConsole;
             return this;
          }

          /**
           * Sets the log file limit (max bytes in a log file before rotation).
           *
           * @param    limit
           *           The log file limit.
           *
           * @return   The builder instance to allow method chaining.
           */
          public ConfigsBuilder fileLimit(int limit)
          {
             if (limit == MISSING_LIMIT_CONFIG_PLACEHOLDER)
             {
                configs.fileLimit = DEFAULT_FILE_LIMIT_BYTES;
                return this;
             }
             if (limit == 0)
             {
                // disables rotation
                configs.fileLimit = 0;
                return this;
             }
             configs.fileLimit = Math.max(limit, MIN_FILE_LIMIT_BYTES);
             return this;
          }

          /**
           * Sets the log file count (max number of files before file overwrite starts).
           *
           * @param    count
           *           The log file count.
           *
           * @return   The builder instance to allow method chaining.
           */
          public ConfigsBuilder fileCount(int count)
          {
             if (count == MISSING_COUNT_CONFIG_PLACEHOLDER)
             {
                configs.fileCount = DEFAULT_FILE_COUNT;
                return this;
             }
             if (count == 0)
             {
                // disables rotation
                configs.fileCount = 0;
                return this;
             }
             configs.fileCount = Math.max(count, MIN_FILE_COUNT);
             return this;
          }

          /**
           * Sets the logger name : level pairs from configs.
           *
           * @param    loggerLevelPairs
           *           The logger name : level pairs from configs.
           *
           * @return   The builder instance to allow method chaining.
           */
          public ConfigsBuilder loggerLevelPairs(Map<String, Level> loggerLevelPairs)
          {
             if (loggerLevelPairs.containsKey("root"))
             {
                configs.rootLevel = loggerLevelPairs.get("root");
                loggerLevelPairs.remove("root");
             }

             if (loggerLevelPairs.containsKey("spawner"))
             {
                configs.spawnerLevel = loggerLevelPairs.get("spawner");
                loggerLevelPairs.remove("spawner");
             }

             configs.loggerLevelPairs = loggerLevelPairs;
             return this;
          }

          /**
           * Returns the built Configs instance.
           *
           * @return   The built Configs instance.
           */
          public Configs build()
          {
             return configs;
          }
       }
    }