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