1 package org.opentrafficsim.base.logger;
2
3 import java.util.IllegalFormatException;
4 import java.util.Locale;
5 import java.util.function.Supplier;
6
7 import org.djutils.logger.CategoryLogger;
8 import org.djutils.logger.CategoryLogger.DelegateLogger;
9 import org.djutils.logger.LogCategory;
10
11 import ch.qos.logback.classic.Level;
12
13 /**
14 * Logger for within OTS context. This logger uses category OTS and a format that may include simulation time, PID and thread
15 * id. Every simulator in OTS can attach itself as simulation time supplier through {@code Logger.setSimtimeSupplier()}. The
16 * time supplier only applies to the {@link Thread} that creates it, and all threads created downstream of this thread. This
17 * means that when different simulations run within a single JVM, each log line will report the time of the relevant simulation.
18 * This does require that the simulator (or animator) is created in a dedicated thread for the parallel simulation. Example
19 * usage:
20 *
21 * <pre>
22 * Logger.ots().trace("Logging example");
23 * </pre>
24 * <p>
25 * Copyright (c) 2025-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
26 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
27 * </p>
28 * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
29 */
30 public final class Logger
31 {
32
33 /** Default OTS log category. */
34 public static final LogCategory OTS = new LogCategory("OTS");
35
36 /** OTS logging pattern which includes the simulation time. */
37 private static final String PATTERN =
38 "%date{HH:mm:ss}%X{time}%X{pid}%X{thread} %-5level %-3logger{0} %class{0}.%method:%line - %msg%n";
39
40 /** Delegate logger with OTS category. */
41 private static final DelegateLogger LOGGER;
42
43 /** Default time format. */
44 private static final String DEFAULT_TIME_FORMAT = "[%8.3fs]";
45
46 /** Supplier of simulation time. */
47 private static InheritableThreadLocal<Supplier<? extends Number>> simTimeSupplier = new InheritableThreadLocal<>();
48
49 /** Format to display the time. */
50 private static InheritableThreadLocal<String> timeFormatString = new InheritableThreadLocal<>();
51
52 /** Default PID format. */
53 private static final String DEFAULT_PID_FORMAT = "pid=%d";
54
55 /** Include PID. */
56 private static boolean includePid = false;
57
58 /** PID format. */
59 private static String pidFormat;
60
61 /** Default thread id format. */
62 private static final String DEFAULT_THREAD_ID_FORMAT = "thread=%d";
63
64 /** Include thread id. */
65 private static boolean includeThreadId = false;
66
67 /** Thread id format. */
68 private static String threadIdFormat;
69
70 static
71 {
72 CategoryLogger.addLogCategory(OTS);
73 CategoryLogger.setPattern(OTS, PATTERN);
74 CategoryLogger.addFormatter(OTS, "time", Logger::getSimTimeString);
75 CategoryLogger.addFormatter(OTS, "pid", Logger::getPidString);
76 CategoryLogger.addFormatter(OTS, "thread", Logger::getThreadIdString);
77 LOGGER = CategoryLogger.with(OTS);
78 }
79
80 /**
81 * Constructor.
82 */
83 private Logger()
84 {
85 //
86 }
87
88 /**
89 * Returns logger with the OTS category.
90 * @return logger with the OTS category
91 */
92 public static DelegateLogger ots()
93 {
94 return LOGGER;
95 }
96
97 /**
98 * Set log level of the OTS logger.
99 * @param level log level
100 */
101 public static void setLogLevel(final Level level)
102 {
103 CategoryLogger.setLogLevel(OTS, level);
104 }
105
106 /**
107 * Sets supplier of the simulation time. The value is formatted with {@code [%8.3fs]}. The time supplier only applies to the
108 * {@code Thread} that creates it, and all threads created downstream of this thread.
109 * @param timeSupplier supplier of the simulation time
110 */
111 public static void setSimTimeSupplier(final Supplier<? extends Number> timeSupplier)
112 {
113 setSimTimeSupplier(timeSupplier, DEFAULT_TIME_FORMAT);
114 }
115
116 /**
117 * Sets supplier of the simulation time with dedicated format string. If the format string does not start with a blank, a
118 * blank is added at the beginning of the format string. The time supplier only applies to the {@code Thread} that creates
119 * it, and all threads created downstream of this thread.
120 * @param timeSupplier supplier of the simulation time
121 * @param timeFormat format string for the time value
122 */
123 public static void setSimTimeSupplier(final Supplier<? extends Number> timeSupplier, final String timeFormat)
124 {
125 simTimeSupplier.set(timeSupplier);
126 timeFormatString.set(formatWithBlank(timeFormat));
127 }
128
129 /**
130 * Removes the supplier of the simulation time, but only if it is the same as the input argument.
131 * @param timeSupplier supplier of the simulation time
132 */
133 public static void removeSimTimeSupplier(final Supplier<? extends Number> timeSupplier)
134 {
135 if (timeSupplier != null && timeSupplier.equals(simTimeSupplier.get()))
136 {
137 simTimeSupplier.remove();
138 }
139 }
140
141 /**
142 * Returns the string of the simulation time for the log message.
143 * @return the string of the simulation time for the log message
144 */
145 private static String getSimTimeString()
146 {
147 Supplier<? extends Number> timeSupplier = simTimeSupplier.get();
148 try
149 {
150 return timeSupplier == null ? ""
151 : String.format(Locale.US, timeFormatString.get(), timeSupplier.get().doubleValue());
152 }
153 catch (IllegalFormatException ex)
154 {
155 return " T_FORMAT_ERR";
156 }
157 }
158
159 /**
160 * Sets the PID to be included in formatting or not. The default format is "pid=%d". To provide a format string use
161 * {@code includePid(String)}.
162 * @param include whether to include the PID in formatting
163 */
164 public static void includePid(final boolean include)
165 {
166 if (include)
167 {
168 includePid(DEFAULT_PID_FORMAT);
169 return;
170 }
171 includePid = false;
172 }
173
174 /**
175 * Sets the PID to be included in formatting with the given format.
176 * @param format format for PID
177 */
178 public static void includePid(final String format)
179 {
180 includePid = true;
181 pidFormat = formatWithBlank(format);
182 }
183
184 /**
185 * Returns the pid (process id).
186 * @return pid
187 */
188 private static String getPidString()
189 {
190 try
191 {
192 return includePid ? String.format(pidFormat, ProcessHandle.current().pid()) : "";
193 }
194 catch (IllegalFormatException ex)
195 {
196 return " PID_FORMAT_ERR";
197 }
198 }
199
200 /**
201 * Sets the thread id to be included in formatting or not. The default format is "thread=%d". To provide a format string use
202 * {@code includeThreadId(String)}.
203 * @param include whether to include the thread id in formatting
204 */
205 public static void includeThreadId(final boolean include)
206 {
207 if (include)
208 {
209 includeThreadId(DEFAULT_THREAD_ID_FORMAT);
210 return;
211 }
212 includeThreadId = false;
213 }
214
215 /**
216 * Sets the PID to be included in formatting with the given format.
217 * @param format format for PID
218 */
219 public static void includeThreadId(final String format)
220 {
221 includeThreadId = true;
222 threadIdFormat = formatWithBlank(format);
223 }
224
225 /**
226 * Returns the thread id.
227 * @return thread id
228 */
229 private static String getThreadIdString()
230 {
231 try
232 {
233 return includeThreadId ? String.format(threadIdFormat, Thread.currentThread().getId()) : "";
234 }
235 catch (IllegalFormatException ex)
236 {
237 return " THREAD_FORMAT_ERR";
238 }
239 }
240
241 /**
242 * Prepends a non-empty format with a blank space if it does not already starts with a blank.
243 * @param format format
244 * @return format that starts with a blank (if not empty)
245 */
246 private static String formatWithBlank(final String format)
247 {
248 return format.length() == 0 || format.charAt(0) == ' ' ? format : " " + format;
249 }
250
251 }