View Javadoc
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 }