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