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 }