1 /++ 2 Contains the custom [KamelosoLogger] class, used to print timestamped and 3 (optionally) coloured logging messages. 4 5 Example: 6 --- 7 auto logger = new KamelosoLogger( 8 No.monochrome, 9 No.brigtTerminal, 10 No.headless, 11 No.flush); 12 13 logger.log("This is LogLevel.all"); 14 logger.info("LogLevel.info"); 15 logger.warn(".warn"); 16 logger.error(".error"); 17 logger.trace(".trace"); 18 //logger.fatal("This will crash the program."); 19 --- 20 21 See_Also: 22 [kameloso.terminal.colours] 23 24 Copyright: [JR](https://github.com/zorael) 25 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 26 27 Authors: 28 [JR](https://github.com/zorael) 29 +/ 30 module kameloso.logger; 31 32 private: 33 34 import std.typecons : Flag, No, Yes; 35 36 public: 37 38 39 // LogLevel 40 /++ 41 Logging levels; copied straight from [std.logger], to save us an import. 42 43 There are eight usable logging level. These level are $(I all), $(I trace), 44 $(I info), $(I warning), $(I error), $(I critical), $(I fatal), and $(I off). 45 If a log function with `LogLevel.fatal` is called the shutdown handler of 46 that logger is called. 47 +/ 48 enum LogLevel : ubyte 49 { 50 /++ 51 Lowest possible assignable `LogLevel`. 52 +/ 53 all = 1, 54 55 /++ 56 `LogLevel` for tracing the execution of the program. 57 +/ 58 trace = 32, 59 60 /++ 61 This level is used to display information about the program. 62 +/ 63 info = 64, 64 65 /++ 66 warnings about the program should be displayed with this level. 67 +/ 68 warning = 96, 69 70 /++ 71 Information about errors should be logged with this level. 72 +/ 73 error = 128, 74 75 /++ 76 Messages that inform about critical errors should be logged with this level. 77 +/ 78 critical = 160, 79 80 /++ 81 Log messages that describe fatal errors should use this level. 82 +/ 83 fatal = 192, 84 85 /++ 86 Highest possible `LogLevel`. 87 +/ 88 off = ubyte.max 89 } 90 91 92 // KamelosoLogger 93 /++ 94 Logger class, used to print timestamped and coloured logging messages. 95 96 It is thread-local so instantiate more if you're threading. 97 +/ 98 final class KamelosoLogger 99 { 100 private: 101 import kameloso.pods : CoreSettings; 102 import kameloso.terminal.colours.tags : expandTags; 103 import lu.conv : Enum; 104 import std.array : Appender; 105 import std.format : format; 106 import std.traits : EnumMembers; 107 108 version(Colours) 109 { 110 import kameloso.constants : DefaultColours; 111 import kameloso.terminal.colours.defs : TerminalForeground, TerminalReset; 112 import kameloso.terminal.colours : applyANSI, asANSI; 113 114 /// Convenience alias. 115 alias logcoloursBright = DefaultColours.logcoloursBright; 116 117 /// Ditto 118 alias logcoloursDark = DefaultColours.logcoloursDark; 119 } 120 121 /// Buffer to compose a line in before printing it to screen in one go. 122 Appender!(char[]) linebuffer; 123 124 /// Sub-buffer to compose the message in. 125 Appender!(char[]) messagebuffer; 126 127 /// The initial size to allocate for buffers. It will grow if needed. 128 enum bufferInitialSize = 4096; 129 130 bool monochrome; /// Whether to use colours or not in logger output. 131 bool brightTerminal; /// Whether or not to use colours for a bright background. 132 bool headless; /// Whether or not to disable all terminal output. 133 bool flush; /// Whether or not to flush standard out after writing to it. 134 135 public: 136 /++ 137 Creates a new [KamelosoLogger] with the passed settings. 138 139 Params: 140 monochrome = Whether or not to print colours. 141 brightTerminal = Bright terminal setting. 142 headless = Headless setting. 143 flush = Flush setting. 144 +/ 145 this( 146 const Flag!"monochrome" monochrome, 147 const Flag!"brightTerminal" brightTerminal, 148 const Flag!"headless" headless, 149 const Flag!"flush" flush) pure nothrow @safe 150 { 151 linebuffer.reserve(bufferInitialSize); 152 messagebuffer.reserve(bufferInitialSize); 153 this.monochrome = monochrome; 154 this.brightTerminal = brightTerminal; 155 this.headless = headless; 156 this.flush = flush; 157 } 158 159 /++ 160 Creates a new [KamelosoLogger] with settings divined from the passed 161 [kameloso.pods.CoreSettings|CoreSettings] struct. 162 163 Params: 164 settings = [kameloso.pods.CoreSettings|CoreSettings] whose 165 values to inherit. 166 +/ 167 this(const CoreSettings settings) pure nothrow @safe 168 { 169 linebuffer.reserve(bufferInitialSize); 170 messagebuffer.reserve(bufferInitialSize); 171 this.monochrome = settings.monochrome; 172 this.brightTerminal = settings.brightTerminal; 173 this.headless = settings.headless; 174 this.flush = settings.flush; 175 } 176 177 version(Colours) 178 { 179 // tint 180 /++ 181 Returns the corresponding 182 [kameloso.terminal.colours.defs.TerminalForeground|TerminalForeground] for the [LogLevel], 183 taking into account whether the terminal is said to be bright or not. 184 185 This is merely a convenient wrapping for [logcoloursBright] and 186 [logcoloursDark]. 187 188 Example: 189 --- 190 TerminalForeground errtint = KamelosoLogger.tint(LogLevel.error, No.brightTerminal); 191 immutable errtintString = errtint.asANSI; 192 --- 193 194 Params: 195 level = The [LogLevel] of the colour we want to scry. 196 bright = Whether the colour should be for a bright terminal 197 background or a dark one. 198 199 Returns: 200 A [kameloso.terminal.colours.defs.TerminalForeground|TerminalForeground] of 201 the right colour. Use with [kameloso.terminal.colours.asANSI|asANSI] 202 to get a string. 203 +/ 204 static uint tint(const LogLevel level, const Flag!"brightTerminal" bright) pure nothrow @nogc @safe 205 { 206 return bright ? logcoloursBright[level] : logcoloursDark[level]; 207 } 208 209 /// 210 unittest 211 { 212 import std.range : only; 213 214 auto range = only( 215 LogLevel.all, 216 LogLevel.info, 217 LogLevel.warning, 218 LogLevel.fatal); 219 220 foreach (immutable logLevel; range) 221 { 222 import std.format : format; 223 224 immutable tintBright = tint(logLevel, Yes.brightTerminal); 225 immutable tintBrightTable = logcoloursBright[logLevel]; 226 assert((tintBright == tintBrightTable), "%s != %s" 227 .format(tintBright, tintBrightTable)); 228 229 immutable tintDark = tint(logLevel, No.brightTerminal); 230 immutable tintDarkTable = logcoloursDark[logLevel]; 231 assert((tintDark == tintDarkTable), "%s != %s" 232 .format(tintDark, tintDarkTable)); 233 } 234 } 235 236 237 // tintImpl 238 /++ 239 Template for returning tints based on the settings of the `this` 240 [KamelosoLogger]. 241 242 This saves us having to pass the brightness setting, and allows for 243 making easy aliases for the log level. 244 245 Params: 246 level = Compile-time [LogLevel]. 247 248 Returns: 249 A tint string. 250 +/ 251 private auto tintImpl(LogLevel level)() const @property pure nothrow @nogc @safe 252 { 253 if (headless || monochrome) 254 { 255 return string.init; 256 } 257 else if (brightTerminal) 258 { 259 enum ctTintBright = tint(level, Yes.brightTerminal).asANSI.idup; 260 return ctTintBright; 261 } 262 else 263 { 264 enum ctTintDark = tint(level, No.brightTerminal).asANSI.idup; 265 return ctTintDark; 266 } 267 } 268 269 270 /+ 271 Generate *tint functions for each [LogLevel]. 272 +/ 273 static foreach (const lv; EnumMembers!LogLevel) 274 { 275 mixin( 276 "auto " ~ Enum!LogLevel.toString(lv) ~ "tint() const @property pure nothrow @nogc @safe 277 { 278 return tintImpl!(LogLevel." ~ Enum!LogLevel.toString(lv) ~ "); 279 }"); 280 } 281 282 /++ 283 Synonymous alias to `alltint`, as a workaround for [LogLevel.all] 284 not being named `LogLevel.log`. 285 +/ 286 alias logtint = alltint; 287 } 288 else 289 { 290 // offtint 291 /++ 292 Dummy function returning an empty string, since there can be no tints 293 on non-version `Colours` builds. 294 295 Returns: 296 An empty string. 297 +/ 298 public static auto offtint() pure nothrow @nogc @safe 299 { 300 return string.init; 301 } 302 303 /+ 304 Generate dummy *tint functions for each [LogLevel] by aliasing them 305 to [offtint]. 306 +/ 307 static foreach (const lv; EnumMembers!LogLevel) 308 { 309 static if (lv != LogLevel.off) 310 { 311 mixin("alias " ~ Enum!LogLevel.toString(lv) ~ "tint = offtint;"); 312 } 313 } 314 315 /++ 316 Synonymous alias to `alltint`, as a workaround for [LogLevel.all] 317 not being named `LogLevel.log`. 318 +/ 319 alias logtint = alltint; 320 } 321 322 323 /++ 324 Outputs the header of a logger message. 325 326 Params: 327 logLevel = The [LogLevel] to treat this message as being of. 328 +/ 329 private void beginLogMsg(const LogLevel logLevel) @safe 330 { 331 import std.datetime : DateTime; 332 import std.datetime.systime : Clock; 333 334 version(Colours) 335 { 336 if (!monochrome) 337 { 338 alias Timestamp = DefaultColours.TimestampColour; 339 //linebuffer.applyANSI(brightTerminal ? Timestamp.bright : Timestamp.dark); 340 linebuffer.applyANSI(TerminalReset.all); 341 } 342 } 343 344 linebuffer.put('['); 345 (cast(DateTime)Clock.currTime).timeOfDay.toString(linebuffer); 346 linebuffer.put("] "); 347 348 version(Colours) 349 { 350 if (!monochrome) 351 { 352 linebuffer.applyANSI(brightTerminal ? 353 logcoloursBright[logLevel] : 354 logcoloursDark[logLevel]); 355 } 356 } 357 } 358 359 360 /++ 361 Outputs the tail of a logger message. 362 +/ 363 private void finishLogMsg() @trusted // writeln trusts stdout.flush, so we should be able to too 364 { 365 import kameloso.terminal.colours : applyANSI; 366 import std.stdio : stdout, writeln; 367 368 version(Colours) 369 { 370 if (!monochrome) 371 { 372 // Reset.blink in case a fatal message was thrown 373 linebuffer.applyANSI(TerminalReset.all); 374 } 375 } 376 377 writeln(linebuffer.data); 378 if (flush) stdout.flush(); 379 } 380 381 382 // printImpl 383 /++ 384 Prints a timestamped log message to screen. Implementation function. 385 386 Prints the arguments as they are if possible (if they are some variant of 387 `char` or `char[]`), and otherwise tries to coerce them by using 388 [std.conv.to]. 389 390 Params: 391 logLevel = The [LogLevel] to treat this message as being of. 392 args = Variadic arguments to compose the output message with. 393 +/ 394 private void printImpl(Args...)(const LogLevel logLevel, auto ref Args args) 395 { 396 import lu.traits : UnqualArray; 397 import std.traits : isAggregateType; 398 399 scope(exit) 400 { 401 linebuffer.clear(); 402 messagebuffer.clear(); 403 } 404 405 beginLogMsg(logLevel); 406 407 foreach (ref arg; args) 408 { 409 alias T = typeof(arg); 410 411 static if (is(UnqualArray!T : char[]) || is(T : char)) 412 { 413 messagebuffer.put(arg); 414 } 415 else static if (is(T == enum)) 416 { 417 import lu.conv : Enum; 418 messagebuffer.put(Enum!T.toString(arg)); 419 } 420 else static if (isAggregateType!T && is(typeof(T.toString))) 421 { 422 import std.traits : isSomeFunction; 423 424 static if (isSomeFunction!(T.toString) || __traits(isTemplate, T.toString)) 425 { 426 static if (__traits(compiles, arg.toString(messagebuffer))) 427 { 428 // Output range sink overload (accepts an Appender) 429 arg.toString(messagebuffer); 430 } 431 else static if (__traits(compiles, 432 arg.toString((const(char)[] text) => messagebuffer.put(text)))) 433 { 434 // Output delegate sink overload 435 arg.toString((const(char)[] text) => messagebuffer.put(text)); 436 } 437 else static if (__traits(compiles, messagebuffer.put(arg.toString))) 438 { 439 // Plain string-returning function or template 440 messagebuffer.put(arg.toString); 441 } 442 else 443 { 444 import std.conv : to; 445 // std.conv.to fallback 446 messagebuffer.put(arg.to!string); 447 } 448 } 449 else static if (is(typeof(T.toString)) && 450 is(UnqualArray!(typeof(T.toString)) : char[])) 451 { 452 // toString string/char[] literal 453 messagebuffer.put(arg.toString); 454 } 455 else 456 { 457 import std.conv : to; 458 // std.conv.to fallback 459 messagebuffer.put(arg.to!string); 460 } 461 } 462 else 463 { 464 import std.conv : to; 465 // std.conv.to fallback 466 messagebuffer.put(arg.to!string); 467 } 468 } 469 470 linebuffer.put(messagebuffer.data.expandTags(logLevel, cast(Flag!"strip")monochrome)); 471 finishLogMsg(); 472 } 473 474 475 // printfImpl 476 /++ 477 Prints a timestamped log message to screen as per the passed runtime pattern, 478 in `printf` style. Implementation function. 479 480 Uses [std.format.formattedWrite|formattedWrite] to coerce the passed 481 arguments as the format pattern dictates. 482 483 Params: 484 logLevel = The [LogLevel] to treat this message as being of. 485 pattern = Runtime pattern to format the output with. 486 args = Variadic arguments to compose the output message with. 487 +/ 488 private void printfImpl(Args...) 489 (const LogLevel logLevel, 490 const string pattern, 491 auto ref Args args) 492 { 493 import std.format : formattedWrite; 494 495 scope(exit) 496 { 497 linebuffer.clear(); 498 messagebuffer.clear(); 499 } 500 501 beginLogMsg(logLevel); 502 messagebuffer.formattedWrite(pattern, args); 503 linebuffer.put(messagebuffer.data.expandTags(logLevel, cast(Flag!"strip")monochrome)); 504 finishLogMsg(); 505 } 506 507 508 // printfImpl 509 /++ 510 Prints a timestamped log message to screen as per the passed compile-time 511 pattern, in `printf` style. Implementation function. 512 513 Uses [std.format.formattedWrite|formattedWrite] to coerce the passed 514 arguments as the format pattern dictates. 515 516 Params: 517 pattern = Compile-time pattern to validate the arguments and format 518 the output with. 519 logLevel = The [LogLevel] to treat this message as being of. 520 args = Variadic arguments to compose the output message with. 521 +/ 522 private void printfImpl(string pattern, Args...)(const LogLevel logLevel, auto ref Args args) 523 { 524 import std.format : formattedWrite; 525 526 scope(exit) 527 { 528 linebuffer.clear(); 529 messagebuffer.clear(); 530 } 531 532 beginLogMsg(logLevel); 533 messagebuffer.formattedWrite!pattern(args); 534 linebuffer.put(messagebuffer.data.expandTags(logLevel, cast(Flag!"strip")monochrome)); 535 finishLogMsg(); 536 } 537 538 539 /// Mixin to error out on `fatal` calls. 540 private enum fatalErrorMixin = 541 `throw new Error("A fatal error message was logged");`; 542 543 /+ 544 Generate `trace`, `tracef`, `log`, `logf` and similar Logger-esque functions. 545 546 Mixes in [fatalExitMixin] on `fatal` to have it exit the program on those. 547 +/ 548 static foreach (const lv; EnumMembers!LogLevel) 549 { 550 mixin( 551 "void " ~ Enum!LogLevel.toString(lv) ~ "(Args...)(auto ref Args args) 552 { 553 if (!headless) printImpl(LogLevel." ~ Enum!LogLevel.toString(lv) ~ ", args); 554 " ~ ((lv == LogLevel.fatal) ? fatalErrorMixin : string.init) ~ " 555 } 556 557 void " ~ Enum!LogLevel.toString(lv) ~ "f(Args...)(const string pattern, auto ref Args args) 558 { 559 if (!headless) printfImpl(LogLevel." ~ Enum!LogLevel.toString(lv) ~ ", pattern, args); 560 " ~ ((lv == LogLevel.fatal) ? fatalErrorMixin : string.init) ~ " 561 } 562 563 void " ~ Enum!LogLevel.toString(lv) ~ "f(string pattern, Args...)(auto ref Args args) 564 { 565 if (!headless) printfImpl!pattern(LogLevel." ~ Enum!LogLevel.toString(lv) ~ ", args); 566 " ~ ((lv == LogLevel.fatal) ? fatalErrorMixin : string.init) ~ " 567 }"); 568 } 569 570 /++ 571 Synonymous alias to [KamelosoLogger.all], as a workaround for 572 [LogLevel.all] not being named `LogLevel.log`. 573 +/ 574 alias log = all; 575 576 /++ 577 Synonymous alias to [KamelosoLogger.allf], as a workaround for 578 [LogLevel.all] not being named `LogLevel.log`. 579 +/ 580 alias logf = allf; 581 } 582 583 /// 584 unittest 585 { 586 struct S1 587 { 588 void toString(Sink)(auto ref Sink sink) const 589 { 590 sink.put("sink toString"); 591 } 592 } 593 594 struct S2 595 { 596 void toString(scope void delegate(const(char)[]) dg) const 597 { 598 dg("delegate toString"); 599 } 600 601 @disable this(this); 602 } 603 604 struct S3 605 { 606 string s = "no toString"; 607 } 608 609 struct S4 610 { 611 string toString = "toString literal"; 612 } 613 614 struct S5 615 { 616 string toString()() const 617 { 618 return "template toString"; 619 } 620 } 621 622 class C 623 { 624 override string toString() const 625 { 626 return "plain toString"; 627 } 628 } 629 630 auto log_ = new KamelosoLogger(Yes.monochrome, No.brightTerminal, No.headless, Yes.flush); 631 632 log_.logf!"log: %s"("log"); 633 log_.infof!"log: %s"("info"); 634 log_.warningf!"log: %s"("warning"); 635 log_.errorf!"log: %s"("error"); 636 log_.criticalf!"log: %s"("critical"); 637 // log_.fatalf!"log: %s"("FATAL"); 638 log_.tracef!"log: %s"("trace"); 639 log_.offf!"log: %s"("off"); 640 641 version(Colours) 642 { 643 log_ = new KamelosoLogger(No.monochrome, Yes.brightTerminal, No.headless, Yes.flush); 644 645 log_.log("log: log"); 646 log_.info("log: info"); 647 log_.warning("log: warning"); 648 log_.error("log: error"); 649 log_.critical("log: critical"); 650 // log_.fatal("log: FATAL"); 651 log_.trace("log: trace"); 652 log_.off("log: off"); 653 654 log_ = new KamelosoLogger(No.monochrome, No.brightTerminal, No.headless, Yes.flush); 655 656 log_.log("log: log"); 657 log_.info("log: info"); 658 log_.warning("log: warning"); 659 log_.error("log: error"); 660 // log_.fatal("log: FATAL"); 661 log_.trace("log: trace"); 662 log_.off("log: off"); 663 } 664 665 log_.log("log <i>info</> log <w>warning</> log <e>error</> log <t>trace</> log <o>off</> log"); 666 667 S1 s1; 668 S2 s2; 669 S3 s3; 670 S4 s4; 671 S5 s5; 672 C c = new C; 673 674 log_.trace(); 675 676 log_.log(s1); 677 log_.info(s2); 678 log_.warning(s3); 679 log_.critical(s4); 680 log_.error(s5); 681 log_.trace(c); 682 683 log_.headless = true; 684 log_.error("THIS SHOULD NEVER BE SEEN"); 685 }