1 /**
2  * The handler module defines the `LogHandler` interface, as well as several
3  * simple handler implementations that can be used or extended by SLF4D
4  * providers.
5  */
6 module slf4d.handler;
7 
8 import slf4d.logger : LogMessage;
9 import slf4d.level;
10 
11 /**
12  * The interface for any component that consumes log messages generated by a
13  * `Logger`. Only messages whose level is greater than or equal to the logger's
14  * level will be sent to handlers. For example, a Logger configured at an INFO
15  * log level will send INFO, WARN, and ERROR messages, but not DEBUG or TRACE.
16  *
17  * Each Logger has a single root LogHandler instance. This "root handler" can
18  * be a very simple handler that sends messages to stdout, or it could be a
19  * more complex composition of handlers to distribute logs to various locations
20  * according to filtering logic.
21  */
22 shared interface LogHandler {
23     /** 
24      * Handles a log message.
25      * Params:
26      *   msg = The log message that was generated.
27      */
28     void handle(immutable LogMessage msg);
29 }
30 
31 /**
32  * A log handler that serializes incoming messages, and uses one or more
33  * `LogWriter` instances to write the serialized messages to some output
34  * resources, like the console, files, or network devices.
35  */
36 shared class SerializingLogHandler : LogHandler {
37     import slf4d.writer : LogSerializer, LogWriter;
38 
39     private LogSerializer serializer;
40     private LogWriter[] writers;
41 
42     public this(shared LogSerializer serializer, shared LogWriter[] writers) {
43         this.serializer = serializer;
44         this.writers = writers;
45     }
46 
47     void handle(immutable LogMessage msg) {
48         import std.stdio;
49         try {
50             string serialized = this.serializer.serialize(msg);
51             foreach (writer; this.writers) {
52                 try {
53                     writer.write(msg, serialized);
54                 } catch (Exception e) {
55                     stderr.writefln!"Failed to write log message: %s"(e.msg);
56                 }
57             }
58         } catch (Exception e) {
59             stderr.writefln!"Failed to serialize log message: %s"(e.msg);
60         }
61     }
62 }
63 
64 unittest {
65     import slf4d.writer;
66     import slf4d.logger;
67 
68     class TestSerializer : LogSerializer {
69         string serialize(immutable LogMessage msg) shared {
70             return msg.message;
71         }
72     }
73 
74     class TestWriter : LogWriter {
75         string[] messages;
76         void write(immutable LogMessage _, string serializedMessage) shared {
77             messages ~= serializedMessage;
78         }
79     }
80 
81     auto writer = new shared TestWriter();
82     auto writer2 = new shared TestWriter();
83     auto handler = new SerializingLogHandler(
84         new shared TestSerializer(),
85         [writer, writer2]
86     );
87     Logger logger = Logger(handler);
88     logger.info("Testing");
89     logger.warn("This is another message.");
90     assert(writer.messages.length == 2);
91     assert(writer.messages[0] == "Testing");
92     assert(writer.messages[1] == "This is another message.");
93     assert(writer2.messages.length == 2);
94     assert(writer2.messages[0] == "Testing");
95     assert(writer2.messages[1] == "This is another message.");
96 }
97 
98 /**
99  * A log handler that discards all messages. Useful for testing.
100  */
101 class DiscardingLogHandler : LogHandler {
102     public void handle(immutable LogMessage _) shared {
103         // Do nothing.
104     }
105 }
106 
107 /**
108  * A log handler that simply appends all messages it receives to an internal
109  * array. This can be useful for testing, but should not be used for general
110  * runtime logging due to the need to synchronize access to the internal
111  * message cache.
112  */
113 shared class CachingLogHandler : LogHandler {
114     /**
115      * The internal cache of messages.
116      */
117     private LogMessage[] messages;
118 
119     /**
120      * "Handles" a log message by appending it to this handler's internal list
121      * of messages, for later inspection.
122      * Params:
123      *   msg = The message to handle.
124      */
125     public void handle(immutable LogMessage msg) shared {
126         synchronized(this) {
127             this.messages ~= msg;
128         }
129     }
130 
131     /**
132      * Resets this handler's internal message cache to an empty list.
133      */
134     public void reset() {
135         synchronized(this) {
136             this.messages = [];
137         }
138     }
139 
140     /**
141      * Gets an immutable duplication of the list of messages this handler has
142      * handled since the last `reset()` call.
143      * Returns: The list of messages.
144      */
145     public LogMessage[] getMessages() {
146         synchronized(this) {
147             return cast(LogMessage[]) messages.idup;
148         }
149     }
150 
151     /**
152      * Gets the number of messages that this handler has handled since the last
153      * `reset()` call.
154      * Returns: The number of messages.
155      */
156     public size_t messageCount() {
157         synchronized(this) {
158             return messages.length;
159         }
160     }
161 
162     /**
163      * Determines if no messages have been logged since the last time `reset()`
164      * was called.
165      * Returns: True if there are no messages, or false otherwise.
166      */
167     public bool empty() {
168         return messageCount == 0;
169     }
170 }
171 
172 unittest {// Test basic functionality.
173     import slf4d.logger : Logger;
174     auto handler = new CachingLogHandler();
175     auto logger = Logger(handler);
176     assert(handler.getMessages().length == 0);
177     logger.info("Hello world!");
178     assert(handler.getMessages().length == 1);
179 }
180 
181 unittest {// Test thread-safety.
182     import core.thread;
183     import slf4d.logger : Logger;
184     CachingLogHandler handler = new CachingLogHandler();
185     Thread[] threads;
186     for (int i = 0; i < 10; i++) {
187         threads ~= new Thread({
188             Logger logger = Logger(handler);
189             for (int j = 0; j < 100; j++) {
190                 logger.infoF!"Message from thread %s: %d"(Thread.getThis().name, j);
191             }
192         });
193     }
194     foreach (t; threads) t.start();
195     foreach (t; threads) t.join();
196     // Check that all messages were logged.
197     assert(handler.messageCount() == 1000);
198 }
199 
200 /**
201  * A log handler that simply passes any log message it receives to a list of
202  * other handlers. Note that handlers should only be added at application
203  * startup, because the `handle` method is not synchronized to improve
204  * performance.
205  */
206 shared class MultiLogHandler : LogHandler {
207     private shared(LogHandler)[] handlers;
208 
209     /**
210      * Constructs this multi-log handler using the given list of sub-handlers.
211      * Params:
212      *   handlers = The handlers that should each handle every message this
213      *              multi-handler receives.
214      */
215     public this(shared(LogHandler)[] handlers) {
216         this.handlers = handlers;
217     }
218 
219     /**
220      * Adds a handler to this multi-handler's list of handlers.
221      * Params:
222      *   handler = The handler to add.
223      * Returns: A reference to this multi-handler.
224      */
225     public MultiLogHandler addHandler(shared(LogHandler) handler) {
226         this.handlers ~= handler;
227         return this;
228     }
229 
230     /**
231      * Handles a log message by calling each of its sub-handlers' `handle`
232      * methods on it.
233      * Params:
234      *   msg = The message to handle.
235      */
236     public void handle(immutable LogMessage msg) shared {
237         foreach (handler; handlers) {
238             handler.handle(msg);
239         }
240     }
241 }
242 
243 unittest {
244     import slf4d.logger : Logger;
245     auto h1 = new CachingLogHandler();
246     auto h2 = new CachingLogHandler();
247     auto multiHandler = new MultiLogHandler([h1, h2]);
248     auto logger = Logger(multiHandler);
249     logger.info("Hello world!");
250     assert(h1.getMessages().length == 1);
251     assert(h2.getMessages().length == 1);
252 }
253 
254 /**
255  * A handler that applies a filter to log messages, and only passes messages to
256  * its internal handler if the filter returns `true`.
257  */
258 shared class FilterLogHandler : LogHandler {
259     private bool function (LogMessage) filterFunction;
260     private LogHandler handler;
261 
262     public this(shared(LogHandler) handler, bool function (LogMessage) filterFunction) {
263         this.handler = handler;
264         this.filterFunction = filterFunction;
265     }
266 
267     void handle(immutable LogMessage msg) {
268         if (this.filterFunction(msg)) {
269             this.handler.handle(msg);
270         }
271     }
272 }
273 
274 unittest {
275     import slf4d.logger : Logger;
276     auto baseHandler = new CachingLogHandler();
277     auto filterHandler = new FilterLogHandler(
278         baseHandler,
279         (msg) {
280             return msg.message.length > 10;
281         }
282     );
283     Logger logger = Logger(filterHandler);
284     logger.info("Testing");
285     assert(baseHandler.getMessages().length == 0);
286     logger.info("This is a long string!");
287     assert(baseHandler.getMessages().length == 1);
288 }
289 
290 /**
291  * A handler that sends log messages to different handlers depending on the
292  * level of the message. For example, you could create a level-mapped log
293  * handler that sends INFO messages to stdout, but sends ERROR messages to
294  * an email notification service.
295  */
296 shared class LevelMappedLogHandler : LogHandler {
297 
298     private static struct LevelRange {
299         public const int minValue;
300         public const bool hasMinValue;
301         public const int maxValue;
302         public const bool hasMaxValue;
303 
304         public static LevelRange infinite() {
305             return LevelRange(-1, false, -1, false);
306         }
307 
308         public static LevelRange of(int minValue, int maxValue) {
309             return LevelRange(minValue, true, maxValue, true);
310         }
311 
312         public static LevelRange of(int value) {
313             return LevelRange(value, true, value, true);
314         }
315 
316         public static LevelRange ofMin(int minValue) {
317             return LevelRange(minValue, true, -1, false);
318         }
319 
320         public static LevelRange ofMax(int maxValue) {
321             return LevelRange(-1, false, maxValue, true);
322         }
323     }
324 
325     private static struct Mapping {
326         public const LevelRange range;
327         public shared(LogHandler) handler;
328     }
329 
330     private Mapping[] mappings;
331 
332     public void addLevelMapping(Level level, shared(LogHandler) handler) {
333         this.mappings ~= cast(shared(Mapping)) Mapping(LevelRange.of(level.value), handler);
334     }
335 
336     public void addRangeLevelMapping(Level minLevel, Level maxLevel, shared(LogHandler) handler) {
337         if (minLevel.value > maxLevel.value) {
338             Level tmp = minLevel;
339             minLevel = maxLevel;
340             maxLevel = tmp;
341         }
342         this.mappings ~= cast(shared(Mapping)) Mapping(LevelRange.of(minLevel.value, maxLevel.value), handler);
343     }
344 
345     public void addMinLevelMapping(Level minLevel, shared(LogHandler) handler) {
346         this.mappings ~= cast(shared(Mapping)) Mapping(LevelRange.ofMin(minLevel.value), handler);
347     }
348 
349     public void addMaxLevelMapping(Level maxLevel, shared(LogHandler) handler) {
350         this.mappings ~= cast(shared(Mapping)) Mapping(LevelRange.ofMax(maxLevel.value), handler);
351     }
352 
353     public void addAnyLevelMapping(shared(LogHandler) handler) {
354         this.mappings ~= cast(shared(Mapping)) Mapping(LevelRange.infinite, handler);
355     }
356 
357     public void handle(immutable LogMessage msg) shared {
358         foreach (mapping; mappings) {
359             if (
360                 (!mapping.range.hasMinValue || msg.level.value >= mapping.range.minValue) &&
361                 (!mapping.range.hasMaxValue || msg.level.value <= mapping.range.maxValue)
362             ) {
363                 mapping.handler.handle(msg);
364             }
365         }
366     }
367 }
368 
369 unittest {
370     import slf4d.logger : Logger;
371     CachingLogHandler baseHandler = new CachingLogHandler();
372     
373     // Test single-level mappings.
374     auto handler = new LevelMappedLogHandler();
375     handler.addLevelMapping(Levels.INFO, baseHandler);
376 
377     Logger logger = Logger(handler);
378     logger.debug_("This should not be logged.");
379     assert(baseHandler.empty);
380     logger.warn("This should also not be logged.");
381     assert(baseHandler.empty);
382     logger.trace("Also not logged.");
383     assert(baseHandler.empty);
384     logger.info("This should be logged!");
385     assert(baseHandler.messageCount == 1);
386     baseHandler.reset();
387     
388     // Test range mappings.
389     handler = new LevelMappedLogHandler();
390     handler.addRangeLevelMapping(Levels.DEBUG, Levels.WARN, baseHandler);
391 
392     Logger logger2 = Logger(handler);
393     logger2.trace("This should not be logged.");
394     assert(baseHandler.empty);
395     logger2.error("This should also not be logged.");
396     assert(baseHandler.empty);
397     logger2.warn("This should be logged.");
398     logger2.info("Also this!");
399     logger2.debug_("And this!");
400     assert(baseHandler.messageCount == 3);
401     baseHandler.reset();
402 
403     // Test min mappings.
404     handler = new LevelMappedLogHandler();
405     handler.addMinLevelMapping(Levels.INFO, baseHandler);
406 
407     Logger logger3 = Logger(handler);
408     logger3.debug_("This should not be logged.");
409     assert(baseHandler.empty);
410     logger3.info("This should be logged.");
411     logger3.error("This too!");
412     assert(baseHandler.messageCount == 2);
413     baseHandler.reset();
414 
415     // Test max mappings.
416     handler = new LevelMappedLogHandler();
417     handler.addMaxLevelMapping(Levels.WARN, baseHandler);
418     
419     Logger logger4 = Logger(handler);
420     logger4.error("This should not be logged.");
421     assert(baseHandler.empty);
422     logger4.warn("This should be logged.");
423     logger4.trace("This too!");
424     assert(baseHandler.messageCount == 2);
425 }