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