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