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 }