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 core.thread;
12 import dalicious.traits;
13 import std.algorithm;
14 import std.array;
15 import std.datetime.stopwatch;
16 import std.format;
17 import std.process;
18 import std.range;
19 import std.stdio;
20 import std.traits;
21 import std.typecons;
22 
23 private
24 {
25     __gshared LogLevel minLevel = LogLevel.info;
26     __gshared File logFile;
27 }
28 
29 
30 static this()
31 {
32     synchronized
33         if (!logFile.isOpen)
34             setLogFile(stderr);
35 }
36 
37 
38 /// Sets the log file.
39 void setLogFile(File logFile)
40 {
41     synchronized
42         .logFile = logFile;
43 }
44 
45 
46 /// Get the log file.
47 File getLogFile() nothrow
48 {
49     return logFile;
50 }
51 
52 
53 /// Sets the minimum log level to be printed.
54 void setLogLevel(LogLevel level) nothrow
55 {
56     synchronized
57         minLevel = level;
58 }
59 
60 LogLevel getLogLevel() nothrow @nogc
61 {
62     return minLevel;
63 }
64 
65 bool shouldLog(LogLevel level) nothrow @nogc
66 {
67     return level >= minLevel;
68 }
69 
70 /**
71     Logs a message in JSON format. If the `name` part is `null` the field
72     will not be included into the Json.
73 
74     Params:
75         args = pairs of `name` (`string`) and `value`
76 */
77 void logJsonDebug(T...)(lazy T args) nothrow
78 {
79     logJson(LogLevel.debug_, args);
80 }
81 /// ditto
82 void logJsonDiagnostic(T...)(lazy T args) nothrow
83 {
84     logJson(LogLevel.diagnostic, args);
85 }
86 /// ditto
87 void logJsonInfo(T...)(lazy T args) nothrow
88 {
89     logJson(LogLevel.info, args);
90 }
91 /// ditto
92 void logJsonWarn(T...)(lazy T args) nothrow
93 {
94     logJson(LogLevel.warn, args);
95 }
96 /// ditto
97 void logJsonError(T...)(lazy T args) nothrow
98 {
99     logJson(LogLevel.error, args);
100 }
101 
102 /// ditto
103 void logJson(T...)(LogLevel level, lazy T args) nothrow
104 {
105     import dalicious.range : Chunks, chunks;
106     import std.datetime.systime : Clock;
107     import std.traits : isSomeString;
108     import vibe.data.json : Json, serializeToJson;
109 
110     enum threadKey = "thread";
111     enum timestampKey = "timestamp";
112 
113     if (level < minLevel)
114         return;
115 
116     try
117     {
118         Json json = Json.emptyObject;
119 
120         static foreach (KeyValuePair; Chunks!(2, T))
121         {
122             static assert(isSomeString!(KeyValuePair.chunks[0]), "missing name");
123         }
124 
125         json[timestampKey] = Clock.currStdTime;
126         json[threadKey] = thisThreadID;
127 
128         foreach (keyValuePair; args.chunks!2)
129             if (keyValuePair[0] !is null)
130                 json[keyValuePair[0]] = serializeToJson(keyValuePair[1]);
131 
132         return log(level, json.to!string);
133     }
134     catch (Exception e)
135     {
136         // this is bad but what can we do..
137         debug assert(false, e.msg);
138     }
139 }
140 
141 ///
142 unittest
143 {
144     import std.stdio : File, stderr;
145     import vibe.data.json : Json, parseJsonString;
146 
147     auto logFile = File.tmpfile();
148     setLogFile(logFile);
149     scope (exit)
150         setLogFile(stderr);
151 
152     logJsonError("error", "mysterious observation", "secret", 42);
153 
154     logFile.rewind();
155     auto observed = parseJsonString(logFile.readln);
156 
157     assert(observed["thread"].type == Json.Type.int_);
158     assert(observed["timestamp"].type == Json.Type.int_);
159     assert(observed["error"] == "mysterious observation");
160     assert(observed["secret"] == 42);
161 }
162 
163 /**
164     Logs a message.
165     Params:
166         level = The log level for the logged message
167         fmt = See http://dlang.org/phobos/std_format.html#format-string
168 */
169 void logDebug(T...)(string fmt, lazy T args) nothrow
170 {
171     log(LogLevel.debug_, fmt, args);
172 }
173 /// ditto
174 void logDiagnostic(T...)(string fmt, lazy T args) nothrow
175 {
176     log(LogLevel.diagnostic, fmt, args);
177 }
178 /// ditto
179 void logInfo(T...)(string fmt, lazy T args) nothrow
180 {
181     log(LogLevel.info, fmt, args);
182 }
183 /// ditto
184 void logWarn(T...)(string fmt, lazy T args) nothrow
185 {
186     log(LogLevel.warn, fmt, args);
187 }
188 /// ditto
189 void logError(T...)(string fmt, lazy T args) nothrow
190 {
191     log(LogLevel.error, fmt, args);
192 }
193 
194 /// ditto
195 void log(T...)(LogLevel level, string fmt, lazy T args) nothrow
196 {
197     if (level < minLevel)
198         return;
199 
200     try
201     {
202         auto txt = appender!string();
203         txt.reserve(256);
204         static if (args.length > 0)
205         {
206             formattedWrite(txt, fmt, args);
207         }
208         else
209         {
210             txt ~= fmt;
211         }
212 
213         if (level >= minLevel)
214         {
215             synchronized
216                 if (logFile.isOpen)
217                 {
218                     logFile.writeln(txt.data);
219                     logFile.flush();
220                 }
221         }
222     }
223     catch (Exception e)
224     {
225         // this is bad but what can we do..
226         debug assert(false, e.msg);
227     }
228 }
229 
230 /// Specifies the log level for a particular log message.
231 enum LogLevel
232 {
233     debug_,
234     diagnostic,
235     info,
236     warn,
237     error,
238     fatal,
239     none
240 }
241 
242 struct ExecutionTracer(LogLevel logLevel = LogLevel.diagnostic)
243 {
244     import std.datetime.stopwatch : StopWatch;
245     import std.typecons : Yes;
246 
247     string functionName;
248     StopWatch timer;
249 
250     this(int dummy, string fnName = __FUNCTION__) nothrow @safe
251     {
252         this.functionName = fnName;
253 
254         () @trusted { logJson(
255             logLevel,
256             `state`, `enter`,
257             `function`, this.functionName,
258         ); }();
259 
260         this.timer = StopWatch(Yes.autoStart);
261     }
262 
263     ~this() nothrow @safe
264     {
265         timer.stop();
266 
267         () @trusted { logJson(
268             logLevel,
269             `state`, `exit`,
270             `function`, functionName,
271             `timeElapsedSecs`, timer.peek().total!`hnsecs` * 100e-9,
272         ); }();
273     }
274 }
275 
276 string traceExecution(LogLevel logLevel = LogLevel.diagnostic)()
277 {
278     import std.conv : to;
279     import std.string : replace;
280     import std.traits : moduleName;
281 
282     return q"{
283         static import $thisModule;
284 
285         scope __executionTracer = $thisModule.ExecutionTracer!($logLevel)(0);
286     }"
287         .replace("$thisModule", moduleName!LogLevel)
288         .replace("$logLevel", "LogLevel." ~ logLevel.to!string);
289 }
290 
291 
292 unittest
293 {
294     import std.regex : ctRegex, matchFirst;
295     import std.stdio : File, stderr;
296     import vibe.data.json : Json, parseJsonString;
297 
298     auto logFile = File.tmpfile();
299     setLogFile(logFile);
300     scope (exit)
301         setLogFile(stderr);
302 
303     void doSomething()
304     {
305         mixin(traceExecution!(LogLevel.error));
306 
307         import core.thread : Thread;
308         import core.time : dur;
309 
310         Thread.getThis.sleep(dur!"hnsecs"(50));
311     }
312 
313     doSomething();
314     logFile.rewind();
315 
316     enum functionFQN = ctRegex!`dalicious\.log\.__unittest_L[0-9]+_C[0-9]+\.doSomething`;
317     auto observed1 = parseJsonString(logFile.readln);
318     auto observed2 = parseJsonString(logFile.readln);
319 
320     assert(observed1["thread"].type == Json.Type.int_);
321     assert(observed1["timestamp"].type == Json.Type.int_);
322     assert(observed1["state"] == "enter");
323     assert(matchFirst(observed1["function"].to!string, functionFQN));
324 
325     assert(observed2["thread"].type == Json.Type.int_);
326     assert(observed2["timestamp"].type == Json.Type.int_);
327     assert(observed2["state"] == "exit");
328     assert(matchFirst(observed2["function"].to!string, functionFQN));
329 }
330 
331 
332 /// Create a new todo. A todo list will be generated on end of compilation.
333 debug enum todo(string task, string file = __FILE__, size_t line = __LINE__) =
334     "[TODO] " ~ task ~ " at " ~ file ~ ":" ~ line.stringof;
335 
336 
337 /// Track progress over time on text-based output device.
338 class ProgressMeter
339 {
340     alias UnitSpec = Tuple!(size_t, "multiplier", char, "name");
341 
342 
343     /// Display unit for the progress meter.
344     enum Unit : UnitSpec
345     {
346         /// Automitcally select apropriate unit.α
347         ///
348         /// See_also: selectUnitFor
349         auto_ = UnitSpec(0, '\0'),
350 
351         /// Display raw counts.
352         one = UnitSpec(1, ' '),
353 
354         /// Display counts in units of `10^3`.
355         kilo = UnitSpec(10^^3, 'k'),
356 
357         /// Display counts in units of `10^6`.
358         mega = UnitSpec(10^^6, 'M'),
359 
360         /// Display counts in units of `10^9`.
361         giga = UnitSpec(10^^9, 'G'),
362 
363         /// Display counts in units of `10^12`.
364         peta = UnitSpec(10^^12, 'P'),
365 
366         /// Alias for `one`.
367         min = one,
368 
369         /// Alias for `peta`.
370         max = peta,
371     }
372 
373 
374     /// Display format for the progress meter.
375     enum Format : ubyte
376     {
377         /// Print an self-updating status line. This is meant for display on a
378         /// terminal. For output into a file `json` should be preferred.
379         human,
380         /// Print a series of JSON objects. This is meant for output into a
381         /// file. For output to a terminal `human` should be preferred.
382         ///
383         /// Example output:
384         ///
385         /// ```json
386         /// {"ticks":41,"elapsedSecs":20.000,"ticksPerSec":2.050,"etaSecs":88.15}
387         /// {"ticks":42,"elapsedSecs":21.000,"ticksPerSec":2.000,"etaSecs":84.00}
388         /// {"ticks":43,"elapsedSecs":22.000,"ticksPerSec":1.955,"etaSecs":80.16}
389         /// ```
390         json,
391     }
392 
393 
394     /// Set this to the total number of ticks if known.
395     size_t totalTicks;
396 
397     /// Print a progress update at most this frequently. Progress lines are
398     /// only ever printed on `start`, `stop`, `tick` and `updateNumTicks`.
399     size_t printEveryMsecs = 500;
400 
401     /// Select output format.
402     ///
403     /// See_also: Format
404     Format format;
405 
406     /// Suppress all output but still keep track of things.
407     Flag!"silent" silent;
408 
409     /// Display unit for format `human`.
410     Unit unit;
411 
412     /// Number of digits of decimal fractions.
413     size_t precision = 3;
414 
415     /// Current number of ticks. Automatically updates the status when the
416     /// timer is running.
417     ///
418     /// See_also: updateNumTicks
419     @property size_t numTicks() const pure nothrow @safe @nogc
420     {
421         return _numTicks;
422     }
423 
424     /// ditto
425     @property void numTicks(size_t value)
426     {
427         updateNumTicks(value);
428     }
429 
430     protected size_t _numTicks;
431     protected File _output;
432     protected bool hasOutput;
433     protected StopWatch timer;
434     protected StopWatch lastPrint;
435 
436 
437     this(
438         size_t totalTicks = 0,
439         size_t printEveryMsecs = 500,
440         Format format = Format.human,
441         Flag!"silent" silent = No.silent,
442         Unit unit = Unit.auto_,
443         size_t precision = 3,
444     ) pure nothrow @safe @nogc
445     {
446         this.totalTicks = totalTicks;
447         this.printEveryMsecs = printEveryMsecs;
448         this.format = format;
449         this.silent = silent;
450         this.unit = unit;
451         this.precision = precision;
452     }
453 
454 
455     ~this()
456     {
457         if(isRunning())
458             stop();
459     }
460 
461 
462     /// Get/set the output file.
463     @property void output(File output)
464     in (!isRunning)
465     {
466         hasOutput = true;
467         _output = output;
468     }
469 
470 
471     /// ditto
472     @property auto ref File output()
473     {
474         if (!hasOutput)
475             output = stderr;
476 
477         return _output;
478     }
479 
480 
481     /// Start the timer. Print the first status line if format is `human`.
482     void start()
483     in (!isRunning, "Attempting to start() a running ProgressMeter.")
484     {
485         _numTicks = 0;
486         if (!silent)
487         {
488             lastPrint.reset();
489             lastPrint.start();
490             printProgressLine(LineLocation.first);
491         }
492         timer.reset();
493         timer.start();
494     }
495 
496 
497     /// Advance the number of ticks. Prints a status line when at least
498     /// `printEveryMsecs` milliseconds have passed since the last print.
499     void tick(size_t newTicks = 1)
500     in (isRunning, "Attempting to tick() a stopped ProgressMeter.")
501     {
502         updateNumTicks(numTicks + newTicks);
503     }
504 
505 
506     /// Set the number of ticks. Prints a status line when at least
507     /// `printEveryMsecs` milliseconds have passed since the last print.
508     void updateNumTicks(size_t numTicks)
509     in (isRunning, "Attempting to update numTicks of a stopped ProgressMeter.")
510     {
511         this._numTicks = numTicks;
512 
513         if (!silent && lastPrint.peek.total!"msecs" >= printEveryMsecs)
514             printProgressLine(LineLocation.middle);
515     }
516 
517 
518     /// Stop the timer. Prints a final status line.
519     void stop()
520     in (isRunning, "Attempting to stop() a stopped ProgressMeter.")
521     {
522         timer.stop();
523 
524         if (!silent)
525             printProgressLine(LineLocation.last);
526     }
527 
528 
529     /// Returns true if the timer is running.
530     @property bool isRunning() const pure nothrow @safe @nogc
531     {
532         return timer.running();
533     }
534 
535 
536     ///
537     static enum isValidTimeUnit(string timeUnit) = is(typeof(timer.peek.total!timeUnit));
538     static assert(isValidTimeUnit!"msecs");
539 
540 
541     ///
542     @property auto elapsed(string timeUnit)() const nothrow @safe if (isValidTimeUnit!timeUnit)
543     {
544         return timer.peek.total!timeUnit;
545     }
546 
547 
548     ///
549     @property auto elapsedSecs() const nothrow @safe
550     {
551         try
552         {
553             return precision.predSwitch!"a <= b"(
554                 0, elapsed!"seconds",
555                 3, elapsed!"msecs" * 1e-3,
556                 6, elapsed!"usecs" * 1e-6,
557                 elapsed!"nsecs" * 1e-9,
558             );
559         }
560         catch (Exception)
561         {
562             assert(0);
563         }
564     }
565 
566 
567     ///
568     @property auto ticksPerSec() const nothrow @safe
569     {
570         return cast(double) numTicks / elapsedSecs;
571     }
572 
573 
574     /// Returns true when `etaSecs` is defined.
575     @property auto hasETA() const nothrow @safe
576     {
577         return totalTicks > 0 && numTicks > 0;
578     }
579 
580     /// ditto
581     alias hasEstimatedTimeOfArrival = hasETA;
582 
583 
584     /// Compute estimated time of arrival (ETA) with a simple linear model.
585     ///
586     /// ```d
587     /// const etaSecs = (totalTicks - numTicks)/ticksPerSec;
588     /// ```
589     ///
590     /// Returns:  Estimated time of arrival id `hasETA`; otherwise `double.infinity`.
591     @property double etaSecs() const nothrow @safe
592     {
593         return hasETA?  (totalTicks - numTicks)/ticksPerSec : double.infinity;
594     }
595 
596     /// ditto
597     alias estimatedTimeOfArrivalSecs = etaSecs;
598 
599 
600     /// Select unit such that number can be displayed with three leading
601     /// decimal digits.
602     static Unit selectUnitFor(size_t number) pure nothrow @safe
603     {
604         foreach (unit; EnumMembers!Unit)
605             if (unit.multiplier > 0 && number / unit.multiplier < 1000)
606                 return unit;
607         return Unit.max;
608     }
609 
610 
611 protected:
612 
613 
614     enum LineLocation : ubyte
615     {
616         first,
617         middle,
618         last,
619     }
620 
621 
622     void printProgressLine(LineLocation lineLocation)
623     {
624         final switch (format)
625         {
626             case Format.human:
627                 printHumanProgressLine(lineLocation);
628                 break;
629             case Format.json:
630                 printJsonProgressLine(lineLocation);
631                 break;
632         }
633 
634         lastPrint.reset();
635     }
636 
637 
638     void printHumanProgressLine(LineLocation lineLocation)
639     {
640         enum progressFormat = "\rrecords: %04.*f%c  elapsed: %04.*f sec  rate: %04.*f records/sec";
641         enum progressFormatWithTotal = "\rrecords: %04.*f/%04.*f%c (%04.2f%%) eta: %04.*f sec  elapsed: %04.*f sec  rate: %04.*f records/sec";
642 
643         auto unit = this.unit == Unit.auto_
644             ? selectUnitFor(max(numTicks, totalTicks))
645             : this.unit;
646 
647         if (!hasETA)
648             output.writef!progressFormat(
649                 precision,
650                 cast(double) numTicks / unit.multiplier,
651                 unit.name,
652                 precision,
653                 elapsedSecs,
654                 precision,
655                 cast(double) numTicks / elapsedSecs,
656             );
657         else
658             output.writef!progressFormatWithTotal(
659                 precision,
660                 cast(double) numTicks / unit.multiplier,
661                 precision,
662                 cast(double) totalTicks / unit.multiplier,
663                 unit.name,
664                 (100.0 * numTicks / totalTicks),
665                 precision,
666                 etaSecs,
667                 precision,
668                 elapsedSecs,
669                 precision,
670                 ticksPerSec,
671             );
672 
673         final switch (lineLocation)
674         {
675             case LineLocation.first:
676             case LineLocation.middle:
677                 output.flush();
678                 break;
679             case LineLocation.last:
680                 output.writeln();
681                 output.flush();
682                 break;
683         }
684     }
685 
686 
687     void printJsonProgressLine(LineLocation lineLocation)
688     {
689         enum formatPrefix = `{"ticks":%d,"elapsedSecs":%.*f`;
690         enum formatRate = `,"ticksPerSec":%.*f`;
691         enum formatNoRate = `,"ticksPerSec":"inf"`;
692         enum formatEta = `,"etaSecs":%.*f,"total":%d`;
693         enum formatSuffix = `}`;
694 
695         if (lineLocation == LineLocation.first)
696             return;
697 
698         output.writef!formatPrefix(
699             numTicks,
700             precision,
701             elapsedSecs,
702         );
703         if (elapsedSecs > 0)
704             output.writef!formatRate(
705                 precision,
706                 ticksPerSec,
707             );
708         else
709             output.writef!formatNoRate;
710 
711         if (hasETA)
712             output.writef!formatEta(precision, etaSecs, totalTicks);
713 
714         output.writefln!formatSuffix;
715         output.flush();
716     }
717 }
718 
719 unittest
720 {
721     import std.array;
722     import std.stdio;
723     import vibe.data.json : parseJsonString;
724 
725     auto logFile = File.tmpfile();
726     enum totalTicks = 5;
727     auto progress = new ProgressMeter(totalTicks, 0, ProgressMeter.Format.json);
728     progress.precision = 9;
729     progress.output = logFile;
730 
731     progress.start();
732     foreach (_; 0 .. totalTicks)
733         progress.tick();
734     progress.stop();
735 
736     logFile.rewind();
737     auto statusLines = [
738         parseJsonString(logFile.readln),
739         parseJsonString(logFile.readln),
740         parseJsonString(logFile.readln),
741         parseJsonString(logFile.readln),
742         parseJsonString(logFile.readln),
743         parseJsonString(logFile.readln),
744     ];
745 
746     assert(statusLines[0]["ticks"] == 1);
747     assert(statusLines[1]["ticks"] == 2);
748     assert(statusLines[2]["ticks"] == 3);
749     assert(statusLines[3]["ticks"] == 4);
750     assert(statusLines[4]["ticks"] == 5);
751     assert(statusLines[5]["ticks"] == 5);
752 
753     assert(statusLines[0]["elapsedSecs"] <= statusLines[1]["elapsedSecs"]);
754     assert(statusLines[1]["elapsedSecs"] <= statusLines[2]["elapsedSecs"]);
755     assert(statusLines[2]["elapsedSecs"] <= statusLines[3]["elapsedSecs"]);
756     assert(statusLines[3]["elapsedSecs"] <= statusLines[4]["elapsedSecs"]);
757     assert(statusLines[4]["elapsedSecs"] <= statusLines[5]["elapsedSecs"]);
758 
759     assert(statusLines[0]["etaSecs"] >= statusLines[4]["etaSecs"]);
760     assert(statusLines[5]["etaSecs"] == 0.0);
761     assert(statusLines[5]["ticksPerSec"].get!double > 0.0);
762 }