1 /++
2     Contains the custom [KamelosoLogger] class, used to print timestamped and
3     (optionally) coloured logging messages.
4 
5     Example:
6     ---
7     auto logger = new KamelosoLogger(
8         No.monochrome,
9         No.brigtTerminal,
10         No.headless,
11         No.flush);
12 
13     logger.log("This is LogLevel.all");
14     logger.info("LogLevel.info");
15     logger.warn(".warn");
16     logger.error(".error");
17     logger.trace(".trace");
18     //logger.fatal("This will crash the program.");
19     ---
20 
21     See_Also:
22         [kameloso.terminal.colours]
23 
24     Copyright: [JR](https://github.com/zorael)
25     License: [Boost Software License 1.0](https://www.boost.org/users/license.html)
26 
27     Authors:
28         [JR](https://github.com/zorael)
29  +/
30 module kameloso.logger;
31 
32 private:
33 
34 import std.typecons : Flag, No, Yes;
35 
36 public:
37 
38 
39 // LogLevel
40 /++
41     Logging levels; copied straight from [std.logger], to save us an import.
42 
43     There are eight usable logging level. These level are $(I all), $(I trace),
44     $(I info), $(I warning), $(I error), $(I critical), $(I fatal), and $(I off).
45     If a log function with `LogLevel.fatal` is called the shutdown handler of
46     that logger is called.
47  +/
48 enum LogLevel : ubyte
49 {
50     /++
51         Lowest possible assignable `LogLevel`.
52      +/
53     all = 1,
54 
55     /++
56         `LogLevel` for tracing the execution of the program.
57      +/
58     trace = 32,
59 
60     /++
61         This level is used to display information about the program.
62      +/
63     info = 64,
64 
65     /++
66         warnings about the program should be displayed with this level.
67      +/
68     warning = 96,
69 
70     /++
71         Information about errors should be logged with this level.
72      +/
73     error = 128,
74 
75     /++
76         Messages that inform about critical errors should be logged with this level.
77      +/
78     critical = 160,
79 
80     /++
81         Log messages that describe fatal errors should use this level.
82      +/
83     fatal = 192,
84 
85     /++
86         Highest possible `LogLevel`.
87      +/
88     off = ubyte.max
89 }
90 
91 
92 // KamelosoLogger
93 /++
94     Logger class, used to print timestamped and coloured logging messages.
95 
96     It is thread-local so instantiate more if you're threading.
97  +/
98 final class KamelosoLogger
99 {
100 private:
101     import kameloso.pods : CoreSettings;
102     import kameloso.terminal.colours.tags : expandTags;
103     import lu.conv : Enum;
104     import std.array : Appender;
105     import std.format : format;
106     import std.traits : EnumMembers;
107 
108     version(Colours)
109     {
110         import kameloso.constants : DefaultColours;
111         import kameloso.terminal.colours.defs : TerminalForeground, TerminalReset;
112         import kameloso.terminal.colours : applyANSI, asANSI;
113 
114         /// Convenience alias.
115         alias logcoloursBright = DefaultColours.logcoloursBright;
116 
117         /// Ditto
118         alias logcoloursDark = DefaultColours.logcoloursDark;
119     }
120 
121     /// Buffer to compose a line in before printing it to screen in one go.
122     Appender!(char[]) linebuffer;
123 
124     /// Sub-buffer to compose the message in.
125     Appender!(char[]) messagebuffer;
126 
127     /// The initial size to allocate for buffers. It will grow if needed.
128     enum bufferInitialSize = 4096;
129 
130     bool monochrome;  /// Whether to use colours or not in logger output.
131     bool brightTerminal;  /// Whether or not to use colours for a bright background.
132     bool headless;  /// Whether or not to disable all terminal output.
133     bool flush;  /// Whether or not to flush standard out after writing to it.
134 
135 public:
136     /++
137         Creates a new [KamelosoLogger] with the passed settings.
138 
139         Params:
140             monochrome = Whether or not to print colours.
141             brightTerminal = Bright terminal setting.
142             headless = Headless setting.
143             flush = Flush setting.
144      +/
145     this(
146         const Flag!"monochrome" monochrome,
147         const Flag!"brightTerminal" brightTerminal,
148         const Flag!"headless" headless,
149         const Flag!"flush" flush) pure nothrow @safe
150     {
151         linebuffer.reserve(bufferInitialSize);
152         messagebuffer.reserve(bufferInitialSize);
153         this.monochrome = monochrome;
154         this.brightTerminal = brightTerminal;
155         this.headless = headless;
156         this.flush = flush;
157     }
158 
159     /++
160         Creates a new [KamelosoLogger] with settings divined from the passed
161         [kameloso.pods.CoreSettings|CoreSettings] struct.
162 
163         Params:
164             settings = [kameloso.pods.CoreSettings|CoreSettings] whose
165                 values to inherit.
166      +/
167     this(const CoreSettings settings) pure nothrow @safe
168     {
169         linebuffer.reserve(bufferInitialSize);
170         messagebuffer.reserve(bufferInitialSize);
171         this.monochrome = settings.monochrome;
172         this.brightTerminal = settings.brightTerminal;
173         this.headless = settings.headless;
174         this.flush = settings.flush;
175     }
176 
177     version(Colours)
178     {
179         // tint
180         /++
181             Returns the corresponding
182             [kameloso.terminal.colours.defs.TerminalForeground|TerminalForeground] for the [LogLevel],
183             taking into account whether the terminal is said to be bright or not.
184 
185             This is merely a convenient wrapping for [logcoloursBright] and
186             [logcoloursDark].
187 
188             Example:
189             ---
190             TerminalForeground errtint = KamelosoLogger.tint(LogLevel.error, No.brightTerminal);
191             immutable errtintString = errtint.asANSI;
192             ---
193 
194             Params:
195                 level = The [LogLevel] of the colour we want to scry.
196                 bright = Whether the colour should be for a bright terminal
197                     background or a dark one.
198 
199             Returns:
200                 A [kameloso.terminal.colours.defs.TerminalForeground|TerminalForeground] of
201                 the right colour. Use with [kameloso.terminal.colours.asANSI|asANSI]
202                 to get a string.
203          +/
204         static uint tint(const LogLevel level, const Flag!"brightTerminal" bright) pure nothrow @nogc @safe
205         {
206             return bright ? logcoloursBright[level] : logcoloursDark[level];
207         }
208 
209         ///
210         unittest
211         {
212             import std.range : only;
213 
214             auto range = only(
215                 LogLevel.all,
216                 LogLevel.info,
217                 LogLevel.warning,
218                 LogLevel.fatal);
219 
220             foreach (immutable logLevel; range)
221             {
222                 import std.format : format;
223 
224                 immutable tintBright = tint(logLevel, Yes.brightTerminal);
225                 immutable tintBrightTable = logcoloursBright[logLevel];
226                 assert((tintBright == tintBrightTable), "%s != %s"
227                     .format(tintBright, tintBrightTable));
228 
229                 immutable tintDark = tint(logLevel, No.brightTerminal);
230                 immutable tintDarkTable = logcoloursDark[logLevel];
231                 assert((tintDark == tintDarkTable), "%s != %s"
232                     .format(tintDark, tintDarkTable));
233             }
234         }
235 
236 
237         // tintImpl
238         /++
239             Template for returning tints based on the settings of the `this`
240             [KamelosoLogger].
241 
242             This saves us having to pass the brightness setting, and allows for
243             making easy aliases for the log level.
244 
245             Params:
246                 level = Compile-time [LogLevel].
247 
248             Returns:
249                 A tint string.
250          +/
251         private auto tintImpl(LogLevel level)() const @property pure nothrow @nogc @safe
252         {
253             if (headless || monochrome)
254             {
255                 return string.init;
256             }
257             else if (brightTerminal)
258             {
259                 enum ctTintBright = tint(level, Yes.brightTerminal).asANSI.idup;
260                 return ctTintBright;
261             }
262             else
263             {
264                 enum ctTintDark = tint(level, No.brightTerminal).asANSI.idup;
265                 return ctTintDark;
266             }
267         }
268 
269 
270         /+
271             Generate *tint functions for each [LogLevel].
272          +/
273         static foreach (const lv; EnumMembers!LogLevel)
274         {
275             mixin(
276 "auto " ~ Enum!LogLevel.toString(lv) ~ "tint() const @property pure nothrow @nogc @safe
277 {
278     return tintImpl!(LogLevel." ~ Enum!LogLevel.toString(lv) ~ ");
279 }");
280         }
281 
282         /++
283             Synonymous alias to `alltint`, as a workaround for [LogLevel.all]
284             not being named `LogLevel.log`.
285          +/
286         alias logtint = alltint;
287     }
288     else
289     {
290         // offtint
291         /++
292             Dummy function returning an empty string, since there can be no tints
293             on non-version `Colours` builds.
294 
295             Returns:
296                 An empty string.
297          +/
298         public static auto offtint() pure nothrow @nogc @safe
299         {
300             return string.init;
301         }
302 
303         /+
304             Generate dummy *tint functions for each [LogLevel] by aliasing them
305             to [offtint].
306          +/
307         static foreach (const lv; EnumMembers!LogLevel)
308         {
309             static if (lv != LogLevel.off)
310             {
311                 mixin("alias " ~ Enum!LogLevel.toString(lv) ~ "tint = offtint;");
312             }
313         }
314 
315         /++
316             Synonymous alias to `alltint`, as a workaround for [LogLevel.all]
317             not being named `LogLevel.log`.
318          +/
319         alias logtint = alltint;
320     }
321 
322 
323     /++
324         Outputs the header of a logger message.
325 
326         Params:
327             logLevel = The [LogLevel] to treat this message as being of.
328      +/
329     private void beginLogMsg(const LogLevel logLevel) @safe
330     {
331         import std.datetime : DateTime;
332         import std.datetime.systime : Clock;
333 
334         version(Colours)
335         {
336             if (!monochrome)
337             {
338                 alias Timestamp = DefaultColours.TimestampColour;
339                 //linebuffer.applyANSI(brightTerminal ? Timestamp.bright : Timestamp.dark);
340                 linebuffer.applyANSI(TerminalReset.all);
341             }
342         }
343 
344         linebuffer.put('[');
345         (cast(DateTime)Clock.currTime).timeOfDay.toString(linebuffer);
346         linebuffer.put("] ");
347 
348         version(Colours)
349         {
350             if (!monochrome)
351             {
352                 linebuffer.applyANSI(brightTerminal ?
353                     logcoloursBright[logLevel] :
354                     logcoloursDark[logLevel]);
355             }
356         }
357     }
358 
359 
360     /++
361         Outputs the tail of a logger message.
362      +/
363     private void finishLogMsg() @trusted  // writeln trusts stdout.flush, so we should be able to too
364     {
365         import kameloso.terminal.colours : applyANSI;
366         import std.stdio : stdout, writeln;
367 
368         version(Colours)
369         {
370             if (!monochrome)
371             {
372                 // Reset.blink in case a fatal message was thrown
373                 linebuffer.applyANSI(TerminalReset.all);
374             }
375         }
376 
377         writeln(linebuffer.data);
378         if (flush) stdout.flush();
379     }
380 
381 
382     // printImpl
383     /++
384         Prints a timestamped log message to screen. Implementation function.
385 
386         Prints the arguments as they are if possible (if they are some variant of
387         `char` or `char[]`), and otherwise tries to coerce them by using
388         [std.conv.to].
389 
390         Params:
391             logLevel = The [LogLevel] to treat this message as being of.
392             args = Variadic arguments to compose the output message with.
393      +/
394     private void printImpl(Args...)(const LogLevel logLevel, auto ref Args args)
395     {
396         import lu.traits : UnqualArray;
397         import std.traits : isAggregateType;
398 
399         scope(exit)
400         {
401             linebuffer.clear();
402             messagebuffer.clear();
403         }
404 
405         beginLogMsg(logLevel);
406 
407         foreach (ref arg; args)
408         {
409             alias T = typeof(arg);
410 
411             static if (is(UnqualArray!T : char[]) || is(T : char))
412             {
413                 messagebuffer.put(arg);
414             }
415             else static if (is(T == enum))
416             {
417                 import lu.conv : Enum;
418                 messagebuffer.put(Enum!T.toString(arg));
419             }
420             else static if (isAggregateType!T && is(typeof(T.toString)))
421             {
422                 import std.traits : isSomeFunction;
423 
424                 static if (isSomeFunction!(T.toString) || __traits(isTemplate, T.toString))
425                 {
426                     static if (__traits(compiles, arg.toString(messagebuffer)))
427                     {
428                         // Output range sink overload (accepts an Appender)
429                         arg.toString(messagebuffer);
430                     }
431                     else static if (__traits(compiles,
432                         arg.toString((const(char)[] text) => messagebuffer.put(text))))
433                     {
434                         // Output delegate sink overload
435                         arg.toString((const(char)[] text) => messagebuffer.put(text));
436                     }
437                     else static if (__traits(compiles, messagebuffer.put(arg.toString)))
438                     {
439                         // Plain string-returning function or template
440                         messagebuffer.put(arg.toString);
441                     }
442                     else
443                     {
444                         import std.conv : to;
445                         // std.conv.to fallback
446                         messagebuffer.put(arg.to!string);
447                     }
448                 }
449                 else static if (is(typeof(T.toString)) &&
450                     is(UnqualArray!(typeof(T.toString)) : char[]))
451                 {
452                     // toString string/char[] literal
453                     messagebuffer.put(arg.toString);
454                 }
455                 else
456                 {
457                     import std.conv : to;
458                     // std.conv.to fallback
459                     messagebuffer.put(arg.to!string);
460                 }
461             }
462             else
463             {
464                 import std.conv : to;
465                 // std.conv.to fallback
466                 messagebuffer.put(arg.to!string);
467             }
468         }
469 
470         linebuffer.put(messagebuffer.data.expandTags(logLevel, cast(Flag!"strip")monochrome));
471         finishLogMsg();
472     }
473 
474 
475     // printfImpl
476     /++
477         Prints a timestamped log message to screen as per the passed runtime pattern,
478         in `printf` style. Implementation function.
479 
480         Uses [std.format.formattedWrite|formattedWrite] to coerce the passed
481         arguments as the format pattern dictates.
482 
483         Params:
484             logLevel = The [LogLevel] to treat this message as being of.
485             pattern = Runtime pattern to format the output with.
486             args = Variadic arguments to compose the output message with.
487      +/
488     private void printfImpl(Args...)
489         (const LogLevel logLevel,
490         const string pattern,
491         auto ref Args args)
492     {
493         import std.format : formattedWrite;
494 
495         scope(exit)
496         {
497             linebuffer.clear();
498             messagebuffer.clear();
499         }
500 
501         beginLogMsg(logLevel);
502         messagebuffer.formattedWrite(pattern, args);
503         linebuffer.put(messagebuffer.data.expandTags(logLevel, cast(Flag!"strip")monochrome));
504         finishLogMsg();
505     }
506 
507 
508     // printfImpl
509     /++
510         Prints a timestamped log message to screen as per the passed compile-time
511         pattern, in `printf` style. Implementation function.
512 
513         Uses [std.format.formattedWrite|formattedWrite] to coerce the passed
514         arguments as the format pattern dictates.
515 
516         Params:
517             pattern = Compile-time pattern to validate the arguments and format
518                 the output with.
519             logLevel = The [LogLevel] to treat this message as being of.
520             args = Variadic arguments to compose the output message with.
521      +/
522     private void printfImpl(string pattern, Args...)(const LogLevel logLevel, auto ref Args args)
523     {
524         import std.format : formattedWrite;
525 
526         scope(exit)
527         {
528             linebuffer.clear();
529             messagebuffer.clear();
530         }
531 
532         beginLogMsg(logLevel);
533         messagebuffer.formattedWrite!pattern(args);
534         linebuffer.put(messagebuffer.data.expandTags(logLevel, cast(Flag!"strip")monochrome));
535         finishLogMsg();
536     }
537 
538 
539     /// Mixin to error out on `fatal` calls.
540     private enum fatalErrorMixin =
541 `throw new Error("A fatal error message was logged");`;
542 
543     /+
544         Generate `trace`, `tracef`, `log`, `logf` and similar Logger-esque functions.
545 
546         Mixes in [fatalExitMixin] on `fatal` to have it exit the program on those.
547      +/
548     static foreach (const lv; EnumMembers!LogLevel)
549     {
550         mixin(
551 "void " ~ Enum!LogLevel.toString(lv) ~ "(Args...)(auto ref Args args)
552 {
553     if (!headless) printImpl(LogLevel." ~ Enum!LogLevel.toString(lv) ~ ", args);
554     " ~ ((lv == LogLevel.fatal) ? fatalErrorMixin : string.init) ~ "
555 }
556 
557 void " ~ Enum!LogLevel.toString(lv) ~ "f(Args...)(const string pattern, auto ref Args args)
558 {
559     if (!headless) printfImpl(LogLevel." ~ Enum!LogLevel.toString(lv) ~ ", pattern, args);
560     " ~ ((lv == LogLevel.fatal) ? fatalErrorMixin : string.init) ~ "
561 }
562 
563 void " ~ Enum!LogLevel.toString(lv) ~ "f(string pattern, Args...)(auto ref Args args)
564 {
565     if (!headless) printfImpl!pattern(LogLevel." ~ Enum!LogLevel.toString(lv) ~ ", args);
566     " ~ ((lv == LogLevel.fatal) ? fatalErrorMixin : string.init) ~ "
567 }");
568     }
569 
570     /++
571         Synonymous alias to [KamelosoLogger.all], as a workaround for
572         [LogLevel.all] not being named `LogLevel.log`.
573      +/
574     alias log = all;
575 
576     /++
577         Synonymous alias to [KamelosoLogger.allf], as a workaround for
578         [LogLevel.all] not being named `LogLevel.log`.
579      +/
580     alias logf = allf;
581 }
582 
583 ///
584 unittest
585 {
586     struct S1
587     {
588         void toString(Sink)(auto ref Sink sink) const
589         {
590             sink.put("sink toString");
591         }
592     }
593 
594     struct S2
595     {
596         void toString(scope void delegate(const(char)[]) dg) const
597         {
598             dg("delegate toString");
599         }
600 
601         @disable this(this);
602     }
603 
604     struct S3
605     {
606         string s = "no toString";
607     }
608 
609     struct S4
610     {
611         string toString = "toString literal";
612     }
613 
614     struct S5
615     {
616         string toString()() const
617         {
618             return "template toString";
619         }
620     }
621 
622     class C
623     {
624         override string toString() const
625         {
626             return "plain toString";
627         }
628     }
629 
630     auto log_ = new KamelosoLogger(Yes.monochrome, No.brightTerminal, No.headless, Yes.flush);
631 
632     log_.logf!"log: %s"("log");
633     log_.infof!"log: %s"("info");
634     log_.warningf!"log: %s"("warning");
635     log_.errorf!"log: %s"("error");
636     log_.criticalf!"log: %s"("critical");
637     // log_.fatalf!"log: %s"("FATAL");
638     log_.tracef!"log: %s"("trace");
639     log_.offf!"log: %s"("off");
640 
641     version(Colours)
642     {
643         log_ = new KamelosoLogger(No.monochrome, Yes.brightTerminal, No.headless, Yes.flush);
644 
645         log_.log("log: log");
646         log_.info("log: info");
647         log_.warning("log: warning");
648         log_.error("log: error");
649         log_.critical("log: critical");
650         // log_.fatal("log: FATAL");
651         log_.trace("log: trace");
652         log_.off("log: off");
653 
654         log_ = new KamelosoLogger(No.monochrome, No.brightTerminal, No.headless, Yes.flush);
655 
656         log_.log("log: log");
657         log_.info("log: info");
658         log_.warning("log: warning");
659         log_.error("log: error");
660         // log_.fatal("log: FATAL");
661         log_.trace("log: trace");
662         log_.off("log: off");
663     }
664 
665     log_.log("log <i>info</> log <w>warning</> log <e>error</> log <t>trace</> log <o>off</> log");
666 
667     S1 s1;
668     S2 s2;
669     S3 s3;
670     S4 s4;
671     S5 s5;
672     C c = new C;
673 
674     log_.trace();
675 
676     log_.log(s1);
677     log_.info(s2);
678     log_.warning(s3);
679     log_.critical(s4);
680     log_.error(s5);
681     log_.trace(c);
682 
683     log_.headless = true;
684     log_.error("THIS SHOULD NEVER BE SEEN");
685 }