1 /******************************************************************************* 2 3 Logging system for applications 4 5 The most common pattern for using this class is to have a module-global 6 object and initialize it on construction: 7 8 --- 9 module superapp.main; 10 11 import ocean.util.log.Logger; 12 13 private Logger log; 14 static this () 15 { 16 log = Log.lookup("superapp.main"); 17 } 18 19 void main (string[] args) 20 { 21 log.info("App started with {} arguments: {}", args.length, args); 22 } 23 --- 24 25 This usage can however be problematic in complex cases, as it introduces 26 a module constructor which can lead to cycles during module construction, 27 hence sometimes it might be worth moving to a narrower scope 28 (e.g. nesting it inside a class / struct). 29 30 `Logger`s can be configured to output their message using a certain `Layout` 31 (see `ocean.util.log.layout` package), which will define how the message 32 looks like, and they can output to one or more `Appender`, which defines 33 where the message is writen. Common choices are standard outputs 34 (stdout / stderr), syslog, or a file. 35 36 In order to make `Logger` common use cases more simple, and allow flexible 37 usage of logger without needing to re-compile the application, a module 38 to configure a hierarchy from a configuration file is available under 39 `ocean.util.log.Config`. 40 41 Copyright: 42 Copyright (c) 2009-2017 dunnhumby Germany GmbH. 43 All rights reserved. 44 45 License: 46 Boost Software License Version 1.0. See LICENSE_BOOST.txt for details. 47 Alternatively, this file may be distributed under the terms of the Tango 48 3-Clause BSD License (see LICENSE_BSD.txt for details). 49 50 *******************************************************************************/ 51 52 module ocean.util.log.Logger; 53 54 import ocean.meta.types.Qualifiers; 55 import ocean.core.Verify; 56 import ocean.core.ExceptionDefinitions; 57 import ocean.io.model.IConduit; 58 import ocean.text.convert.Formatter; 59 import ocean.time.Clock; 60 import ocean.util.log.Appender; 61 import ocean.util.log.Event; 62 import ocean.util.log.Hierarchy; 63 import ocean.util.log.ILogger; 64 65 version (unittest) 66 { 67 import ocean.core.Test; 68 } 69 70 71 /******************************************************************************* 72 73 These represent the standard LOG4J event levels. 74 75 *******************************************************************************/ 76 77 public alias ILogger.Level Level; 78 79 80 /******************************************************************************* 81 82 Manager for routing Logger calls to the default hierarchy. Note 83 that you may have multiple hierarchies per application, but must 84 access the hierarchy directly for root() and lookup() methods within 85 each additional instance. 86 87 *******************************************************************************/ 88 89 public struct Log 90 { 91 alias typeof(this) This; 92 93 /*************************************************************************** 94 95 Structure for accumulating number of log events issued. 96 97 Note: 98 this takes the logging level in account, so calls that are not 99 logged because of the minimum logging level are not counted. 100 101 ***************************************************************************/ 102 103 public struct Stats 104 { 105 alias typeof(this) This; 106 107 /// Number of debug log events issued 108 public uint logged_debug; 109 110 /// Number of trace log events issued 111 public uint logged_trace; 112 113 /// Number of verbose log events issued 114 public uint logged_verbose; 115 116 /// Number of info log events issued 117 public uint logged_info; 118 119 /// Number of warn log events issued 120 public uint logged_warn; 121 122 /// Number of error log events issued 123 public uint logged_error; 124 125 /// Number of fatal log events issue 126 public uint logged_fatal; 127 128 static assert(Level.max == This.tupleof.length, 129 "Number of members doesn't match Levels"); 130 131 /*********************************************************************** 132 133 Total count of all events emitted during this period. 134 135 Returns: 136 Total count of all events emitted during this period. 137 138 ***********************************************************************/ 139 140 public uint total () 141 { 142 uint total; 143 144 foreach (field; this.tupleof) 145 { 146 total += field; 147 } 148 149 return total; 150 } 151 152 /// Resets the counters 153 private void reset () 154 { 155 foreach (ref field; this.tupleof) 156 { 157 field = field.init; 158 } 159 } 160 161 /*********************************************************************** 162 163 Accumulate the LogEvent into the stats. 164 165 Params: 166 event_level = level of the event that has been logged. 167 168 ***********************************************************************/ 169 170 private void accumulate (Level event_level) 171 { 172 with (Level) switch (event_level) 173 { 174 case Debug: 175 this.logged_debug++; 176 break; 177 case Trace: 178 this.logged_trace++; 179 break; 180 case Verbose: 181 this.logged_verbose++; 182 break; 183 case Info: 184 this.logged_info++; 185 break; 186 case Warn: 187 this.logged_warn++; 188 break; 189 case Error: 190 this.logged_error++; 191 break; 192 case Fatal: 193 this.logged_fatal++; 194 break; 195 case None: 196 break; 197 default: 198 assert(false, "Non supported log level"); 199 } 200 } 201 } 202 203 /// Stores all the existing `Logger` in a hierarchical manner 204 private static HierarchyT!(Logger) hierarchy_; 205 206 /// Logger stats 207 private static Stats logger_stats; 208 209 /*************************************************************************** 210 211 Return the enum value associated with `name`, or a default value 212 213 Params: 214 name = Case-independent string representation of an `ILogger.Level` 215 If the name is not one of the logger, `def` is returned. 216 def = Default value to return if no match is found for `name` 217 218 Returns: 219 The `Level` value for `name`, or `def` 220 221 ***************************************************************************/ 222 223 public static Level convert (cstring name, Level def = Level.Trace) 224 { 225 return ILogger.convert(name, def); 226 } 227 228 /*************************************************************************** 229 230 Return the name associated with level 231 232 Params: 233 level = The `Level` to get the name for 234 235 Returns: 236 The name associated with `level`. 237 238 ***************************************************************************/ 239 240 public static string convert (Level level) 241 { 242 return ILogger.convert(level); 243 } 244 245 246 /*************************************************************************** 247 248 Return an instance of the named logger 249 250 Names should be hierarchical in nature, using dot notation (with '.') 251 to separate each name section. For example, a typical name might be 252 something like "ocean.io.Stdout". 253 254 If the logger does not currently exist, it is created and inserted into 255 the hierarchy. A parent will be attached to it, which will be either 256 the root logger or the closest ancestor in terms of the hierarchical 257 name space. 258 259 ***************************************************************************/ 260 261 public static Logger lookup (cstring name) 262 { 263 return This.hierarchy().lookup(name); 264 } 265 266 /*************************************************************************** 267 268 Return the root Logger instance. 269 270 This is the ancestor of all loggers and, as such, can be used to 271 manipulate the entire hierarchy. For instance, setting the root 'level' 272 attribute will affect all other loggers in the tree. 273 274 ***************************************************************************/ 275 276 public static Logger root () 277 { 278 return This.hierarchy().root; 279 } 280 281 /*************************************************************************** 282 283 Return (and potentially initialize) the hierarchy singleton 284 285 Logger themselves have little knowledge about their hierarchy. 286 Everything is handled by a `HierarchyT!(Logger)` instance, which is 287 stored as a singleton in this `struct`, and for which convenient 288 functions are provided. 289 This function returns said singleton, and initialize it on first call. 290 291 ***************************************************************************/ 292 293 public static HierarchyT!(Logger) hierarchy () 294 { 295 if (This.hierarchy_ is null) 296 { 297 This.hierarchy_ = new HierarchyT!(Logger)("ocean"); 298 } 299 return This.hierarchy_; 300 } 301 302 303 /*************************************************************************** 304 305 Initialize the behaviour of a basic logging hierarchy. 306 307 Adds a StreamAppender to the root node, and sets the activity level 308 to be everything enabled. 309 310 ***************************************************************************/ 311 312 public static void config (OutputStream stream, bool flush = true) 313 { 314 This.root.add(new AppendStream(stream, flush)); 315 } 316 317 /*************************************************************************** 318 319 Gets the stats of the logger system between two calls to this method. 320 321 Returns: 322 number of log events issued after last call to stats, aggregated 323 per logger level 324 325 ***************************************************************************/ 326 327 public static Stats stats () 328 { 329 // Make a copy to return 330 Stats s = This.logger_stats; 331 This.logger_stats.reset(); 332 333 return s; 334 } 335 } 336 337 338 /******************************************************************************* 339 340 Loggers are named entities, sometimes shared, sometimes specific to 341 a particular portion of code. The names are generally hierarchical in 342 nature, using dot notation (with '.') to separate each named section. 343 For example, a typical name might be something like "mail.send.writer" 344 345 --- 346 import ocean.util.log.Logger; 347 348 auto log = Log.lookup("mail.send.writer"); 349 350 log.info("an informational message"); 351 log.error("an exception message: {}", exception.toString); 352 353 // etc ... 354 --- 355 356 It is considered good form to pass a logger instance as a function or 357 class-ctor argument, or to assign a new logger instance during static 358 class construction. For example: if it were considered appropriate to 359 have one logger instance per class, each might be constructed like so: 360 --- 361 module myapp.util.Transmogrifier; 362 363 private Logger log; 364 365 static this() 366 { 367 log = Log.lookup("myapp.util.Transmogrifier"); 368 } 369 --- 370 371 Messages passed to a Logger are assumed to be either self-contained 372 or configured with "{}" notation a la `ocean.text.convert.Formatter`: 373 --- 374 log.warn ("temperature is {} degrees!", 101); 375 --- 376 377 Note that an internal workspace is used to format the message, which 378 is limited to 2048 bytes. Use "{.256}" truncation notation to limit 379 the size of individual message components. You can also use your own 380 formatting buffer: 381 --- 382 log.buffer(new char[](4096)); 383 384 log.warn("a very long warning: {}", someLongWarning); 385 --- 386 387 Or you can use explicit formatting: 388 --- 389 char[4096] buf = void; 390 391 log.warn(log.format(buf, "a very long warning: {}", someLongWarning)); 392 --- 393 394 If argument construction has some overhead which you'd like to avoid, 395 you can check to see whether a logger is active or not: 396 397 --- 398 if (log.enabled(log.Warn)) 399 log.warn("temperature is {} degrees!", complexFunction()); 400 --- 401 402 The `ocean.util.log` package closely follows both the API and the behaviour 403 as documented at the official Log4J site, where you'll find a good tutorial. 404 Those pages are hosted over: 405 http://logging.apache.org/log4j/docs/documentation.html 406 407 *******************************************************************************/ 408 409 public final class Logger : ILogger 410 { 411 public alias Level.Info Debug; /// Shortcut to `Level` values 412 public alias Level.Trace Trace; /// Ditto 413 public alias Level.Info Verbose; /// Ditto 414 public alias Level.Info Info; /// Ditto 415 public alias Level.Warn Warn; /// Ditto 416 public alias Level.Error Error; /// Ditto 417 public alias Level.Fatal Fatal; /// Ditto 418 419 /// The hierarchy that host this logger (most likely Log.hierarchy). 420 private HierarchyT!(Logger) host_; 421 /// Next logger in the list, maintained by Hierarchy 422 package Logger next; 423 /// Parent of this logger (maintained by Hierarchy) 424 package Logger parent; 425 /// List of `Appender`s this Logger emits messages to 426 private Appender appender_; 427 /// Name of this logger 428 private string name_; 429 /// Buffer to use for formatting. Can be `null`, see `buffer` properties 430 private mstring buffer_; 431 /// `Level` at which this `Logger` is configured 432 package Level level_; 433 /// When `true`, this `Logger` will use its ancestors `Appender`s as well 434 private bool additive_; 435 /// Indicator if the log emits should be counted towards global stats. 436 package bool collect_stats; 437 438 /*************************************************************************** 439 440 Construct a LoggerInstance with the specified name for the given 441 hierarchy. By default, logger instances are additive and are set 442 to emit all events. 443 444 Params: 445 host = Hierarchy instance that is hosting this logger 446 name = name of this Logger 447 448 ***************************************************************************/ 449 450 package this (HierarchyT!(Logger) host, string name) 451 { 452 this.host_ = host; 453 this.level_ = Level.Trace; 454 this.additive_ = true; 455 this.collect_stats = true; 456 this.name_ = name; 457 this.buffer_ = new mstring(2048); 458 } 459 460 /*************************************************************************** 461 462 Is this logger enabled for the specified Level? 463 464 Params: 465 level = Level to check for. 466 467 Returns: 468 `true` if `level` is `>=` to the current level of this `Logger`. 469 470 ***************************************************************************/ 471 472 public bool enabled (Level level = Level.Fatal) 473 { 474 return this.host_.context.enabled(this.level_, level); 475 } 476 477 /*************************************************************************** 478 479 Append a message with a severity of `Level.Debug`. 480 481 Unlike other log level, this has to be abbreviated as `debug` 482 is a keyword. 483 484 Params: 485 Args = Auto-deduced format string arguments 486 fmt = Format string to use. 487 See `ocean.text.convert.Formatter` documentation for 488 more informations. 489 args = Arguments to format according to `fmt`. 490 491 ***************************************************************************/ 492 493 public void dbg (Args...) (cstring fmt, Args args) 494 { 495 this.format(Level.Debug, fmt, args); 496 } 497 498 /*************************************************************************** 499 500 Append a message with a severity of `Level.Trace`. 501 502 Params: 503 Args = Auto-deduced format string arguments 504 fmt = Format string to use. 505 See `ocean.text.convert.Formatter` documentation for 506 more informations. 507 args = Arguments to format according to `fmt`. 508 509 ***************************************************************************/ 510 511 public void trace (Args...) (cstring fmt, Args args) 512 { 513 this.format(Level.Trace, fmt, args); 514 } 515 516 /*************************************************************************** 517 518 Append a message with a severity of `Level.Verbose`. 519 520 Params: 521 Args = Auto-deduced format string arguments 522 fmt = Format string to use. 523 See `ocean.text.convert.Formatter` documentation for 524 more informations. 525 args = Arguments to format according to `fmt`. 526 527 ***************************************************************************/ 528 529 public void verbose (Args...) (cstring fmt, Args args) 530 { 531 this.format(Level.Debug, fmt, args); 532 } 533 534 /*************************************************************************** 535 536 Append a message with a severity of `Level.Info`. 537 538 Params: 539 Args = Auto-deduced format string arguments 540 fmt = Format string to use. 541 See `ocean.text.convert.Formatter` documentation for 542 more informations. 543 args = Arguments to format according to `fmt`. 544 545 ***************************************************************************/ 546 547 public void info (Args...) (cstring fmt, Args args) 548 { 549 this.format(Level.Info, fmt, args); 550 } 551 552 /*************************************************************************** 553 554 Append a message with a severity of `Level.Warn`. 555 556 Params: 557 Args = Auto-deduced format string arguments 558 fmt = Format string to use. 559 See `ocean.text.convert.Formatter` documentation for 560 more informations. 561 args = Arguments to format according to `fmt`. 562 563 ***************************************************************************/ 564 565 public void warn (Args...) (cstring fmt, Args args) 566 { 567 this.format(Level.Warn, fmt, args); 568 } 569 570 /*************************************************************************** 571 572 Append a message with a severity of `Level.Error`. 573 574 Params: 575 Args = Auto-deduced format string arguments 576 fmt = Format string to use. 577 See `ocean.text.convert.Formatter` documentation for 578 more informations. 579 args = Arguments to format according to `fmt`. 580 581 ***************************************************************************/ 582 583 public void error (Args...) (cstring fmt, Args args) 584 { 585 this.format(Level.Error, fmt, args); 586 } 587 588 /*************************************************************************** 589 590 Append a message with a severity of `Level.Fatal`. 591 592 Params: 593 Args = Auto-deduced format string arguments 594 fmt = Format string to use. 595 See `ocean.text.convert.Formatter` documentation for 596 more informations. 597 args = Arguments to format according to `fmt`. 598 599 ***************************************************************************/ 600 601 public void fatal (Args...) (cstring fmt, Args args) 602 { 603 this.format(Level.Fatal, fmt, args); 604 } 605 606 /*************************************************************************** 607 608 Returns: 609 The name of this Logger (sans the appended dot). 610 611 ***************************************************************************/ 612 613 public string name () 614 { 615 auto i = this.name_.length; 616 if (i > 0) 617 --i; 618 return this.name_[0 .. i]; 619 } 620 621 /*************************************************************************** 622 623 Returns: 624 The Level this logger is set to 625 626 ***************************************************************************/ 627 628 public Level level () 629 { 630 return this.level_; 631 } 632 633 /*************************************************************************** 634 635 Set the current level for this logger (and only this logger). 636 637 ***************************************************************************/ 638 639 public Logger level (Level l) 640 { 641 return this.level(l, false); 642 } 643 644 /*************************************************************************** 645 646 Set the current level for this logger, 647 and (optionally) all of its descendants. 648 649 Params: 650 level = Level to set the Logger(s) to 651 propagate = If `true`, set the `level` to all of this `Logger`'s 652 descendants as well. 653 654 Returns: 655 `this` 656 657 ***************************************************************************/ 658 659 public Logger level (Level level, bool propagate) 660 { 661 this.level_ = level; 662 663 if (propagate) 664 { 665 this.host_.propagateValue!("level_")(this.name_, level); 666 } 667 668 return this; 669 } 670 671 /*************************************************************************** 672 673 Is this logger additive configured as additive ? 674 675 Additive loggers should walk ancestors looking for more appenders. 676 677 ***************************************************************************/ 678 679 public bool additive () 680 { 681 return this.additive_; 682 } 683 684 /*************************************************************************** 685 686 Set the additive status of this logger. See bool additive(). 687 688 Params: 689 enabled = new value for the additive property. 690 691 Returns: 692 `this` 693 694 ***************************************************************************/ 695 696 public Logger additive (bool enabled) 697 { 698 this.additive_ = enabled; 699 return this; 700 } 701 702 /*************************************************************************** 703 704 Add (another) appender to this logger. 705 706 Appenders are each invoked for log events as they are produced. 707 At most, one instance of each appender will be invoked. 708 709 Params: 710 another = Appender to add to this logger. Mustn't be `null`. 711 712 Returns: 713 `this` 714 715 ***************************************************************************/ 716 717 public Logger add (Appender another) 718 { 719 verify(another !is null); 720 another.next = appender_; 721 this.appender_ = another; 722 return this; 723 } 724 725 /*************************************************************************** 726 727 Remove all appenders from this Logger 728 729 Returns: 730 `this` 731 732 ***************************************************************************/ 733 734 public Logger clear () 735 { 736 this.appender_ = null; 737 return this; 738 } 739 740 /*************************************************************************** 741 742 Returns: 743 The current formatting buffer (null if none). 744 745 ***************************************************************************/ 746 747 public mstring buffer () 748 { 749 return this.buffer_; 750 } 751 752 /*************************************************************************** 753 754 Set the current formatting buffer. 755 756 The size of the internal buffer determines the length of the string that 757 can be logged. 758 To avoid GC allocations during logging, and excessive memory allocation 759 when user types are logged, the buffer is never resized internally. 760 761 Params: 762 buf = Buffer to use. If `null` is used, nothing will be logged. 763 764 Returns: 765 `this` 766 767 ***************************************************************************/ 768 769 public Logger buffer (mstring buf) 770 { 771 buffer_ = buf; 772 return this; 773 } 774 775 /*************************************************************************** 776 777 Toggles the stats collecting for this logger and optionally 778 for all its descendants. 779 780 Params: 781 value = indicator if the stats collection for this logger should 782 happen 783 propagate = should we propagate this change to all children 784 loggers 785 786 ***************************************************************************/ 787 788 public void collectStats (bool value, bool propagate) 789 { 790 this.collect_stats = value; 791 792 if (propagate) 793 { 794 this.host_.propagateValue!("collect_stats")(this.name_, value); 795 } 796 } 797 798 /*************************************************************************** 799 800 Returns: 801 Time since the program start 802 803 ***************************************************************************/ 804 805 public TimeSpan runtime () 806 { 807 return Clock.now - Clock.startTime(); 808 } 809 810 /*************************************************************************** 811 812 Emit a textual log message from the given string 813 814 Params: 815 level = Message severity 816 exp = Lazily evaluated string. 817 If the provided `level` is not enabled, `exp` won't be 818 evaluated at all. 819 820 Returns: 821 `this` 822 823 ***************************************************************************/ 824 825 public Logger append (Level level, lazy cstring exp) 826 { 827 if (host_.context.enabled (level_, level)) 828 { 829 LogEvent event = { 830 level: level, 831 name: name.length ? name_[0..$-1] : "root", 832 host: host_, 833 time: Clock.now, 834 msg: exp, 835 }; 836 this.append(event); 837 } 838 return this; 839 } 840 841 /// Ditto 842 public alias append opCall; 843 844 /*************************************************************************** 845 846 Emit a log message 847 848 Implementation part of the public-ly available `append` function. 849 This walks through appenders and emit the message for each `Appender` 850 it can be emitted for. 851 852 Params: 853 event = a `LogEvent` containing informations about the message 854 to emit. 855 856 ***************************************************************************/ 857 858 private void append (LogEvent event) 859 { 860 // indicator if the event was at least once emitted to the 861 // appender (to use for global stats) 862 bool event_emitted; 863 864 // combine appenders from all ancestors 865 auto links = this; 866 Appender.Mask masks = 0; 867 do { 868 auto appender = links.appender_; 869 870 // this level have an appender? 871 while (appender) 872 { 873 auto mask = appender.mask; 874 875 // have we visited this appender already? 876 if ((masks & mask) is 0) 877 // is appender enabled for this level? 878 if (appender.level <= event.level) 879 { 880 // append message and update mask 881 appender.append(event); 882 masks |= mask; 883 event_emitted = true; 884 } 885 // process all appenders for this node 886 appender = appender.next; 887 } 888 // process all ancestors 889 } while (links.additive_ && ((links = links.parent) !is null)); 890 891 // If the event was emitted to at least one appender, and the 892 // collecting stats for this log is enabled, increment the 893 // stats counters 894 if (this.collect_stats && event_emitted) 895 { 896 Log.logger_stats.accumulate(event.level); 897 } 898 } 899 900 /*************************************************************************** 901 902 Format and emit a textual log message from the given arguments 903 904 The formatted string emitted will have a length up to `buffer.length`, 905 which is 2048 by default. 906 If no formatting argument is provided (the call has only 2 parameters, 907 e.g. `format(Level.Trace, "Baguette");`), then the string will be just 908 emitted to the appender(s) verbatim and won't be limited in length. 909 910 Params: 911 Args = Auto-deduced argument list 912 level = Message severity 913 fmt = Format string to use, see `ocean.text.convert.Formatter` 914 args = Arguments to format according to `fmt`. 915 916 ***************************************************************************/ 917 918 public void format (Args...) (Level level, cstring fmt, Args args) 919 { 920 static if (Args.length == 0) 921 this.append(level, fmt); 922 else 923 { 924 // If the buffer has length 0 / is null, we just don't log anything 925 if (this.buffer_.length) 926 this.append(level, snformat(this.buffer_, fmt, args)); 927 } 928 } 929 930 /*************************************************************************** 931 932 See if the provided Logger name is a parent of this one. 933 934 Note that each Logger name has a '.' appended to the end, such that 935 name segments will not partially match. 936 937 ***************************************************************************/ 938 939 package bool isChildOf (string candidate) 940 { 941 auto len = candidate.length; 942 943 // possible parent if length is shorter 944 if (len < this.name_.length) 945 // does the prefix match? Note we append a "." to each 946 // (the root is a parent of everything) 947 return (len == 0 || candidate == this.name_[0 .. len]); 948 return false; 949 } 950 951 /*************************************************************************** 952 953 See if the provided `Logger` is a better match as a parent of this one. 954 955 This is used to restructure the hierarchy when a new logger instance 956 is introduced 957 958 ***************************************************************************/ 959 960 package bool isCloserAncestor (Logger other) 961 { 962 auto name = other.name_; 963 if (this.isChildOf(name)) 964 // is this a better (longer) match than prior parent? 965 if ((this.parent is null) 966 || (name.length >= this.parent.name_.length)) 967 return true; 968 return false; 969 } 970 } 971 972 973 // Instantiation test for templated functions 974 unittest 975 { 976 static void test () 977 { 978 Logger log = Log.lookup("ocean.util.log.Logger"); 979 log.dbg("Souvent, pour s'amuser, les hommes d'équipage"); 980 log.trace("Prennent des albatros, vastes oiseaux des mers,"); 981 log.verbose("Qui suivent, indolents compagnons de voyage,"); 982 log.info("Le navire glissant sur les gouffres amers."); 983 984 log.warn("Le Poète est semblable au prince des nuées"); 985 log.error("Qui hante la tempête et se rit de l'archer"); 986 log.fatal("Exilé sur le sol au milieu des huées,"); 987 log.format(Level.Fatal, "Ses ailes de géant l'empêchent de marcher."); 988 } 989 } 990 991 unittest 992 { 993 test!("==")(Log.convert("info"), Level.Info); 994 test!("==")(Log.convert("Info"), Level.Info); 995 test!("==")(Log.convert("INFO"), Level.Info); 996 test!("==")(Log.convert("FATAL"), Level.Fatal); 997 // Use the default value 998 test!("==")(Log.convert("Info!"), Level.Trace); 999 test!("==")(Log.convert("Baguette", Level.Warn), Level.Warn); 1000 // The first entry in the array 1001 test!("==")(Log.convert("trace", Level.Error), Level.Trace); 1002 } 1003 1004 // Test that argumentless format call does not shrink the output 1005 unittest 1006 { 1007 static class Buffer : Appender 1008 { 1009 public struct Event { Logger.Level level; cstring message; } 1010 public Event[] result; 1011 1012 public override Mask mask () { Mask m = 42; return m; } 1013 public override string name () { return "BufferAppender"; } 1014 public override void append (LogEvent e) 1015 { 1016 this.result ~= Event(e.level, e.msg); 1017 } 1018 } 1019 1020 // Test string of 87 chars 1021 static immutable TestStr = "Ce qui se conçoit bien s'énonce clairement - Et les mots pour le dire arrivent aisément"; 1022 scope appender = new Buffer(); 1023 char[32] log_buffer; 1024 Logger log = (new Logger(Log.hierarchy(), "dummy")) 1025 .additive(false).add(appender).buffer(log_buffer); 1026 log.info("{}", TestStr); 1027 log.error(TestStr); 1028 test!("==")(appender.result.length, 2); 1029 // Trimmed string 1030 test!("==")(appender.result[0].level, Logger.Level.Info); 1031 test!("==")(appender.result[0].message, TestStr[0 .. 32]); 1032 // Full string 1033 test!("==")(appender.result[1].level, Logger.Level.Error); 1034 test!("==")(appender.result[1].message, TestStr); 1035 } 1036 1037 // Test that the logger does not allocate if the Appender does not 1038 unittest 1039 { 1040 static class StaticBuffer : Appender 1041 { 1042 public struct Event { Logger.Level level; cstring message; char[128] buffer; } 1043 1044 private Event[6] buffers; 1045 private size_t index; 1046 1047 public override Mask mask () { Mask m = 42; return m; } 1048 public override string name () { return "StaticBufferAppender"; } 1049 public override void append (LogEvent e) 1050 { 1051 assert(this.index < this.buffers.length); 1052 auto str = snformat(this.buffers[this.index].buffer, "{}", e.msg); 1053 this.buffers[this.index].message = str; 1054 this.buffers[this.index++].level = e.level; 1055 } 1056 } 1057 1058 scope appender = new StaticBuffer; 1059 Logger log = (new Logger(Log.hierarchy(), "dummy")) 1060 .additive(false) 1061 .add(appender); 1062 1063 testNoAlloc({ 1064 scope obj = new Object; 1065 log.trace("If you can keep your head when all about you, {}", 1066 "Are losing theirs and blaming it on you;"); 1067 log.info("If you can trust yourself when all men doubt you,"); 1068 log.warn("But make {} for their {} too;", "allowance", "doubting"); 1069 log.error("You'll have to google for the rest I'm afraid"); 1070 log.fatal("{} - {} - {} - {}", 1071 "This is some arg fmt", 42, obj, 1337.0f); 1072 log.format(Logger.Level.Info, "Just some {}", "more allocation tests"); 1073 }()); 1074 1075 test!("==")(appender.buffers[0].level, Logger.Level.Trace); 1076 test!("==")(appender.buffers[1].level, Logger.Level.Info); 1077 test!("==")(appender.buffers[2].level, Logger.Level.Warn); 1078 test!("==")(appender.buffers[3].level, Logger.Level.Error); 1079 test!("==")(appender.buffers[4].level, Logger.Level.Fatal); 1080 test!("==")(appender.buffers[5].level, Logger.Level.Info); 1081 1082 test!("==")(appender.buffers[0].message, 1083 "If you can keep your head when all about you, " ~ 1084 "Are losing theirs and blaming it on you;"); 1085 test!("==")(appender.buffers[1].message, 1086 "If you can trust yourself when all men doubt you,"); 1087 test!("==")(appender.buffers[2].message, 1088 "But make allowance for their doubting too;"); 1089 test!("==")(appender.buffers[3].message, 1090 "You'll have to google for the rest I'm afraid"); 1091 test!("==")(appender.buffers[4].message, 1092 "This is some arg fmt - 42 - object.Object - 1337.00"); 1093 test!("==")(appender.buffers[5].message, "Just some more allocation tests"); 1094 } 1095 1096 unittest 1097 { 1098 Logger log = (new Logger(Log.hierarchy(), "dummy")).additive(false); 1099 test!(">=")(log.runtime(), TimeSpan.fromNanos(0)); 1100 }