1 /** 2 * This module defines the `Logger`; the central component from which all log 3 * messages originate. You 4 */ 5 module slf4d.logger; 6 7 import slf4d.level; 8 import slf4d.handler; 9 import std.datetime : Clock, SysTime; 10 import std.typecons : Nullable, nullable; 11 import std.file; 12 13 /** 14 * The logger is the core component of SLF4D. Use it to generate log messages 15 * in your code. An ad-hoc Logger can be created and used anywhere, but usually 16 * you'll want to use the `getLogger()` function to obtain a pre-configured 17 * Logger that has been set up with an application-specific LogHandler. 18 * 19 * Note that the D language offers some special keywords like `__MODULE__` and 20 * `__PRETTY_FUNCTION__` which at compile time resolve to the respective source 21 * symbols, and are used heavily here for adding context to log messages. 22 * Because these must be resolved in the actual source location and *not* in 23 * the logger's own code, we make use of default values for most logging 24 * functions. Functions will usually end with a list of arguments that default 25 * to these keywords, and you shouldn't need to ever provide a value for these. 26 */ 27 struct Logger { 28 private shared LogHandler handler; 29 public const Level level; 30 public const string name; 31 32 /** 33 * Initializes a new logger. Usually, you won't use this constructor, and 34 * instead, you should obtain a Logger via `loggerFactory.getLogger()`. 35 * Params: 36 * handler = The handler to handle log messages. 37 * level = The log level of this logger. Only log messages with a level 38 * of equal or greater severity will be logged by this logger. 39 * name = The name of the logger. It defaults to the name of the module 40 * where the logger was initialized. 41 */ 42 public this(shared LogHandler handler, Level level = Levels.TRACE, string name = __MODULE__) { 43 this.handler = handler; 44 this.level = level; 45 this.name = name; 46 } 47 48 /** 49 * Gets a builder that can be used to fluently build a log message. 50 * Returns: The builder. 51 */ 52 public LogBuilder builder() { 53 return LogBuilder.forLogger(this); 54 } 55 56 /** 57 * Logs a message. Only messages whose level is greater than or equal to 58 * this logger's level will be logged. 59 * Params: 60 * msg = The message to log. 61 */ 62 public void log(immutable LogMessage msg) { 63 if (this.level.value <= msg.level.value) { 64 this.handler.handle(msg); 65 } 66 } 67 68 // Test the basic `log` method to ensure log messages are filtered according to the Logger's level. 69 unittest { 70 auto handler = new CachingLogHandler(); 71 Logger log = Logger(handler, Levels.INFO); 72 73 // Test logging something that's of a sufficient level. 74 LogMessage errorMessage = LogMessage( 75 Levels.ERROR, 76 "Oh no!", 77 log.name, 78 Clock.currTime(), 79 ExceptionInfo.from(new Exception("Oh no!")), 80 LogMessageSourceContext() 81 ); 82 log.log(errorMessage); 83 LogMessage lastMessage = handler.getMessages()[0]; 84 assert(lastMessage == errorMessage); 85 handler.reset(); 86 87 // Test logging something that's not of a sufficient level, and should not show up. 88 LogMessage traceMessage = LogMessage( 89 Levels.TRACE, 90 "Trace message", 91 log.name, 92 Clock.currTime(), 93 ExceptionInfo.from(null), 94 LogMessageSourceContext() 95 ); 96 log.log(traceMessage); 97 assert(handler.empty); 98 } 99 100 /** 101 * Logs a message. 102 * Params: 103 * level = The log level. 104 * msg = The string message to log. 105 * exception = The exception that prompted this log. This may be null. 106 * attributes = Key-value pairs of additional user-defined attributes. 107 * moduleName = The name of the module. This will resolve to the current 108 * module name by default. 109 * functionName = The name of the function. This will resolve to the 110 * current function name by default. 111 * fileName = The name of the source file. This will resolve to the 112 * current source file by default. 113 * lineNumber = The line number in the source file. This will resolve to 114 * the current source file's line number by default. 115 */ 116 public void log( 117 Level level, 118 string msg, 119 Exception exception = null, 120 string[string] attributes = string[string].init, 121 string moduleName = __MODULE__, 122 string functionName = __PRETTY_FUNCTION__, 123 string fileName = __FILE__, 124 size_t lineNumber = __LINE__ 125 ) { 126 if (attributes is null) { 127 attributes = string[string].init; 128 } 129 this.log(LogMessage( 130 level, 131 msg, 132 this.name, 133 Clock.currTime(), 134 ExceptionInfo.from(exception), 135 LogMessageSourceContext(moduleName, functionName, fileName, lineNumber), 136 cast(immutable(string[string])) attributes.dup 137 )); 138 } 139 140 /** 141 * Logs an exception message. It creates a basic message string formatted 142 * like "ExceptionName: message". 143 * Params: 144 * level = The log level. 145 * exception = The exception that prompted this log. 146 * attributes = Key-value pairs of additional user-defined attributes. 147 * moduleName = The name of the module. This will resolve to the current 148 * module name by default. 149 * functionName = The name of the function. This will resolve to the 150 * current function name by default. 151 * fileName = The name of the source file. This will resolve to the 152 * current source file by default. 153 * lineNumber = The line number in the source file. This will resolve to 154 * the current source file's line number by default. 155 */ 156 public void log( 157 Level level, 158 Exception exception, 159 string[string] attributes = string[string].init, 160 string moduleName = __MODULE__, 161 string functionName = __PRETTY_FUNCTION__, 162 string fileName = __FILE__, 163 size_t lineNumber = __LINE__ 164 ) { 165 string msg = exception.classinfo.name ~ ": " ~ exception.msg; 166 this.log(level, msg, exception, attributes, moduleName, functionName, fileName, lineNumber); 167 } 168 169 /** 170 * Logs a formatted string message. 171 * Params: 172 * level = The log level. 173 * args = The arguments for the formatted string. 174 * exception = The exception that prompted this log. This may be null. 175 * attributes = Key-value pairs of additional user-defined attributes. 176 * moduleName = The name of the module. This will resolve to the current 177 * module name by default. 178 * functionName = The name of the function. This will resolve to the 179 * current function name by default. 180 * fileName = The name of the source file. This will resolve to the 181 * current source file by default. 182 * lineNumber = The line number in the source file. This will resolve to 183 * the current source file's line number by default. 184 */ 185 public void logF(string fmt, T...)( 186 Level level, 187 T args, 188 Exception exception = null, 189 string[string] attributes = string[string].init, 190 string moduleName = __MODULE__, 191 string functionName = __PRETTY_FUNCTION__, 192 string fileName = __FILE__, 193 size_t lineNumber = __LINE__ 194 ) { 195 if (this.level.value <= level.value) { 196 import std.format : format; 197 this.log(level, format!(fmt)(args), exception, attributes, moduleName, functionName, fileName, lineNumber); 198 } 199 } 200 201 202 public void trace( 203 string msg, 204 Exception exception = null, 205 string[string] attributes = string[string].init, 206 string moduleName = __MODULE__, 207 string functionName = __PRETTY_FUNCTION__, 208 string fileName = __FILE__, 209 size_t lineNumber = __LINE__ 210 ) { 211 this.log(Levels.TRACE, msg, exception, attributes, moduleName, functionName, fileName, lineNumber); 212 } 213 214 public void trace( 215 Exception exception, 216 string[string] attributes = string[string].init, 217 string moduleName = __MODULE__, 218 string functionName = __PRETTY_FUNCTION__, 219 string fileName = __FILE__, 220 size_t lineNumber = __LINE__ 221 ) { 222 this.log(Levels.TRACE, exception, attributes, moduleName, functionName, fileName, lineNumber); 223 } 224 225 public void traceF(string fmt, T...)( 226 T args, 227 Exception exception = null, 228 string[string] attributes = string[string].init, 229 string moduleName = __MODULE__, 230 string functionName = __PRETTY_FUNCTION__, 231 string fileName = __FILE__, 232 size_t lineNumber = __LINE__ 233 ) { 234 this.logF!(fmt, T)(Levels.TRACE, args, exception, attributes, moduleName, functionName, fileName, lineNumber); 235 } 236 237 public LogBuilder traceBuilder() { 238 return LogBuilder.forLogger(this).lvl(Levels.TRACE); 239 } 240 241 public void debug_( 242 string msg, 243 Exception exception = null, 244 string[string] attributes = string[string].init, 245 string moduleName = __MODULE__, 246 string functionName = __PRETTY_FUNCTION__, 247 string fileName = __FILE__, 248 size_t lineNumber = __LINE__ 249 ) { 250 this.log(Levels.DEBUG, msg, exception, attributes, moduleName, functionName, fileName, lineNumber); 251 } 252 253 public void debug_( 254 Exception exception, 255 string[string] attributes = string[string].init, 256 string moduleName = __MODULE__, 257 string functionName = __PRETTY_FUNCTION__, 258 string fileName = __FILE__, 259 size_t lineNumber = __LINE__ 260 ) { 261 this.log(Levels.DEBUG, exception, attributes, moduleName, functionName, fileName, lineNumber); 262 } 263 264 public void debugF(string fmt, T...)( 265 T args, 266 Exception exception = null, 267 string[string] attributes = string[string].init, 268 string moduleName = __MODULE__, 269 string functionName = __PRETTY_FUNCTION__, 270 string fileName = __FILE__, 271 size_t lineNumber = __LINE__ 272 ) { 273 this.logF!(fmt, T)(Levels.DEBUG, args, exception, attributes, moduleName, functionName, fileName, lineNumber); 274 } 275 276 public LogBuilder debugBuilder() { 277 return LogBuilder.forLogger(this).lvl(Levels.DEBUG); 278 } 279 280 public void info( 281 string msg, 282 Exception exception = null, 283 string[string] attributes = string[string].init, 284 string moduleName = __MODULE__, 285 string functionName = __PRETTY_FUNCTION__, 286 string fileName = __FILE__, 287 size_t lineNumber = __LINE__ 288 ) { 289 this.log(Levels.INFO, msg, exception, attributes, moduleName, functionName, fileName, lineNumber); 290 } 291 292 public void info( 293 Exception exception, 294 string[string] attributes = string[string].init, 295 string moduleName = __MODULE__, 296 string functionName = __PRETTY_FUNCTION__, 297 string fileName = __FILE__, 298 size_t lineNumber = __LINE__ 299 ) { 300 this.log(Levels.INFO, exception, attributes, moduleName, functionName, fileName, lineNumber); 301 } 302 303 public void infoF(string fmt, T...)( 304 T args, 305 Exception exception = null, 306 string[string] attributes = string[string].init, 307 string moduleName = __MODULE__, 308 string functionName = __PRETTY_FUNCTION__, 309 string fileName = __FILE__, 310 size_t lineNumber = __LINE__ 311 ) { 312 this.logF!(fmt, T)(Levels.INFO, args, exception, attributes, moduleName, functionName, fileName, lineNumber); 313 } 314 315 public LogBuilder infoBuilder() { 316 return LogBuilder.forLogger(this).lvl(Levels.INFO); 317 } 318 319 public void warn( 320 string msg, 321 Exception exception = null, 322 string[string] attributes = string[string].init, 323 string moduleName = __MODULE__, 324 string functionName = __PRETTY_FUNCTION__, 325 string fileName = __FILE__, 326 size_t lineNumber = __LINE__ 327 ) { 328 this.log(Levels.WARN, msg, exception, attributes, moduleName, functionName, fileName, lineNumber); 329 } 330 331 public void warn( 332 Exception exception, 333 string[string] attributes = string[string].init, 334 string moduleName = __MODULE__, 335 string functionName = __PRETTY_FUNCTION__, 336 string fileName = __FILE__, 337 size_t lineNumber = __LINE__ 338 ) { 339 this.log(Levels.WARN, exception, attributes, moduleName, functionName, fileName, lineNumber); 340 } 341 342 public void warnF(string fmt, T...)( 343 T args, 344 Exception exception = null, 345 string[string] attributes = string[string].init, 346 string moduleName = __MODULE__, 347 string functionName = __PRETTY_FUNCTION__, 348 string fileName = __FILE__, 349 size_t lineNumber = __LINE__ 350 ) { 351 this.logF!(fmt, T)(Levels.WARN, args, exception, attributes, moduleName, functionName, fileName, lineNumber); 352 } 353 354 public LogBuilder warnBuilder() { 355 return LogBuilder.forLogger(this).lvl(Levels.WARN); 356 } 357 358 public void error( 359 string msg, 360 Exception exception = null, 361 string[string] attributes = string[string].init, 362 string moduleName = __MODULE__, 363 string functionName = __PRETTY_FUNCTION__, 364 string fileName = __FILE__, 365 size_t lineNumber = __LINE__ 366 ) { 367 this.log(Levels.ERROR, msg, exception, attributes, moduleName, functionName, fileName, lineNumber); 368 } 369 370 public void error( 371 Exception exception, 372 string[string] attributes = string[string].init, 373 string moduleName = __MODULE__, 374 string functionName = __PRETTY_FUNCTION__, 375 string fileName = __FILE__, 376 size_t lineNumber = __LINE__ 377 ) { 378 this.log(Levels.ERROR, exception, attributes, moduleName, functionName, fileName, lineNumber); 379 } 380 381 public void errorF(string fmt, T...)( 382 T args, 383 Exception exception = null, 384 string[string] attributes = string[string].init, 385 string moduleName = __MODULE__, 386 string functionName = __PRETTY_FUNCTION__, 387 string fileName = __FILE__, 388 size_t lineNumber = __LINE__ 389 ) { 390 this.logF!(fmt, T)(Levels.ERROR, args, exception, attributes, moduleName, functionName, fileName, lineNumber); 391 } 392 393 public LogBuilder errorBuilder() { 394 return LogBuilder.forLogger(this).lvl(Levels.ERROR); 395 } 396 397 // General test for all functions. 398 unittest { 399 import slf4d.testing_provider; 400 auto p = new TestingLoggingProvider(); 401 Logger log = p.getLoggerFactory().getLogger(); 402 403 log.log(Levels.INFO, "Hello world"); 404 assert(p.messageCount == 1); 405 log.logF!"Test %d"(Levels.WARN, 24); 406 assert(p.messageCount == 2); 407 log.logF!"Testing without args"(Levels.INFO); 408 assert(p.messageCount == 3); 409 p.reset(); 410 411 log.trace("Test"); 412 log.trace(new Exception("Oh no!")); 413 log.traceF!"Test %d"(42); 414 log.traceBuilder().msg("Test").log(); 415 assert(p.messages.length == 4); 416 p.reset(); 417 418 log.debug_("Test"); 419 log.debug_(new Exception("Oh no!")); 420 log.debugF!"Test %d"(42); 421 log.debugBuilder().msg("Test").log(); 422 assert(p.messages.length == 4); 423 p.reset(); 424 425 log.info("Test"); 426 log.info(new Exception("Oh no!")); 427 log.infoF!"Test %d"(42); 428 log.infoBuilder().msg("Test").log(); 429 assert(p.messages.length == 4); 430 p.reset(); 431 432 log.warn("Test"); 433 log.warn(new Exception("Oh no!")); 434 log.warnF!"Test %d"(42); 435 log.warnBuilder().msg("Test").log(); 436 assert(p.messages.length == 4); 437 p.reset(); 438 439 log.error("Test"); 440 log.error(new Exception("Oh no!")); 441 log.errorF!"Test %d"(42); 442 log.errorBuilder().msg("Test").log(); 443 assert(p.messages.length == 4); 444 p.reset(); 445 } 446 } 447 448 /** 449 * A log message that was created by a logger. 450 */ 451 struct LogMessage { 452 /** 453 * The log level for this message. This is an indication of the severity. 454 */ 455 public immutable Level level; 456 457 /** 458 * The actual content of the log message. 459 */ 460 public immutable string message; 461 462 /** 463 * The name of the logger that produced this log message. 464 */ 465 public immutable string loggerName; 466 467 /** 468 * The time at which this message occurred. 469 */ 470 public immutable SysTime timestamp; 471 472 /** 473 * The exception (if any) that was thrown when this message was logged. 474 */ 475 public immutable Nullable!ExceptionInfo exception; 476 477 /** 478 * Additional context about where this log message was generated in the 479 * program's source. 480 */ 481 public immutable LogMessageSourceContext sourceContext; 482 483 /** 484 * A key-value map of any additional user-defined attributes. 485 */ 486 public immutable string[string] attributes; 487 } 488 489 /** 490 * Container for information about where a log message was created in a 491 * program's source. 492 */ 493 struct LogMessageSourceContext { 494 /** 495 * The name of the D module that the log message was generated from. 496 */ 497 public immutable string moduleName; 498 499 /** 500 * The name of the function that the message was generated from. 501 */ 502 public immutable string functionName; 503 504 /** 505 * The name of the file that the message was generated from. 506 */ 507 public immutable string fileName; 508 509 /** 510 * The line number in the file where this message was generated. 511 */ 512 public immutable size_t lineNumber; 513 } 514 515 /** 516 * Container for information about an exception that was logged. 517 */ 518 struct ExceptionInfo { 519 /** 520 * The exception's message. 521 */ 522 public immutable string message; 523 524 /** 525 * The source file in which the exception occurred. 526 */ 527 public immutable string sourceFileName; 528 529 /** 530 * The line number at which the exception occurred. 531 */ 532 public immutable size_t sourceLineNumber; 533 534 /** 535 * The name of the exception class that was thrown. 536 */ 537 public immutable string exceptionClassName; 538 539 /** 540 * The stack trace for the exception, if it's available. 541 */ 542 public immutable Nullable!string stackTrace; 543 544 /** 545 * Constructs a nullable ExceptionInfo from an Exception object. 546 * Params: 547 * e = The exception. 548 * Returns: A nullable ExceptionInfo. 549 */ 550 public static Nullable!ExceptionInfo from(Exception e) { 551 if (e is null) return Nullable!ExceptionInfo.init; 552 553 Nullable!string st; 554 if (e.info !is null) { 555 st = e.info.toString(); 556 } 557 return ExceptionInfo(e.msg, e.file, e.line, e.classinfo.name, st).nullable; 558 } 559 560 unittest { 561 assert(ExceptionInfo.from(null).isNull); 562 563 try { 564 throw new Exception("Oh no!"); 565 } catch (Exception e) { 566 auto info = ExceptionInfo.from(e); 567 assert(!info.isNull); 568 assert(info.get.message == "Oh no!"); 569 assert(info.get.exceptionClassName == "object.Exception"); 570 } 571 } 572 } 573 574 /** 575 * A fluent builder for constructing a log message using method chaining. 576 */ 577 struct LogBuilder { 578 private Level level; 579 private string message; 580 private Logger logger; 581 private Exception exception; 582 private string[string] attributes; 583 584 /** 585 * Creates a log builder for the given logger. 586 * Params: 587 * logger = The logger that this builder will add the log message to. 588 * Returns: The log builder. 589 */ 590 package static LogBuilder forLogger(Logger logger) { 591 return LogBuilder(logger.level, "", logger); 592 } 593 594 /** 595 * Sets the level of the log message. 596 * Params: 597 * level = The log level. 598 * Returns: A reference to the builder. 599 */ 600 public ref LogBuilder lvl(Level level) return { 601 this.level = level; 602 return this; 603 } 604 605 /** 606 * Sets the message of the log. 607 * Params: 608 * message = The message to set. 609 * Returns: A reference to the builder. 610 */ 611 public ref LogBuilder msg(string message) return { 612 this.message = message; 613 return this; 614 } 615 616 /** 617 * Sets the exception of the log. 618 * Params: 619 * exception = The exception to set. 620 * Returns: A reference to the builder. 621 */ 622 public ref LogBuilder exc(Exception exception) return { 623 this.exception = exception; 624 return this; 625 } 626 627 /** 628 * Adds an attribute to the log message. 629 * Params: 630 * key = The attribute's key. 631 * value = The value of the attribute. 632 * Returns: A reference to the builder. 633 */ 634 public ref LogBuilder attr(string key, string value) return { 635 this.attributes[key] = value; 636 return this; 637 } 638 639 /** 640 * Builds the log message and adds it to the logger associated with this 641 * builder, and adds source context information using default function 642 * arguments. 643 * Params: 644 * moduleName = The name of the module. This will resolve to the current 645 * module name by default. 646 * functionName = The name of the function. This will resolve to the 647 * current function name by default. 648 * fileName = The name of the source file. This will resolve to the 649 * current source file by default. 650 * lineNumber = The line number in the source file. This will resolve to 651 * the current source file's line number by default. 652 */ 653 public void log( 654 string moduleName = __MODULE__, 655 string functionName = __PRETTY_FUNCTION__, 656 string fileName = __FILE__, 657 size_t lineNumber = __LINE__ 658 ) { 659 logger.log(LogMessage( 660 this.level, 661 this.message, 662 this.logger.name, 663 Clock.currTime(), 664 ExceptionInfo.from(this.exception), 665 LogMessageSourceContext( 666 moduleName, 667 functionName, 668 fileName, 669 lineNumber 670 ), 671 cast(immutable(string[string])) attributes.dup 672 )); 673 } 674 }