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 }