com.clumd.projects.java_common_utils.logging.LogRoot Maven / Gradle / Ivy
Show all versions of java-common-utils Show documentation
package com.clumd.projects.java_common_utils.logging;
import com.clumd.projects.java_common_utils.files.FileUtils;
import com.clumd.projects.java_common_utils.logging.api.CustomLogHandler;
import com.clumd.projects.java_common_utils.logging.common.CustomLevel;
import com.clumd.projects.java_common_utils.logging.controllers.ConsoleController;
import com.clumd.projects.java_common_utils.logging.controllers.DenseConsoleController;
import com.clumd.projects.java_common_utils.logging.controllers.FileController;
import lombok.NonNull;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Handler;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import java.util.logging.StreamHandler;
/**
* This class is a de-facto root for all Extended Logging functionality.
*
* All methods are Static and this class cannot be instantiated.
*
* The INIT method MUST be called before this LogRoot can be used to generate Loggers
*/
public final class LogRoot {
private static final Map OVERRIDDEN_THREAD_NAME_MAPPINGS = new HashMap<>();
private static final UUID SPECIFIC_RUN_ID = UUID.randomUUID(); // The ID for a specific run, of a specific machine.
private static final int SINGLE_FILE_LOG_SIZE = 10000000; //~10MB in bytes.
private static final int LOG_FILE_ROTATIONS = 3; // max files to keep track of before re-writing old logs.
public static final String TAB = " ";
public static final String ANON_THREAD = "Anon/Unknown Thread";
private static String discardablePackageId;
private static String loggingRootId;
private static String staticSystemName;
private LogRoot() {
// Don't allow this class to be instantiated. It should be used for static method calls only.
}
/**
* Used to initialise the Log Root with some base parameters.
*
* @param discardablePackageIdEndingInDot The package name of the application using the logger, which must end in a
* 'dot' as this is how the package structure in Java is defined. For example
* 'com.x.y.z.' what this will allow is that any logger messages coming from
* WITHIN your defined package, will be able to truncate the often
* unnecessary base package definition in each log message.
* @param loggingRootID The ID which we should use as a core prefix, comparable to an 'App
* reference' for every Log Message created in this application.
* @param systemID The ID of the system running this instance of the program using this
* logger API.
* @return A LogRoot instance, which can then be enhanced with instances of Log Handlers.
*/
public static LogRoot init(
@NonNull final String discardablePackageIdEndingInDot,
@NonNull final String loggingRootID,
final String systemID
) {
LogRoot.discardablePackageId = discardablePackageIdEndingInDot;
LogRoot.loggingRootId = loggingRootID;
// Obtain the local system's name to identify its logfile in a distributed system.
try {
staticSystemName = systemID == null
? InetAddress
.getLocalHost()
.toString()
.replace("/", "-")
.replace("\\\\", "-")
: systemID;
} catch (UnknownHostException e) {
throw new IllegalStateException("Failed to obtain the local system's name.", e);
}
// Remove all parent chaining
Logger root = Logger.getLogger("");
for (Handler h : root.getHandlers()) {
root.removeHandler(h);
}
root = Logger.getLogger(loggingRootId);
for (Handler h : root.getHandlers()) {
root.removeHandler(h);
}
root.setLevel(CustomLevel.ALL);
// Return an instance for self reference to the 'withHandlers' method for nice constructor fluency.
// This method also ensures any default CustomLogControllers used have initialised static variables to reference
return new LogRoot();
}
/**
* As {@link LogRoot#init(String, String, String)}, but with systemID defaulting to null, which will use some
* programmatic ID, involving system hostname.
*/
public static LogRoot init(
@NonNull final String discardablePackageIdEndingInDot,
@NonNull final String loggingRootID
) {
return init(discardablePackageIdEndingInDot, loggingRootID, null);
}
/**
* Used to enhance the LogRoot with instances of Log Handler which will be used to capture all LogRecords written
* during the runtime of the application using this API.
*
* @param wantedLogHandlers The Collection of handlers which we want to be given each message in this app.
*/
public void withHandlers(Collection wantedLogHandlers) {
Logger root = Logger.getLogger("");
// init each wanted handler
for (CustomLogHandler handler : wantedLogHandlers) {
handler.acceptLogRootRefs(SPECIFIC_RUN_ID, staticSystemName, OVERRIDDEN_THREAD_NAME_MAPPINGS);
if (handler instanceof StreamHandler streamHandler) {
root.addHandler(streamHandler);
} else {
throw new IllegalArgumentException("Every custom log controller MUST extend java.util.logging.StreamHandler.");
}
}
}
/**
* Create a basic instance of a Console Handler with various defaults set.
*
* @param useSpacerLines Used to decide whether we want to add additional spacer lines to messages in the console.
* @return The instantiated ConsoleHandler instance.
*/
public static CustomLogHandler basicConsoleHandler(boolean useSpacerLines) {
return new ConsoleController(useSpacerLines);
}
/**
* Creates a sparse no-fluff Console Handler with dense message output.
*
* @return The instantiated ConsoleHandler instance.
*/
public static CustomLogHandler basicDenseConsoleHandler() {
return new DenseConsoleController();
}
/**
* Create a basic instance of a File Handler with various defaults set.
*
* @param atDir The Directory where the system should write its log files to.
* @return The instantiated FileHandler instance.
* @throws IOException Thrown if there was a problem creating or writing to the directory/file you intended.
*/
public static CustomLogHandler basicFileHandler(@NonNull String atDir) throws IOException {
FileUtils.makeAllDirs(atDir);
return new FileController(
atDir + "/" + loggingRootId + "_" + staticSystemName + "_%g.log",
SINGLE_FILE_LOG_SIZE,
LOG_FILE_ROTATIONS,
true
);
}
/**
* As {@link LogRoot#basicFileHandler(String)} but the directory is defaulted to the CURRENT WORKING DIRECTORY WHEN
* THE JAVA PROCESS WAS STARTED.
*
* @return The instantiated FileHandler instance.
* @throws IOException Thrown if there was a problem creating or writing to the directory/file you intended.
*/
public static CustomLogHandler basicFileHandler() throws IOException {
return basicFileHandler(
new File(
System.getProperty("user.dir")
).getAbsolutePath()
);
}
/**
* Used to create a Logger instance by referencing the Class you want the logger for.
*
* @param forClass The Class you would like the logger to be created for.
* @return The instantiated ExtendedLogger
*/
public static ExtendedLogger createLogger(@NonNull final Class> forClass) {
return createLogger(null, forClass.getName());
}
/**
* Used to create a Logger instance by a string name.
*
* If there is some sort of hierarchy in these loggers, such as going into various sub-packages, then these should
* be dot-separated.
*
* @param loggerIdentifier The String name of the class you would like the Logger of.
* @return The instantiated ExtendedLogger
*/
public static ExtendedLogger createLogger(@NonNull String loggerIdentifier) {
return createLogger(null, loggerIdentifier);
}
private static String buildLogName(final String prefix, final String loggerIdentifier) {
if (loggingRootId == null) {
throw new ExceptionInInitializerError("Logging Root ID is not set, have you called the \"LogRoot.init\" method yet?");
}
return loggingRootId
+ '.' + (prefix == null ? "" : prefix + ":")
+ (
loggerIdentifier.startsWith(discardablePackageId)
? loggerIdentifier.substring(discardablePackageId.length())
: loggerIdentifier
);
}
/**
* Used to create a Logger instance by a string name, with a custom prefix.
*
* This may be useful for classes who require Multiple instances of the same logger, but for slightly different
* purposes.
*
* If there is some sort of hierarchy in these loggers, such as going into various sub-packages, then these should
* be dot-separated on the loggerIdentifier, NOT on the prefix.
*
* @param prefix The name of the prefix for this Logger instance.
* @param loggerIdentifier The String name of the class you would like the Logger of.
* @return The instantiated ExtendedLogger
*/
public static ExtendedLogger createLogger(final String prefix, final String loggerIdentifier) {
String loggerName = buildLogName(prefix, loggerIdentifier);
Logger extLog = LogManager.getLogManager().getLogger(loggerName);
if (extLog == null) {
extLog = new ExtendedLogger(loggerName);
LogManager.getLogManager().addLogger(extLog);
}
if (Thread.currentThread().threadId() > 1) {
updateThreadIdName(Thread.currentThread().threadId(), Thread.currentThread().getName());
}
return (ExtendedLogger) extLog;
}
/**
* Used to set the logging level of the indicated branch of package hierarchy.
*
* This will be SPECIFIC to loggers created within this API.
*
* @param selectedLevel The level you would like to set the indicated branch to.
* @param viaLogger The Logger we should use to determine the Branch of logging hierarchy to update the log
* level to.
*/
public static void setBranchLoggingLevel(@NonNull final CustomLevel selectedLevel, @NonNull final Logger viaLogger) {
setGivenLoggersToLevel(
getAllLoggerNames(viaLogger.getName()),
selectedLevel
);
}
/**
* Used to set the logging level of the indicated branch of package hierarchy.
*
* This will be SPECIFIC to loggers created within this API.
*
* @param selectedLevel The level you would like to set the indicated branch to.
* @param viaLogIdentifier The Logger reference we should use to determine the Branch of logging hierarchy to update
* the log level to.
*/
public static void setBranchLoggingLevel(@NonNull final CustomLevel selectedLevel, @NonNull final String viaLogIdentifier) {
String loggerName = buildLogName(null, viaLogIdentifier);
setGivenLoggersToLevel(
getAllLoggerNames(loggerName),
selectedLevel
);
}
/**
* Used to set the logging level of the indicated branch of package hierarchy.
*
* This operation can apply ACROSS ALL LOGGERS KNOWN TO THE JVM.
*
* @param selectedLevel The level you would like to set the indicated branch to.
* @param viaLogIdentifier The Logger reference we should use to determine the Branch of logging hierarchy to update
* the log level to.
*/
public static void setGlobalBranchLoggingLevel(@NonNull final CustomLevel selectedLevel, @NonNull final String viaLogIdentifier) {
setGivenLoggersToLevel(
getAllLoggerNames(viaLogIdentifier),
selectedLevel
);
}
/**
* Used to set the logging level of the indicated branch of package hierarchy.
*
* This will be SPECIFIC to loggers created within this API.
*
* @param selectedLevel The level you would like to set the indicated branch to.
* @param viaLogPrefix The specific prefix necessary to determine an exact Logger instance.
* @param viaLogIdentifier The Logger reference we should use to determine the Branch of logging hierarchy to update
* the log level to.
*/
public static void setBranchLoggingLevel(@NonNull final CustomLevel selectedLevel, final String viaLogPrefix, @NonNull final String viaLogIdentifier) {
String loggerName = buildLogName(viaLogPrefix, viaLogIdentifier);
setGivenLoggersToLevel(
getAllLoggerNames(loggerName),
selectedLevel
);
}
/**
* Set the log level across the entire JVM to the given Custom Level.
*
* Please note, CUSTOM log levels DO NOT necessarily overlap with the baked in JUL log levels.
*
* @param selectedLevel The LogLevel you would like to set across the whole application.
*/
public static void setGlobalLoggingLevel(@NonNull final CustomLevel selectedLevel) {
setGivenLoggersToLevel(
getAllLoggerNames(null),
selectedLevel
);
}
/**
* Set the log level across every Logger created within this API to the given Custom Level, but do NOT touch loggers
* used by 3rd party dependencies.
*
* @param selectedLevel The LogLevel you would like to set for the indicated loggers.
*/
public static void setApplicationGlobalLevel(@NonNull final CustomLevel selectedLevel) {
setGivenLoggersToLevel(
getAllLoggerNames(loggingRootId),
selectedLevel
);
}
public static void updateThreadIdName(long threadID, @NonNull String threadName) {
OVERRIDDEN_THREAD_NAME_MAPPINGS.put(threadID, threadName);
}
private static List getAllLoggerNames(String filteredBy) {
List names = Collections.list(
LogManager
.getLogManager()
.getLoggerNames()
);
if (filteredBy != null) {
return names
.stream()
.filter(n -> n.startsWith(filteredBy))
.toList();
}
return names;
}
private static void setGivenLoggersToLevel(final Collection givenLoggers, final CustomLevel selectedLevel) {
givenLoggers
.forEach(logName -> Logger
.getLogger(logName)
.setLevel(selectedLevel)
);
}
}