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