1 /**
2  * This module defines the `Logger`; the central component from which all log
3  * messages originate. You 
4  */
5 module slf4d.logger;
6 
7 import slf4d.level;
8 import slf4d.handler;
9 import std.datetime : Clock, SysTime;
10 import std.typecons : Nullable, nullable;
11 import std.file;
12 
13 /** 
14  * The logger is the core component of SLF4D. Use it to generate log messages
15  * in your code. An ad-hoc Logger can be created and used anywhere, but usually
16  * you'll want to use the `getLogger()` function to obtain a pre-configured
17  * Logger that has been set up with an application-specific LogHandler.
18  * 
19  * Note that the D language offers some special keywords like `__MODULE__` and
20  * `__PRETTY_FUNCTION__` which at compile time resolve to the respective source
21  * symbols, and are used heavily here for adding context to log messages.
22  * Because these must be resolved in the actual source location and *not* in
23  * the logger's own code, we make use of default values for most logging
24  * functions. Functions will usually end with a list of arguments that default
25  * to these keywords, and you shouldn't need to ever provide a value for these.
26  */
27 struct Logger {
28     private shared LogHandler handler;
29     public const Level level;
30     public const string name;
31 
32     /** 
33      * Initializes a new logger. Usually, you won't use this constructor, and
34      * instead, you should obtain a Logger via `loggerFactory.getLogger()`.
35      * Params:
36      *   handler = The handler to handle log messages.
37      *   level = The log level of this logger. Only log messages with a level
38      *           of equal or greater severity will be logged by this logger.
39      *   name = The name of the logger. It defaults to the name of the module
40      *          where the logger was initialized.
41      */
42     public this(shared LogHandler handler, Level level = Levels.TRACE, string name = __MODULE__) {
43         this.handler = handler;
44         this.level = level;
45         this.name = name;
46     }
47 
48     /** 
49      * Gets a builder that can be used to fluently build a log message.
50      * Returns: The builder.
51      */
52     public LogBuilder builder() {
53         return LogBuilder.forLogger(this);
54     }
55 
56     /** 
57      * Logs a message. Only messages whose level is greater than or equal to
58      * this logger's level will be logged.
59      * Params:
60      *   msg = The message to log.
61      */
62     public void log(immutable LogMessage msg) {
63         if (this.level.value <= msg.level.value) {
64             this.handler.handle(msg);
65         }
66     }
67 
68     // Test the basic `log` method to ensure log messages are filtered according to the Logger's level.
69     unittest {
70         auto handler = new CachingLogHandler();
71         Logger log = Logger(handler, Levels.INFO);
72 
73         // Test logging something that's of a sufficient level.
74         LogMessage errorMessage = LogMessage(
75             Levels.ERROR,
76             "Oh no!",
77             log.name,
78             Clock.currTime(),
79             ExceptionInfo.from(new Exception("Oh no!")),
80             LogMessageSourceContext()
81         );
82         log.log(errorMessage);
83         LogMessage lastMessage = handler.getMessages()[0];
84         assert(lastMessage == errorMessage);
85         handler.reset();
86 
87         // Test logging something that's not of a sufficient level, and should not show up.
88         LogMessage traceMessage = LogMessage(
89             Levels.TRACE,
90             "Trace message",
91             log.name,
92             Clock.currTime(),
93             ExceptionInfo.from(null),
94             LogMessageSourceContext()
95         );
96         log.log(traceMessage);
97         assert(handler.empty);
98     }
99 
100     /** 
101      * Logs a message.
102      * Params:
103      *   level = The log level.
104      *   msg = The string message to log.
105      *   exception = The exception that prompted this log. This may be null.
106      *   attributes = Key-value pairs of additional user-defined attributes.
107      *   moduleName = The name of the module. This will resolve to the current
108      *                module name by default.
109      *   functionName = The name of the function. This will resolve to the
110      *                current function name by default.
111      *   fileName = The name of the source file. This will resolve to the
112      *              current source file by default.
113      *   lineNumber = The line number in the source file. This will resolve to
114      *                the current source file's line number by default.
115      */
116     public void log(
117         Level level,
118         string msg,
119         Exception exception = null,
120         string[string] attributes = string[string].init,
121         string moduleName = __MODULE__,
122         string functionName = __PRETTY_FUNCTION__,
123         string fileName = __FILE__,
124         size_t lineNumber = __LINE__
125     ) {
126         if (attributes is null) {
127             attributes = string[string].init;
128         }
129         this.log(LogMessage(
130             level,
131             msg,
132             this.name,
133             Clock.currTime(),
134             ExceptionInfo.from(exception),
135             LogMessageSourceContext(moduleName, functionName, fileName, lineNumber),
136             cast(immutable(string[string])) attributes.dup
137         ));
138     }
139 
140     /** 
141      * Logs an exception message. It creates a basic message string formatted
142      * like "ExceptionName: message".
143      * Params:
144      *   level = The log level.
145      *   exception = The exception that prompted this log.
146      *   attributes = Key-value pairs of additional user-defined attributes.
147      *   moduleName = The name of the module. This will resolve to the current
148      *                module name by default.
149      *   functionName = The name of the function. This will resolve to the
150      *                current function name by default.
151      *   fileName = The name of the source file. This will resolve to the
152      *              current source file by default.
153      *   lineNumber = The line number in the source file. This will resolve to
154      *                the current source file's line number by default.
155      */
156     public void log(
157         Level level,
158         Exception exception,
159         string[string] attributes = string[string].init,
160         string moduleName = __MODULE__,
161         string functionName = __PRETTY_FUNCTION__,
162         string fileName = __FILE__,
163         size_t lineNumber = __LINE__
164     ) {
165         string msg = exception.classinfo.name ~ ": " ~ exception.msg;
166         this.log(level, msg, exception, attributes, moduleName, functionName, fileName, lineNumber);
167     }
168 
169     /** 
170      * Logs a formatted string message.
171      * Params:
172      *   level = The log level.
173      *   args = The arguments for the formatted string.
174      *   exception = The exception that prompted this log. This may be null.
175      *   attributes = Key-value pairs of additional user-defined attributes.
176      *   moduleName = The name of the module. This will resolve to the current
177      *                module name by default.
178      *   functionName = The name of the function. This will resolve to the
179      *                current function name by default.
180      *   fileName = The name of the source file. This will resolve to the
181      *              current source file by default.
182      *   lineNumber = The line number in the source file. This will resolve to
183      *                the current source file's line number by default.
184      */
185     public void logF(string fmt, T...)(
186         Level level,
187         T args,
188         Exception exception = null,
189         string[string] attributes = string[string].init,
190         string moduleName = __MODULE__,
191         string functionName = __PRETTY_FUNCTION__,
192         string fileName = __FILE__,
193         size_t lineNumber = __LINE__
194     ) {
195         if (this.level.value <= level.value) {
196             import std.format : format;
197             this.log(level, format!(fmt)(args), exception, attributes, moduleName, functionName, fileName, lineNumber);
198         }
199     }
200 
201 
202     public void trace(
203         string msg,
204         Exception exception = null,
205         string[string] attributes = string[string].init,
206         string moduleName = __MODULE__,
207         string functionName = __PRETTY_FUNCTION__,
208         string fileName = __FILE__,
209         size_t lineNumber = __LINE__
210     ) {
211         this.log(Levels.TRACE, msg, exception, attributes, moduleName, functionName, fileName, lineNumber);
212     }
213 
214     public void trace(
215         Exception exception,
216         string[string] attributes = string[string].init,
217         string moduleName = __MODULE__,
218         string functionName = __PRETTY_FUNCTION__,
219         string fileName = __FILE__,
220         size_t lineNumber = __LINE__
221     ) {
222         this.log(Levels.TRACE, exception, attributes, moduleName, functionName, fileName, lineNumber);
223     }
224 
225     public void traceF(string fmt, T...)(
226         T args,
227         Exception exception = null,
228         string[string] attributes = string[string].init,
229         string moduleName = __MODULE__,
230         string functionName = __PRETTY_FUNCTION__,
231         string fileName = __FILE__,
232         size_t lineNumber = __LINE__
233     ) {
234         this.logF!(fmt, T)(Levels.TRACE, args, exception, attributes, moduleName, functionName, fileName, lineNumber);
235     }
236 
237     public LogBuilder traceBuilder() {
238         return LogBuilder.forLogger(this).lvl(Levels.TRACE);
239     }
240 
241     public void debug_(
242         string msg,
243         Exception exception = null,
244         string[string] attributes = string[string].init,
245         string moduleName = __MODULE__,
246         string functionName = __PRETTY_FUNCTION__,
247         string fileName = __FILE__,
248         size_t lineNumber = __LINE__
249     ) {
250         this.log(Levels.DEBUG, msg, exception, attributes, moduleName, functionName, fileName, lineNumber);
251     }
252 
253     public void debug_(
254         Exception exception,
255         string[string] attributes = string[string].init,
256         string moduleName = __MODULE__,
257         string functionName = __PRETTY_FUNCTION__,
258         string fileName = __FILE__,
259         size_t lineNumber = __LINE__
260     ) {
261         this.log(Levels.DEBUG, exception, attributes, moduleName, functionName, fileName, lineNumber);
262     }
263 
264     public void debugF(string fmt, T...)(
265         T args,
266         Exception exception = null,
267         string[string] attributes = string[string].init,
268         string moduleName = __MODULE__,
269         string functionName = __PRETTY_FUNCTION__,
270         string fileName = __FILE__,
271         size_t lineNumber = __LINE__
272     ) {
273         this.logF!(fmt, T)(Levels.DEBUG, args, exception, attributes, moduleName, functionName, fileName, lineNumber);
274     }
275 
276     public LogBuilder debugBuilder() {
277         return LogBuilder.forLogger(this).lvl(Levels.DEBUG);
278     }
279 
280     public void info(
281         string msg,
282         Exception exception = null,
283         string[string] attributes = string[string].init,
284         string moduleName = __MODULE__,
285         string functionName = __PRETTY_FUNCTION__,
286         string fileName = __FILE__,
287         size_t lineNumber = __LINE__
288     ) {
289         this.log(Levels.INFO, msg, exception, attributes, moduleName, functionName, fileName, lineNumber);
290     }
291 
292     public void info(
293         Exception exception,
294         string[string] attributes = string[string].init,
295         string moduleName = __MODULE__,
296         string functionName = __PRETTY_FUNCTION__,
297         string fileName = __FILE__,
298         size_t lineNumber = __LINE__
299     ) {
300         this.log(Levels.INFO, exception, attributes, moduleName, functionName, fileName, lineNumber);
301     }
302 
303     public void infoF(string fmt, T...)(
304         T args,
305         Exception exception = null,
306         string[string] attributes = string[string].init,
307         string moduleName = __MODULE__,
308         string functionName = __PRETTY_FUNCTION__,
309         string fileName = __FILE__,
310         size_t lineNumber = __LINE__
311     ) {
312         this.logF!(fmt, T)(Levels.INFO, args, exception, attributes, moduleName, functionName, fileName, lineNumber);
313     }
314 
315     public LogBuilder infoBuilder() {
316         return LogBuilder.forLogger(this).lvl(Levels.INFO);
317     }
318 
319     public void warn(
320         string msg,
321         Exception exception = null,
322         string[string] attributes = string[string].init,
323         string moduleName = __MODULE__,
324         string functionName = __PRETTY_FUNCTION__,
325         string fileName = __FILE__,
326         size_t lineNumber = __LINE__
327     ) {
328         this.log(Levels.WARN, msg, exception, attributes, moduleName, functionName, fileName, lineNumber);
329     }
330 
331     public void warn(
332         Exception exception,
333         string[string] attributes = string[string].init,
334         string moduleName = __MODULE__,
335         string functionName = __PRETTY_FUNCTION__,
336         string fileName = __FILE__,
337         size_t lineNumber = __LINE__
338     ) {
339         this.log(Levels.WARN, exception, attributes, moduleName, functionName, fileName, lineNumber);
340     }
341 
342     public void warnF(string fmt, T...)(
343         T args,
344         Exception exception = null,
345         string[string] attributes = string[string].init,
346         string moduleName = __MODULE__,
347         string functionName = __PRETTY_FUNCTION__,
348         string fileName = __FILE__,
349         size_t lineNumber = __LINE__
350     ) {
351         this.logF!(fmt, T)(Levels.WARN, args, exception, attributes, moduleName, functionName, fileName, lineNumber);
352     }
353 
354     public LogBuilder warnBuilder() {
355         return LogBuilder.forLogger(this).lvl(Levels.WARN);
356     }
357 
358     public void error(
359         string msg,
360         Exception exception = null,
361         string[string] attributes = string[string].init,
362         string moduleName = __MODULE__,
363         string functionName = __PRETTY_FUNCTION__,
364         string fileName = __FILE__,
365         size_t lineNumber = __LINE__
366     ) {
367         this.log(Levels.ERROR, msg, exception, attributes, moduleName, functionName, fileName, lineNumber);
368     }
369 
370     public void error(
371         Exception exception,
372         string[string] attributes = string[string].init,
373         string moduleName = __MODULE__,
374         string functionName = __PRETTY_FUNCTION__,
375         string fileName = __FILE__,
376         size_t lineNumber = __LINE__
377     ) {
378         this.log(Levels.ERROR, exception, attributes, moduleName, functionName, fileName, lineNumber);
379     }
380 
381     public void errorF(string fmt, T...)(
382         T args,
383         Exception exception = null,
384         string[string] attributes = string[string].init,
385         string moduleName = __MODULE__,
386         string functionName = __PRETTY_FUNCTION__,
387         string fileName = __FILE__,
388         size_t lineNumber = __LINE__
389     ) {
390         this.logF!(fmt, T)(Levels.ERROR, args, exception, attributes, moduleName, functionName, fileName, lineNumber);
391     }
392 
393     public LogBuilder errorBuilder() {
394         return LogBuilder.forLogger(this).lvl(Levels.ERROR);
395     }
396 
397     // General test for all functions.
398     unittest {
399         import slf4d.testing_provider;
400         auto p = new TestingLoggingProvider();
401         Logger log = p.getLoggerFactory().getLogger();
402 
403         log.log(Levels.INFO, "Hello world");
404         assert(p.messageCount == 1);
405         log.logF!"Test %d"(Levels.WARN, 24);
406         assert(p.messageCount == 2);
407         log.logF!"Testing without args"(Levels.INFO);
408         assert(p.messageCount == 3);
409         p.reset();
410 
411         log.trace("Test");
412         log.trace(new Exception("Oh no!"));
413         log.traceF!"Test %d"(42);
414         log.traceBuilder().msg("Test").log();
415         assert(p.messages.length == 4);
416         p.reset();
417 
418         log.debug_("Test");
419         log.debug_(new Exception("Oh no!"));
420         log.debugF!"Test %d"(42);
421         log.debugBuilder().msg("Test").log();
422         assert(p.messages.length == 4);
423         p.reset();
424 
425         log.info("Test");
426         log.info(new Exception("Oh no!"));
427         log.infoF!"Test %d"(42);
428         log.infoBuilder().msg("Test").log();
429         assert(p.messages.length == 4);
430         p.reset();
431 
432         log.warn("Test");
433         log.warn(new Exception("Oh no!"));
434         log.warnF!"Test %d"(42);
435         log.warnBuilder().msg("Test").log();
436         assert(p.messages.length == 4);
437         p.reset();
438 
439         log.error("Test");
440         log.error(new Exception("Oh no!"));
441         log.errorF!"Test %d"(42);
442         log.errorBuilder().msg("Test").log();
443         assert(p.messages.length == 4);
444         p.reset();
445     }
446 }
447 
448 /** 
449  * A log message that was created by a logger.
450  */
451 struct LogMessage {
452     /** 
453      * The log level for this message. This is an indication of the severity.
454      */
455     public immutable Level level;
456 
457     /** 
458      * The actual content of the log message.
459      */
460     public immutable string message;
461 
462     /** 
463      * The name of the logger that produced this log message.
464      */
465     public immutable string loggerName;
466 
467     /** 
468      * The time at which this message occurred.
469      */
470     public immutable SysTime timestamp;
471 
472     /** 
473      * The exception (if any) that was thrown when this message was logged.
474      */
475     public immutable Nullable!ExceptionInfo exception;
476 
477     /** 
478      * Additional context about where this log message was generated in the
479      * program's source.
480      */
481     public immutable LogMessageSourceContext sourceContext;
482 
483     /** 
484      * A key-value map of any additional user-defined attributes.
485      */
486     public immutable string[string] attributes;
487 }
488 
489 /** 
490  * Container for information about where a log message was created in a
491  * program's source.
492  */
493 struct LogMessageSourceContext {
494     /** 
495      * The name of the D module that the log message was generated from.
496      */
497     public immutable string moduleName;
498 
499     /** 
500      * The name of the function that the message was generated from.
501      */
502     public immutable string functionName;
503 
504     /** 
505      * The name of the file that the message was generated from.
506      */
507     public immutable string fileName;
508 
509     /** 
510      * The line number in the file where this message was generated.
511      */
512     public immutable size_t lineNumber;
513 }
514 
515 /** 
516  * Container for information about an exception that was logged.
517  */
518 struct ExceptionInfo {
519     /** 
520      * The exception's message.
521      */
522     public immutable string message;
523 
524     /** 
525      * The source file in which the exception occurred.
526      */
527     public immutable string sourceFileName;
528 
529     /** 
530      * The line number at which the exception occurred.
531      */
532     public immutable size_t sourceLineNumber;
533 
534     /** 
535      * The name of the exception class that was thrown.
536      */
537     public immutable string exceptionClassName;
538 
539     /** 
540      * The stack trace for the exception, if it's available.
541      */
542     public immutable Nullable!string stackTrace;
543 
544     /** 
545      * Constructs a nullable ExceptionInfo from an Exception object.
546      * Params:
547      *   e = The exception.
548      * Returns: A nullable ExceptionInfo.
549      */
550     public static Nullable!ExceptionInfo from(Exception e) {
551         if (e is null) return Nullable!ExceptionInfo.init;
552 
553         Nullable!string st;
554         if (e.info !is null) {
555             st = e.info.toString();
556         }
557         return ExceptionInfo(e.msg, e.file, e.line, e.classinfo.name, st).nullable;
558     }
559 
560     unittest {
561         assert(ExceptionInfo.from(null).isNull);
562 
563         try {
564             throw new Exception("Oh no!");
565         } catch (Exception e) {
566             auto info = ExceptionInfo.from(e);
567             assert(!info.isNull);
568             assert(info.get.message == "Oh no!");
569             assert(info.get.exceptionClassName == "object.Exception");
570         }
571     }
572 }
573 
574 /** 
575  * A fluent builder for constructing a log message using method chaining.
576  */
577 struct LogBuilder {
578     private Level level;
579     private string message;
580     private Logger logger;
581     private Exception exception;
582     private string[string] attributes;
583 
584     /** 
585      * Creates a log builder for the given logger.
586      * Params:
587      *   logger = The logger that this builder will add the log message to.
588      * Returns: The log builder.
589      */
590     package static LogBuilder forLogger(Logger logger) {
591         return LogBuilder(logger.level, "", logger);
592     }
593 
594     /** 
595      * Sets the level of the log message.
596      * Params:
597      *   level = The log level.
598      * Returns: A reference to the builder.
599      */
600     public ref LogBuilder lvl(Level level) return {
601         this.level = level;
602         return this;
603     }
604 
605     /** 
606      * Sets the message of the log.
607      * Params:
608      *   message = The message to set.
609      * Returns: A reference to the builder.
610      */
611     public ref LogBuilder msg(string message) return {
612         this.message = message;
613         return this;
614     }
615 
616     /** 
617      * Sets the exception of the log.
618      * Params:
619      *   exception = The exception to set.
620      * Returns: A reference to the builder.
621      */
622     public ref LogBuilder exc(Exception exception) return {
623         this.exception = exception;
624         return this;
625     }
626 
627     /** 
628      * Adds an attribute to the log message.
629      * Params:
630      *   key = The attribute's key.
631      *   value = The value of the attribute.
632      * Returns: A reference to the builder.
633      */
634     public ref LogBuilder attr(string key, string value) return {
635         this.attributes[key] = value;
636         return this;
637     }
638 
639     /** 
640      * Builds the log message and adds it to the logger associated with this
641      * builder, and adds source context information using default function
642      * arguments.
643      * Params:
644      *   moduleName = The name of the module. This will resolve to the current
645      *                module name by default.
646      *   functionName = The name of the function. This will resolve to the
647      *                current function name by default.
648      *   fileName = The name of the source file. This will resolve to the
649      *              current source file by default.
650      *   lineNumber = The line number in the source file. This will resolve to
651      *                the current source file's line number by default.
652      */
653     public void log(
654         string moduleName = __MODULE__,
655         string functionName = __PRETTY_FUNCTION__,
656         string fileName = __FILE__,
657         size_t lineNumber = __LINE__
658     ) {
659         logger.log(LogMessage(
660             this.level,
661             this.message,
662             this.logger.name,
663             Clock.currTime(),
664             ExceptionInfo.from(this.exception),
665             LogMessageSourceContext(
666                 moduleName,
667                 functionName,
668                 fileName,
669                 lineNumber
670             ),
671             cast(immutable(string[string])) attributes.dup
672         ));
673     }
674 }