1 /**
2     Central logging facility for dentist.
3 
4     Copyright: © 2018 Arne Ludwig <arne.ludwig@posteo.de>
5     License: Subject to the terms of the MIT license, as written in the
6              included LICENSE file.
7     Authors: Arne Ludwig <arne.ludwig@posteo.de>
8 */
9 module dentist.util.log;
10 
11 import std.array;
12 import std.datetime;
13 import std.format;
14 import std.process;
15 import std.range;
16 import std.stdio;
17 import core.thread;
18 
19 private
20 {
21     LogLevel minLevel = LogLevel.info;
22 }
23 
24 /// Sets the minimum log level to be printed.
25 void setLogLevel(LogLevel level) nothrow
26 {
27     minLevel = level;
28 }
29 
30 LogLevel getLogLevel()
31 {
32     return minLevel;
33 }
34 
35 bool shouldLog(LogLevel level)
36 {
37     return level >= minLevel;
38 }
39 
40 /**
41     Logs a message in JSON format.
42     Params:
43         args = pairs of `name` (`string`) and `value`
44 */
45 void logJsonDebug(T...)(lazy T args) nothrow
46 {
47     logJson(LogLevel.debug_, args);
48 }
49 /// ditto
50 void logJsonDiagnostic(T...)(lazy T args) nothrow
51 {
52     logJson(LogLevel.diagnostic, args);
53 }
54 /// ditto
55 void logJsonInfo(T...)(lazy T args) nothrow
56 {
57     logJson(LogLevel.info, args);
58 }
59 /// ditto
60 void logJsonWarn(T...)(lazy T args) nothrow
61 {
62     logJson(LogLevel.warn, args);
63 }
64 /// ditto
65 void logJsonError(T...)(lazy T args) nothrow
66 {
67     logJson(LogLevel.error, args);
68 }
69 
70 /// ditto
71 void logJson(T...)(LogLevel level, lazy T args) nothrow
72 {
73     import dentist.util.range : Chunks, chunks;
74     import std.datetime.systime : Clock;
75     import std.traits : isSomeString;
76     import vibe.data.json : Json;
77 
78     enum threadKey = "thread";
79     enum timestampKey = "timestamp";
80 
81     if (level < minLevel)
82         return;
83 
84     try
85     {
86         Json json = Json.emptyObject;
87 
88         static foreach (KeyValuePair; Chunks!(2, T))
89         {
90             static assert(isSomeString!(KeyValuePair.chunks[0]), "missing name");
91         }
92 
93         json[timestampKey] = Clock.currStdTime;
94         json[threadKey] = thisThreadID;
95 
96         foreach (keyValuePair; args.chunks!2)
97         {
98             json[keyValuePair[0]] = keyValuePair[1];
99         }
100 
101         return log(level, json.to!string);
102     }
103     catch (Exception e)
104     {
105         // this is bad but what can we do..
106         debug assert(false, e.msg);
107     }
108 }
109 
110 ///
111 unittest
112 {
113     import std.regex : ctRegex, matchFirst;
114     import std.stdio : File, stderr;
115 
116     auto origStderr = stderr;
117     stderr = File.tmpfile();
118     scope (exit)
119     {
120         stderr.close();
121         stderr = origStderr;
122     }
123 
124     logJsonError("error", "mysterious observation", "secret", 42);
125 
126     stderr.rewind();
127     auto expected = ctRegex!(
128             `\{"secret":42,"error":"mysterious observation","thread":[0-9]+,"timestamp":[0-9]+\}` ~ '\n');
129     auto observed = stderr.readln;
130 
131     assert(matchFirst(observed, expected), "got unexpected output `" ~ observed ~ "`");
132 }
133 
134 /**
135     Logs a message.
136     Params:
137         level = The log level for the logged message
138         fmt = See http://dlang.org/phobos/std_format.html#format-string
139 */
140 void logDebug(T...)(string fmt, lazy T args) nothrow
141 {
142     log(LogLevel.debug_, fmt, args);
143 }
144 /// ditto
145 void logDiagnostic(T...)(string fmt, lazy T args) nothrow
146 {
147     log(LogLevel.diagnostic, fmt, args);
148 }
149 /// ditto
150 void logInfo(T...)(string fmt, lazy T args) nothrow
151 {
152     log(LogLevel.info, fmt, args);
153 }
154 /// ditto
155 void logWarn(T...)(string fmt, lazy T args) nothrow
156 {
157     log(LogLevel.warn, fmt, args);
158 }
159 /// ditto
160 void logError(T...)(string fmt, lazy T args) nothrow
161 {
162     log(LogLevel.error, fmt, args);
163 }
164 
165 /// ditto
166 void log(T...)(LogLevel level, string fmt, lazy T args) nothrow
167 {
168     if (level < minLevel)
169         return;
170     string pref;
171     final switch (level)
172     {
173     case LogLevel.debug_:
174         pref = "TRACE";
175         break;
176     case LogLevel.diagnostic:
177         pref = "DEBUG";
178         break;
179     case LogLevel.info:
180         pref = "INFO";
181         break;
182     case LogLevel.warn:
183         pref = "WARN";
184         break;
185     case LogLevel.error:
186         pref = "ERROR";
187         break;
188     case LogLevel.fatal:
189         pref = "FATAL";
190         break;
191     case LogLevel.none:
192         assert(false);
193     }
194     auto threadid = () @trusted{ return cast(ulong) cast(void*) Thread.getThis(); }();
195     threadid ^= threadid >> 32;
196 
197     try
198     {
199         auto txt = appender!string();
200         txt.reserve(256);
201         static if (args.length > 0)
202         {
203             formattedWrite(txt, fmt, args);
204         }
205         else
206         {
207             txt ~= fmt;
208         }
209 
210         if (level >= minLevel)
211         {
212             File output = stderr;
213 
214             synchronized if (output.isOpen)
215             {
216                 output.writeln(txt.data);
217                 output.flush();
218             }
219         }
220     }
221     catch (Exception e)
222     {
223         // this is bad but what can we do..
224         debug assert(false, e.msg);
225     }
226 }
227 
228 /// Specifies the log level for a particular log message.
229 enum LogLevel
230 {
231     debug_,
232     diagnostic,
233     info,
234     warn,
235     error,
236     fatal,
237     none
238 }
239 
240 string traceExecution(LogLevel logLevel = LogLevel.diagnostic)()
241 {
242     import std.conv : to;
243     import std..string : replace;
244     import std.traits : moduleName;
245 
246     return q"{
247         import std.datetime.stopwatch : $prefix__StopWatch = StopWatch;
248 
249         $prefix__StopWatch $prefix__enter() {
250             import $thisModule : LogLevel;
251             import std.traits : fullyQualifiedName;
252             import std.typecons : Yes;
253 
254             logJson(
255                 $logLevel,
256                 `state`, `enter`,
257                 `function`, $function,
258             );
259 
260             return $prefix__StopWatch(Yes.autoStart);
261         }
262 
263         void $prefix__exit($prefix__StopWatch timer) {
264             import $thisModule : LogLevel;
265             import std.traits : fullyQualifiedName;
266 
267             timer.stop();
268             logJson(
269                 $logLevel,
270                 `state`, `exit`,
271                 `function`, $function,
272                 `timeElapsed`, timer.peek().total!`hnsecs`,
273             );
274         }
275 
276         auto $prefix__timer = $prefix__enter();
277 
278         scope (exit)
279             $prefix__exit($prefix__timer);
280     }"
281         .replace("$thisModule", moduleName!LogLevel)
282         .replace("$function", "fullyQualifiedName!(__traits(parent, $prefix__enter))")
283         .replace("$logLevel", "LogLevel." ~ logLevel.to!string)
284         .replace("$prefix", `__traceExecution`);
285 }
286 
287 unittest
288 {
289     import std.regex : ctRegex, matchFirst;
290     import std.stdio : File, stderr;
291 
292     auto origStderr = stderr;
293     stderr = File.tmpfile();
294     scope (exit)
295     {
296         stderr.close();
297         stderr = origStderr;
298     }
299 
300     void doSomething()
301     {
302         mixin(traceExecution!(LogLevel.error));
303 
304         import core.thread : Thread;
305         import core.time : dur;
306 
307         Thread.getThis.sleep(dur!"hnsecs"(50));
308     }
309 
310     doSomething();
311     stderr.rewind();
312 
313     auto expected1 = ctRegex!
314             `\{"state":"enter","function":"dentist\.util\.log\.__unittest_L[0-9]+_C[0-9]+\.doSomething","thread":[0-9]+,"timestamp":[0-9]+\}`;
315     auto expected2 = ctRegex!
316             `\{"state":"exit","timeElapsed":[0-9]{2,},"function":"dentist\.util\.log\.__unittest_L[0-9]+_C[0-9]+\.doSomething","thread":[0-9]+,"timestamp":[0-9]+\}`;
317     auto observed1 = stderr.readln;
318     auto observed2 = stderr.readln;
319 
320     assert(matchFirst(observed1, expected1), "got unexpected output `" ~ observed1 ~ "`");
321     assert(matchFirst(observed2, expected2), "got unexpected output `" ~ observed2 ~ "`");
322 }