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