Logger.java
package org.opentrafficsim.base.logger;
import java.util.IllegalFormatException;
import java.util.Locale;
import java.util.function.Supplier;
import org.djutils.logger.CategoryLogger;
import org.djutils.logger.CategoryLogger.DelegateLogger;
import org.djutils.logger.LogCategory;
import ch.qos.logback.classic.Level;
/**
* Logger for within OTS context. This logger uses category OTS and a format that may include simulation time, PID and thread
* id. Every simulator in OTS can attach itself as simulation time supplier through {@code Logger.setSimtimeSupplier()}. The
* time supplier only applies to the {@link Thread} that creates it, and all threads created downstream of this thread. This
* means that when different simulations run within a single JVM, each log line will report the time of the relevant simulation.
* This does require that the simulator (or animator) is created in a dedicated thread for the parallel simulation. Example
* usage:
*
* <pre>
* Logger.ots().trace("Logging example");
* </pre>
* <p>
* Copyright (c) 2025-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
* BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
* </p>
* @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
*/
public final class Logger
{
/** Default OTS log category. */
public static final LogCategory OTS = new LogCategory("OTS");
/** OTS logging pattern which includes the simulation time. */
private static final String PATTERN =
"%date{HH:mm:ss}%X{time}%X{pid}%X{thread} %-5level %-3logger{0} %class{0}.%method:%line - %msg%n";
/** Delegate logger with OTS category. */
private static final DelegateLogger LOGGER;
/** Default time format. */
private static final String DEFAULT_TIME_FORMAT = "[%8.3fs]";
/** Supplier of simulation time. */
private static InheritableThreadLocal<Supplier<? extends Number>> simTimeSupplier = new InheritableThreadLocal<>();
/** Format to display the time. */
private static InheritableThreadLocal<String> timeFormatString = new InheritableThreadLocal<>();
/** Default PID format. */
private static final String DEFAULT_PID_FORMAT = "pid=%d";
/** Include PID. */
private static boolean includePid = false;
/** PID format. */
private static String pidFormat;
/** Default thread id format. */
private static final String DEFAULT_THREAD_ID_FORMAT = "thread=%d";
/** Include thread id. */
private static boolean includeThreadId = false;
/** Thread id format. */
private static String threadIdFormat;
static
{
CategoryLogger.addLogCategory(OTS);
CategoryLogger.setPattern(OTS, PATTERN);
CategoryLogger.addFormatter(OTS, "time", Logger::getSimTimeString);
CategoryLogger.addFormatter(OTS, "pid", Logger::getPidString);
CategoryLogger.addFormatter(OTS, "thread", Logger::getThreadIdString);
LOGGER = CategoryLogger.with(OTS);
}
/**
* Constructor.
*/
private Logger()
{
//
}
/**
* Returns logger with the OTS category.
* @return logger with the OTS category
*/
public static DelegateLogger ots()
{
return LOGGER;
}
/**
* Set log level of the OTS logger.
* @param level log level
*/
public static void setLogLevel(final Level level)
{
CategoryLogger.setLogLevel(OTS, level);
}
/**
* Sets supplier of the simulation time. The value is formatted with {@code [%8.3fs]}. The time supplier only applies to the
* {@code Thread} that creates it, and all threads created downstream of this thread.
* @param timeSupplier supplier of the simulation time
*/
public static void setSimTimeSupplier(final Supplier<? extends Number> timeSupplier)
{
setSimTimeSupplier(timeSupplier, DEFAULT_TIME_FORMAT);
}
/**
* Sets supplier of the simulation time with dedicated format string. If the format string does not start with a blank, a
* blank is added at the beginning of the format string. The time supplier only applies to the {@code Thread} that creates
* it, and all threads created downstream of this thread.
* @param timeSupplier supplier of the simulation time
* @param timeFormat format string for the time value
*/
public static void setSimTimeSupplier(final Supplier<? extends Number> timeSupplier, final String timeFormat)
{
simTimeSupplier.set(timeSupplier);
timeFormatString.set(formatWithBlank(timeFormat));
}
/**
* Removes the supplier of the simulation time, but only if it is the same as the input argument.
* @param timeSupplier supplier of the simulation time
*/
public static void removeSimTimeSupplier(final Supplier<? extends Number> timeSupplier)
{
if (timeSupplier != null && timeSupplier.equals(simTimeSupplier.get()))
{
simTimeSupplier.remove();
}
}
/**
* Returns the string of the simulation time for the log message.
* @return the string of the simulation time for the log message
*/
private static String getSimTimeString()
{
Supplier<? extends Number> timeSupplier = simTimeSupplier.get();
try
{
return timeSupplier == null ? ""
: String.format(Locale.US, timeFormatString.get(), timeSupplier.get().doubleValue());
}
catch (IllegalFormatException ex)
{
return " T_FORMAT_ERR";
}
}
/**
* Sets the PID to be included in formatting or not. The default format is "pid=%d". To provide a format string use
* {@code includePid(String)}.
* @param include whether to include the PID in formatting
*/
public static void includePid(final boolean include)
{
if (include)
{
includePid(DEFAULT_PID_FORMAT);
return;
}
includePid = false;
}
/**
* Sets the PID to be included in formatting with the given format.
* @param format format for PID
*/
public static void includePid(final String format)
{
includePid = true;
pidFormat = formatWithBlank(format);
}
/**
* Returns the pid (process id).
* @return pid
*/
private static String getPidString()
{
try
{
return includePid ? String.format(pidFormat, ProcessHandle.current().pid()) : "";
}
catch (IllegalFormatException ex)
{
return " PID_FORMAT_ERR";
}
}
/**
* Sets the thread id to be included in formatting or not. The default format is "thread=%d". To provide a format string use
* {@code includeThreadId(String)}.
* @param include whether to include the thread id in formatting
*/
public static void includeThreadId(final boolean include)
{
if (include)
{
includeThreadId(DEFAULT_THREAD_ID_FORMAT);
return;
}
includeThreadId = false;
}
/**
* Sets the PID to be included in formatting with the given format.
* @param format format for PID
*/
public static void includeThreadId(final String format)
{
includeThreadId = true;
threadIdFormat = formatWithBlank(format);
}
/**
* Returns the thread id.
* @return thread id
*/
private static String getThreadIdString()
{
try
{
return includeThreadId ? String.format(threadIdFormat, Thread.currentThread().getId()) : "";
}
catch (IllegalFormatException ex)
{
return " THREAD_FORMAT_ERR";
}
}
/**
* Prepends a non-empty format with a blank space if it does not already starts with a blank.
* @param format format
* @return format that starts with a blank (if not empty)
*/
private static String formatWithBlank(final String format)
{
return format.length() == 0 || format.charAt(0) == ' ' ? format : " " + format;
}
}