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 }