1 /**
2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 Handles console logging in pretty colors.
7 
8 The module disables colors when stdout and stderr isn't a TTY that support
9 colors. This is to avoid ASCII escape sequences in piped output.
10 
11 The loggers use strings (their name) to form a hierarchy. Such that "a" is the
12 root and "a.b" is a child of "a". This can be used by you to change the log
13 level of a "sub tree" (`SpanMode.depth`).
14 */
15 module colorlog;
16 
17 import logger = std.experimental.logger;
18 import std.array : empty;
19 import std.conv : to;
20 import std.experimental.logger : LogLevel;
21 import std.stdio : writefln, stderr, stdout;
22 
23 public import my.term_color;
24 
25 /// The parent of all loggers.
26 immutable RootLogger = "";
27 
28 /// The verbosity level of the logging to use.
29 enum VerboseMode {
30     /// Info+
31     info,
32     /// Trace+
33     trace,
34     /// Warnings+
35     warning,
36 }
37 
38 LogLevel toLogLevel(VerboseMode mode) @safe pure nothrow @nogc {
39     final switch (mode) {
40     case VerboseMode.info:
41         return logger.LogLevel.info;
42     case VerboseMode.trace:
43         return logger.LogLevel.all;
44     case VerboseMode.warning:
45         return logger.LogLevel.warning;
46     }
47 }
48 
49 /** Configure `std.experimental.logger` `sharedLog` with a colorlog instance
50  * and register it with name "_".
51  */
52 void confLogger(VerboseMode mode) @trusted {
53     logger.globalLogLevel = toLogLevel(mode);
54 
55     static if (__VERSION__ <= 2100) {
56         final switch (mode) {
57         case VerboseMode.info:
58             logger.sharedLog = new SimpleLogger(logger.LogLevel.info);
59             break;
60         case VerboseMode.trace:
61             logger.sharedLog = new DebugLogger(logger.LogLevel.all);
62             break;
63         case VerboseMode.warning:
64             logger.sharedLog = new SimpleLogger(logger.LogLevel.info);
65             break;
66         }
67     } else {
68         final switch (mode) {
69         case VerboseMode.info:
70             logger.sharedLog = cast(shared) new SimpleLogger(logger.LogLevel.info);
71             break;
72         case VerboseMode.trace:
73             logger.sharedLog = cast(shared) new DebugLogger(logger.LogLevel.all);
74             break;
75         case VerboseMode.warning:
76             logger.sharedLog = cast(shared) new SimpleLogger(logger.LogLevel.info);
77             break;
78         }
79     }
80 
81     () @trusted { register(logger.sharedLog, RootLogger); }();
82 }
83 
84 /** Default logger with low clutter in the console which mean it is suitable
85  * for use with user interfacing log messages.
86  *
87  * Automatically colors the log messages depending on their severity.
88  *
89  * As you may note the time is not printed. It isn't particularly useful for a
90  * user of your program as a CLI. A user would just be annoyed. But for e.g. a
91  * system log it may be. In that case, use another logger than this one.
92  */
93 class SimpleLogger : logger.Logger {
94     this(const LogLevel lvl = LogLevel.warning) @safe {
95         super(lvl);
96         initColors;
97     }
98 
99     override void writeLogMsg(ref LogEntry payload) @trusted {
100         auto out_ = stderr;
101         auto use_color = Color.red;
102         auto use_mode = Mode.bold;
103         const use_bg = Background.black;
104 
105         switch (payload.logLevel) {
106         case LogLevel.trace:
107             out_ = stdout;
108             use_color = Color.white;
109             use_mode = Mode.init;
110             break;
111         case LogLevel.info:
112             out_ = stdout;
113             use_color = Color.white;
114             break;
115         default:
116         }
117 
118         out_.writefln("%s: %s", payload.logLevel.to!string.color(use_color)
119                 .bg(use_bg).mode(use_mode), payload.msg);
120     }
121 }
122 
123 /** Logger focused on debugging a program with colorization of the severity.
124  *
125  * The logger print the `file:line` at the end of the message to make it easier
126  * to read a log.  By doing so this logger can be used to print e.g. an
127  * indented AST and still keep the `file:line`.
128  *
129  * Same motivation here as the `SimpleLogger` when it comes to the timestamp.
130  * It isn't particularly useful for what the logger is intended to be used for.
131  */
132 class DebugLogger : logger.Logger {
133     this(const logger.LogLevel lvl = LogLevel.trace) @safe {
134         super(lvl);
135         initColors;
136     }
137 
138     override void writeLogMsg(ref LogEntry payload) @trusted {
139         auto out_ = stderr;
140         auto use_color = Color.red;
141         auto use_mode = Mode.bold;
142         const use_bg = Background.black;
143 
144         switch (payload.logLevel) {
145         case LogLevel.trace:
146             out_ = stdout;
147             use_color = Color.white;
148             use_mode = Mode.init;
149             break;
150         case LogLevel.info:
151             out_ = stdout;
152             use_color = Color.white;
153             break;
154         default:
155         }
156 
157         out_.writefln("%s: %s [%s:%d]", payload.logLevel.to!string.color(use_color)
158                 .bg(use_bg).mode(use_mode), payload.msg, payload.funcName, payload.line);
159     }
160 }
161 
162 /// A string mixin to create a SimpleLogger for the module.
163 string mixinModuleLogger(logger.LogLevel defaultLogLvl = logger.LogLevel.all) @safe pure {
164     return "shared static this() { import std.experimental.logger : LogLevel; make!SimpleLogger(LogLevel."
165         ~ defaultLogLvl.to!string ~ "); }";
166 }
167 
168 /// Register a logger for the module and make it configurable from "outside" via the registry.
169 static if (__VERSION__ <= 2100) {
170     void register(logger.Logger logger, string name = __MODULE__) {
171         synchronized (poolLock) {
172             loggers[name] = cast(shared) logger;
173         }
174     }
175 } else {
176     void register(shared logger.Logger logger, string name = __MODULE__) {
177         synchronized (poolLock) {
178             loggers[name] = logger;
179         }
180     }
181 }
182 
183 /// Create a logger for the module and make it configurable from "outside" via the registry.
184 void make(LoggerT)(const logger.LogLevel lvl = logger.LogLevel.all, string name = __MODULE__) @trusted {
185     static if (__VERSION__ <= 2100) {
186         register(new LoggerT(lvl), name);
187     } else {
188         register(cast(shared) new LoggerT(lvl), name);
189     }
190 }
191 
192 /// Returns: the name of all registered loggers.
193 string[] getRegisteredLoggers() @trusted {
194     import std.array : appender;
195 
196     auto app = appender!(string[])();
197     synchronized (poolLock) {
198         foreach (a; (cast() loggers).byKey)
199             app.put(a);
200     }
201 
202     return app.data;
203 }
204 
205 /// Remove all registered loggers.
206 void clearAllLoggers() @trusted {
207     synchronized (poolLock) {
208         loggers = null;
209     }
210 }
211 
212 enum SpanMode {
213     /// Set only the specified logger.
214     single,
215     /// Set the logger and all its children.
216     depth
217 }
218 
219 /// Set the log level for `name`.
220 void setLogLevel(const string name, const logger.LogLevel lvl, const SpanMode span = SpanMode
221         .single) @trusted {
222     static void setSingle(const string name, const logger.LogLevel lvl) {
223         if (auto v = name in loggers) {
224             auto uv = cast()*v;
225             uv.logLevel = lvl;
226         } else {
227             throw new UnknownLogger("no such logger registered: " ~ name);
228         }
229     }
230 
231     static void depth(string startName, const logger.LogLevel lvl) {
232         import std.algorithm : filter, startsWith;
233 
234         auto loggersU = cast(logger.Logger[string]) loggers;
235         foreach (a; loggersU.byKeyValue.filter!(a => a.key.startsWith(startName))) {
236             a.value.logLevel = lvl;
237         }
238     }
239 
240     synchronized (poolLock) {
241         final switch (span) {
242         case SpanMode.single:
243             setSingle(name, lvl);
244             break;
245         case SpanMode.depth:
246             setSingle(name, lvl);
247             depth(name, lvl);
248             break;
249         }
250     }
251 }
252 
253 /// Set the log level for all loggers in `names`.
254 void setLogLevel(const string[] names, const logger.LogLevel lvl,
255         const SpanMode span = SpanMode.single) @safe {
256     if (names.empty)
257         return;
258 
259     logger.globalLogLevel = lvl;
260     foreach (a; names)
261         setLogLevel(a, lvl, span);
262 }
263 
264 /// Set the log level for all loggers in `names`.
265 void setLogLevel(const NameLevel[] names, const SpanMode span = SpanMode.single) @safe {
266     import std.algorithm : min;
267 
268     if (names.empty)
269         return;
270 
271     auto global = logger.LogLevel.off;
272 
273     foreach (a; names) {
274         setLogLevel(a.name, a.level, span);
275         global = min(a.level, global);
276     }
277 
278     logger.globalLogLevel = global;
279 }
280 
281 /** Log a mesage to the specified logger.
282  *
283  * This only takes the global lock one time and then cache the logger.
284  */
285 logger.Logger log(string name = __MODULE__)() @trusted {
286     static logger.Logger local;
287 
288     if (local !is null)
289         return local;
290 
291     synchronized (poolLock) {
292         if (auto v = name in loggers) {
293             local = cast()*v;
294             return cast()*v;
295         }
296     }
297 
298     throw new UnknownLogger("no such logger registered: " ~ name);
299 }
300 
301 struct NameLevel {
302     string name;
303     LogLevel level;
304 }
305 
306 /// Parse a comma+equal separated string for logger names that can be used with `setLogLevel`.
307 NameLevel[] parseLogNames(string arg, logger.LogLevel defaultLogLvl = logger.LogLevel.all) @safe pure {
308     import std.algorithm : splitter, joiner, map;
309     import std.array : array;
310     import std.string : split;
311 
312     NameLevel[] conv(string s) {
313         try {
314             auto sp = split(s, '=');
315             if (sp.length != 2)
316                 return [NameLevel(s, defaultLogLvl)];
317             return [NameLevel(sp[0], sp[1].to!(logger.LogLevel))];
318         } catch (Exception e) {
319         }
320         return NameLevel[].init;
321     }
322 
323     return arg.splitter(',').map!conv.joiner.array;
324 }
325 
326 /** Always takes the global lock to find the logger.
327  *
328  * This is safe to use whenever because if they logger is replaced it will be
329  * returned.  Normally though this feature isn't needed. Normally all loggers
330  * are registered during module initialization and then they are not changed.
331  */
332 logger.Logger logSlow(string name = __MODULE__)() @trusted {
333     synchronized (poolLock) {
334         if (auto v = name in loggers) {
335             return cast()*v;
336         }
337     }
338 
339     throw new UnknownLogger("no such logger registered: " ~ name);
340 }
341 
342 /// Unknown logger.
343 class UnknownLogger : Exception {
344     this(string msg, string file = __FILE__, int line = __LINE__) @safe pure nothrow {
345         super(msg, file, line);
346     }
347 }
348 
349 @("shall instantiate a logger and register")
350 unittest {
351     scope (exit)
352         clearAllLoggers;
353     make!TestLogger();
354     assert([__MODULE__] == getRegisteredLoggers());
355 }
356 
357 @("shall register a logger and register")
358 unittest {
359     scope (exit)
360         clearAllLoggers;
361     register(new TestLogger);
362     assert([__MODULE__] == getRegisteredLoggers());
363 }
364 
365 @("shall log a message")
366 unittest {
367     scope (exit)
368         clearAllLoggers;
369     make!TestLogger();
370 
371     logSlow.warning("hej");
372 
373     synchronized (poolLock) {
374         assert(!((cast(TestLogger) loggers[__MODULE__]).lastMsg.empty), "no message logged");
375     }
376 }
377 
378 @("shall change the log level")
379 unittest {
380     scope (exit)
381         clearAllLoggers;
382     make!TestLogger(logger.LogLevel.warning);
383     make!TestLogger(logger.LogLevel.warning, __MODULE__ ~ ".b");
384 
385     setLogLevel(__MODULE__, logger.LogLevel.trace);
386 
387     logSlow.trace("hej");
388     logSlow!(__MODULE__ ~ ".b").trace("hej");
389 
390     synchronized (poolLock) {
391         assert(!((cast(TestLogger) loggers[__MODULE__]).lastMsg.empty),
392                 "message found when it shouldn't");
393         assert(((cast(TestLogger) loggers[__MODULE__ ~ ".b"]).lastMsg.empty),
394                 "message found when it shouldn't");
395     }
396 }
397 
398 @("shall change the log level from parent and up")
399 unittest {
400     scope (exit)
401         clearAllLoggers;
402     make!TestLogger(logger.LogLevel.warning);
403     make!TestLogger(logger.LogLevel.warning, __MODULE__ ~ ".b");
404 
405     setLogLevel(__MODULE__, logger.LogLevel.trace, SpanMode.depth);
406 
407     logSlow.trace("hej");
408     logSlow!(__MODULE__ ~ ".b").trace("hej");
409 
410     synchronized (poolLock) {
411         assert(!((cast(TestLogger) loggers[__MODULE__]).lastMsg.empty),
412                 "message found when it shouldn't");
413         assert(!((cast(TestLogger) loggers[__MODULE__ ~ ".b"]).lastMsg.empty),
414                 "message found when it shouldn't");
415     }
416 }
417 
418 @("shall parse a comma separate list")
419 unittest {
420     assert(parseLogNames("hej=trace,foo") == [
421             NameLevel("hej", logger.LogLevel.trace),
422             NameLevel("foo", logger.LogLevel.all)
423             ]);
424 }
425 
426 private:
427 
428 import core.sync.mutex : Mutex;
429 
430 // The width of the prefix.
431 immutable _prefixWidth = 8;
432 
433 // Mutex for the logger pool.
434 shared Mutex poolLock;
435 shared logger.Logger[string] loggers;
436 
437 shared static this() {
438     poolLock = cast(shared) new Mutex();
439 }
440 
441 class TestLogger : logger.Logger {
442     this(const logger.LogLevel lvl = LogLevel.trace) @safe {
443         super(lvl);
444     }
445 
446     string lastMsg;
447 
448     override void writeLogMsg(ref LogEntry payload) @trusted {
449         import std.format : format;
450 
451         lastMsg = format!"%s: %s [%s:%d]"(payload.logLevel, payload.msg,
452                 payload.funcName, payload.line);
453     }
454 }