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 }