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 }