1 /*******************************************************************************
2 
3     Utility functions to configure tango loggers from a config file.
4 
5     Configures tango loggers, uses the AppendSyslog class to provide logfile
6     rotation.
7 
8     In the config file, a logger can be configured using the following syntax:
9 
10         ; Which logger to configure. In this case LoggerName is being configured.
11         ; A whole hierachy can be specified like LOG.MyApp.ThatOutput.X
12         ; And each level can be configured.
13         [LOG.LoggerName]
14 
15         ; Whether to output to the terminal
16         ; warn, error, and fatal are written to stderr, info and trace to stdout
17         console   = true
18 
19         ; File to output to, no output to file if not given
20         file      = log/logger_name.log
21 
22         ; Whether to propagate the options down in the hierachy
23         propagate = false
24 
25         ; The verbosity level, corresponse to the tango logger levels
26         level     = info
27 
28         ; Is this logger additive? That is, should we walk ancestors
29         ; looking for more appenders?
30         additive  = true
31 
32     Note that `LOG.Root` will be treated specially: it will configure the
33     'root' logger, which is the parent of all loggers.
34     `Root` is case insensitive, so `LOG.root` or `LOG.ROOT` will work as well.
35 
36     See the class Config for further options and documentation.
37 
38     There are global logger configuration options as well:
39 
40         ; Global options are in the section [LOG]
41         [LOG]
42 
43         ; Buffer size for output
44         buffer_size = 2048
45 
46     See the class MetaConfig for further options and documentation.
47 
48     Upon calling the configureLoggers function, logger related configuration
49     will be read and the according loggers configured accordingly.
50 
51     Usage Example (you probably will only need to do this):
52 
53     ----
54         import Log = ocean.util.log.Config;
55         // ...
56         Log.configureLoggers(Config().iterateCategory!(Log.Config)("LOG"),
57                              Config().get!(Log.MetaConfig)("LOG"));
58     ----
59 
60     Copyright:
61         Copyright (c) 2009-2016 dunnhumby Germany GmbH.
62         All rights reserved.
63 
64     License:
65         Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
66         Alternatively, this file may be distributed under the terms of the Tango
67         3-Clause BSD License (see LICENSE_BSD.txt for details).
68 
69 *******************************************************************************/
70 
71 module ocean.util.log.Config;
72 
73 
74 import ocean.meta.types.Qualifiers;
75 
76 import ocean.io.Stdout;
77 import ocean.core.Array : insertShift, removePrefix, removeSuffix, sort;
78 import ocean.util.config.ConfigFiller;
79 import ocean.util.config.ConfigParser;
80 import ocean.util.log.AppendFile;
81 import ocean.util.log.AppendSysLog;
82 import ocean.text.util.StringSearch;
83 
84 import ocean.util.log.Logger;
85 import ocean.util.log.InsertConsole;
86 import ocean.util.log.Appender;
87 import ocean.util.log.AppendStderrStdout;
88 import ocean.util.log.Event;
89 import ocean.util.log.ILogger;
90 
91 // Log layouts
92 import ocean.util.log.layout.LayoutMessageOnly;
93 import ocean.util.log.layout.LayoutStatsLog;
94 import ocean.util.log.layout.LayoutSimple;
95 import ocean.util.log.LayoutDate;
96 
97 import core.sys.posix.strings;
98 
99 
100 /*******************************************************************************
101 
102     Configuration class for loggers
103 
104 *******************************************************************************/
105 
106 class Config
107 {
108     /***************************************************************************
109 
110         Level of the logger
111 
112     ***************************************************************************/
113 
114     public cstring level;
115 
116     /***************************************************************************
117 
118         Whether to use console output or not
119 
120     ***************************************************************************/
121 
122     public SetInfo!(bool) console;
123 
124     /***************************************************************************
125 
126         Whether to use syslog output or not
127 
128     ***************************************************************************/
129 
130     public SetInfo!(bool) syslog;
131 
132     /***************************************************************************
133 
134         Layout to use for console output
135 
136     ***************************************************************************/
137 
138     public istring console_layout = "simple";
139 
140     /***************************************************************************
141 
142         Whether to use file output and if, which file path
143 
144     ***************************************************************************/
145 
146     public SetInfo!(istring) file;
147 
148     /***************************************************************************
149 
150         Layout to use for file output
151 
152     ***************************************************************************/
153 
154     public istring file_layout = "date";
155 
156     /***************************************************************************
157 
158         Whether to propagate that level to the children
159 
160     ***************************************************************************/
161 
162     public bool propagate;
163 
164     /***************************************************************************
165 
166         Whether this logger should be additive or not
167 
168     ***************************************************************************/
169 
170     bool additive;
171 
172     /***************************************************************************
173 
174         Whether this logger should be part of the global logging stats
175         mechanism
176 
177     ***************************************************************************/
178 
179     bool collect_stats = true;
180 
181     /***************************************************************************
182 
183         Buffer size of the buffer output, overwrites the global setting
184         given in MetaConfig
185 
186     ***************************************************************************/
187 
188     public size_t buffer_size = 0;
189 }
190 
191 /*******************************************************************************
192 
193     Configuration class for logging
194 
195 *******************************************************************************/
196 
197 class MetaConfig
198 {
199     /***************************************************************************
200 
201         Tango buffer size, if 0, internal stack based buffer of 2048 will be
202         used.
203 
204     ***************************************************************************/
205 
206     size_t buffer_size   = 0;
207 }
208 
209 /*******************************************************************************
210 
211     Convenience alias for iterating over Config classes
212 
213 *******************************************************************************/
214 
215 alias ocean.util.config.ConfigFiller.ConfigIterator!(Config, ConfigParser) ConfigIterator;
216 
217 /*******************************************************************************
218 
219     Convenience alias for layouts
220 
221 *******************************************************************************/
222 
223 alias Appender.Layout Layout;
224 
225 /*******************************************************************************
226 
227     Gets a new layout instance, based on the given name.
228 
229     Params:
230         layout_str = name of the desired layout
231 
232     Returns:
233         an instance of a suitable layout based on the input string
234 
235     Throws:
236         if `layout_str` cannot be matched to any layout
237 
238 *******************************************************************************/
239 
240 public Layout newLayout ( cstring layout_str )
241 {
242     mstring tweaked_str = layout_str.dup;
243 
244     StringSearch!() s;
245 
246     s.strToLower(tweaked_str);
247 
248     tweaked_str = removePrefix(tweaked_str, "layout");
249 
250     tweaked_str = removeSuffix(tweaked_str, "layout");
251 
252     switch ( tweaked_str )
253     {
254         case "messageonly":
255             return new LayoutMessageOnly;
256 
257         case "stats":
258         case "statslog":
259             return new LayoutStatsLog;
260 
261         case "simple":
262             return new LayoutSimple;
263 
264         case "date":
265             return new LayoutDate;
266 
267         default:
268             // Has to be 2 statements because `istring ~ cstring`
269             // yields `cstring` instead of `istring`.
270             istring msg = "Invalid layout requested : ";
271             msg ~= layout_str;
272             throw new Exception(msg, __FILE__, __LINE__);
273     }
274 }
275 
276 ///
277 unittest
278 {
279     // In a real app those would be full-fledged implementation
280     alias LayoutSimple AquaticLayout;
281     alias LayoutSimple SubmarineLayout;
282 
283     void myConfigureLoggers (
284         ConfigIterator config,
285         MetaConfig m_config,
286         scope Appender delegate ( istring file, Layout layout ) file_appender,
287         bool use_insert_appender = false)
288     {
289         Layout makeLayout (cstring name)
290         {
291             if (name == "aquatic")
292                 return new AquaticLayout;
293             if (name == "submarine")
294                 return new SubmarineLayout;
295             return ocean.util.log.Config.newLayout(name);
296         }
297         ocean.util.log.Config.configureNewLoggers(config, m_config,
298             file_appender, &makeLayout, use_insert_appender);
299     }
300 }
301 
302 /*******************************************************************************
303 
304     Sets up logging configuration for `ocean.util.log.Logger`
305 
306     Calls the provided `file_appender` delegate once per log being configured and
307     passes the returned appender to the log's add() method.
308 
309     Params:
310         config   = an instance of an class iterator for Config
311         m_config = an instance of the MetaConfig class
312         file_appender = delegate which returns appender instances to write to
313                         a file
314         use_insert_appender = true if the InsertConsole appender should be used
315                               (needed when using the AppStatus module)
316 
317 *******************************************************************************/
318 
319 public void configureNewLoggers (
320     ConfigIterator config, MetaConfig m_config,
321     scope Appender delegate ( istring file, Layout layout ) file_appender,
322     bool use_insert_appender = false)
323 {
324     configureNewLoggers(config, m_config, file_appender,
325         (cstring v) { return newLayout(v); }, use_insert_appender);
326 }
327 
328 
329 /*******************************************************************************
330 
331     Sets up logging configuration for `ocean.util.log.Logger`
332 
333     Calls the provided `file_appender` delegate once per log being configured
334     and passes the returned `Appender` to the `Logger.add` method.
335 
336     This is an extra overload because using a delegate literal as a parameter's
337     default argument causes linker error in D1.
338 
339     Params:
340         config   = an instance of an class iterator for Config
341         m_config = an instance of the MetaConfig class
342         file_appender = delegate which returns appender instances to write to
343                         a file
344         makeLayout = A delegate that returns a `Layout` instance from
345                      a name, or throws on error.
346                      By default, wraps `ocean.util.log.Config.newLayout`
347         use_insert_appender = true if the InsertConsole appender should be used
348                               (needed when using the AppStatus module)
349 
350 *******************************************************************************/
351 
352 public void configureNewLoggers (
353     ConfigIterator config, MetaConfig m_config,
354     scope Appender delegate ( istring file, Layout layout ) file_appender,
355     scope Layout delegate (cstring) makeLayout, bool use_insert_appender = false)
356 {
357     // DMD1 cannot infer the common type between both return, we have to work
358     // around it...
359     static Appender console_appender_fn (bool insert_appender, Layout layout)
360     {
361         if (insert_appender)
362             return new InsertConsole(layout);
363         else
364             return new AppendStderrStdout(ILogger.Level.Warn, layout);
365     }
366 
367     // The type needs to be spelt out loud because DMD2 is clever enough
368     // to see it's a function and not a delegate, but not clever enough
369     // to understand we want a delegate in the end...
370     scope Appender delegate(Layout) appender_dg = (Layout l)
371                        { return console_appender_fn(use_insert_appender, l); };
372 
373     configureLoggers(config, m_config, file_appender, appender_dg, makeLayout);
374 }
375 
376 
377 /*******************************************************************************
378 
379     Sets up logging configuration. Calls the provided file_appender delegate once
380     per log being configured and passes the returned appender to the log's add()
381     method.
382 
383     Params:
384         config   = an instance of an class iterator for Config
385         m_config = an instance of the MetaConfig class
386         file_appender = delegate which returns appender instances to write to
387                         a file
388         console_appender = Delegate which returns an Appender suitable to use
389                            as console appender. Might not be called if console
390                            writing is disabled.
391         makeLayout      = A delegate that returns a `Layout` instance from
392                           a name, or throws on error.
393 
394 *******************************************************************************/
395 
396 private void configureLoggers
397     (ConfigIterator config, MetaConfig m_config,
398      scope Appender delegate (istring file, Layout layout) file_appender,
399      scope Appender delegate (Layout) console_appender,
400      scope Layout delegate (cstring) makeLayout)
401 {
402     // It is important to ensure that parent loggers are configured before child
403     // loggers. This is because parent loggers will override the settings of
404     // child loggers when the 'propagate' property is enabled, thus preventing
405     // child loggers from customizing their properties via the config file(s).
406     // However, since the parsed configuration is stored in an AA, ordered
407     // iteration of the logging config is not directly possible. For this
408     // reason, the names of all loggers present in the configuration are first
409     // sorted, and the loggers are then configured based on the sorted list. The
410     // sorting is performed in increasing order of the lengths of the logger
411     // names so that parent loggers appear before child loggers.
412 
413     istring[] logger_names;
414     istring   root_name;
415 
416     foreach (name; config)
417     {
418         if (name.length == "root".length
419             && strncasecmp(name.ptr, "root".ptr, "root".length) == 0)
420             root_name = name;
421         else
422             logger_names ~= name;
423     }
424 
425     sort(logger_names);
426     // As 'Root' is the parent logger of all loggers, we need to special-case it
427     // and put it at the beginning of the list
428     if (root_name.length)
429     {
430         logger_names.insertShift(0);
431         logger_names[0] = root_name;
432     }
433 
434     Config settings;
435 
436     foreach (name; logger_names)
437     {
438         bool console_enabled = false;
439         bool syslog_enabled = false;
440 
441         config.fill(name, settings);
442 
443         if (root_name == name)
444         {
445             name = null;
446             console_enabled = settings.console(true);
447             syslog_enabled = settings.syslog(false);
448         }
449         else
450         {
451             console_enabled = settings.console();
452             syslog_enabled = settings.syslog();
453         }
454         configureLogger(name.length ? Log.lookup(name) : Log.root,
455              settings, name,
456              file_appender, console_appender,
457              console_enabled, syslog_enabled, m_config.buffer_size,
458              makeLayout);
459     }
460 }
461 
462 
463 /*******************************************************************************
464 
465     Sets up logging configuration. Calls the provided file_appender delegate once
466     per log being configured and passes the returned appender to the log's add()
467     method.
468 
469     Params:
470         log      = Logger to configure
471         settings = an instance of an class iterator for Config
472         name     = name of this logger
473         file_appender = delegate which returns appender instances to write to
474                         a file
475         console_appender = Delegate which returns an Appender suitable to use
476                            as console appender. Might not be called if console
477                            writing is disabled.
478         console_enabled = `true` if a console appender should be added (by
479                           calling `console_enabled`).
480         syslog_enabled  = `true` if a syslog appender should be added.
481         makeLayout      = A delegate that returns a `Layout` instance from
482                           a name, or throws on error.
483                           By default, wraps `ocean.util.log.Config.newLayout`.
484 
485 *******************************************************************************/
486 
487 public void configureLogger
488     (Logger log, Config settings, istring name,
489      scope Appender delegate ( istring file, Layout layout ) file_appender,
490      scope Appender delegate (Layout) console_appender,
491      bool console_enabled, bool syslog_enabled, size_t buffer_size,
492      scope Layout delegate (cstring) makeLayout = (cstring v) { return newLayout(v); })
493 {
494     if (settings.buffer_size)
495         buffer_size = settings.buffer_size;
496 
497     if (buffer_size > 0)
498         log.buffer(new mstring(buffer_size));
499 
500     log.clear();
501 
502     // if console/file/syslog is specifically set, don't inherit other
503     // appenders (unless we have been specifically asked to be additive)
504     log.additive = settings.additive ||
505         !(settings.console.set || settings.file.set || settings.syslog.set);
506 
507     if (settings.file.set)
508     {
509         log.add(file_appender(settings.file(), makeLayout(settings.file_layout)));
510     }
511 
512     if (syslog_enabled)
513         log.add(new AppendSysLog);
514 
515     if (console_enabled)
516     {
517         log.add(console_appender(makeLayout(settings.console_layout)));
518     }
519 
520     log.collectStats(settings.collect_stats, settings.propagate);
521     setupLoggerLevel(log, name, settings);
522 }
523 
524 version (unittest)
525 {
526     import ocean.core.Array : copy;
527     import ocean.core.Test : test;
528 }
529 
530 // When the 'propagate' property of a logger is set, its settings get propagated
531 // to all child loggers. However, every child logger should be able to define
532 // its own settings overriding any automatically propagated setting from the
533 // parent logger. Since loggers are stored in an AA, the order in which they are
534 // configured is undeterministic. This could potentially result in parent
535 // loggers being configured after child loggers and thus overriding any
536 // specifically defined setting in the child logger. To avoid this from
537 // happening, parent loggers are deliberately configured before child loggers.
538 // This unit test block confirms that this strict configuration order is
539 // enforced, and parent loggers never override the settings of child loggers.
540 unittest
541 {
542     class TempAppender : Appender
543     {
544         private mstring latest_log_msg;
545         private Mask mask_;
546 
547         final override public void append (LogEvent event)
548         {
549             copy(this.latest_log_msg, event.toString());
550         }
551 
552         final override public Mask mask () { return this.mask_; }
553         final override public istring name () { return null; }
554     }
555 
556     auto config_parser = new ConfigParser();
557 
558     auto config_str =
559 `
560 [LOG.A]
561 level = trace
562 propagate = true
563 file = dummy
564 
565 [LOG.A.B]
566 level = info
567 propagate = true
568 file = dummy
569 
570 [LOG.A.B.C]
571 level = warn
572 propagate = true
573 file = dummy
574 
575 [LOG.A.B.C.D]
576 level = error
577 propagate = true
578 file = dummy
579 
580 [LOG.Root]
581 level = trace
582 propagate = true
583 file = dummy
584 `;
585 
586     auto temp_appender = new TempAppender;
587 
588     Appender appender(istring, Layout)
589     {
590         return temp_appender;
591     }
592 
593     config_parser.parseString(config_str);
594 
595     auto log_config = iterate!(Config)("LOG", config_parser);
596     auto dummy_meta_config = new MetaConfig();
597 
598     configureNewLoggers(log_config, dummy_meta_config, &appender);
599 
600     auto log_D = Log.lookup("A.B.C.D");
601 
602     log_D.trace("trace log (shouldn't be sent to appender)");
603     test!("==")(temp_appender.latest_log_msg, "");
604 
605     log_D.info("info log (shouldn't be sent to appender)");
606     test!("==")(temp_appender.latest_log_msg, "");
607 
608     log_D.warn("warn log (shouldn't be sent to appender)");
609     test!("==")(temp_appender.latest_log_msg, "");
610 
611     log_D.error("error log");
612     test!("==")(temp_appender.latest_log_msg, "error log");
613 
614     log_D.fatal("fatal log");
615     test!("==")(temp_appender.latest_log_msg, "fatal log");
616 }
617 
618 /*******************************************************************************
619 
620     Sets up the level configuration of a logger.
621 
622     Params:
623         log = logger to configure
624         name = name of logger
625         config = config settings for the logger
626 
627     Throws:
628         Exception if the config for a logger specifies an invalid level
629 
630 *******************************************************************************/
631 
632 public void setupLoggerLevel ( Logger log, istring name, Config config )
633 {
634     with (config) if (level.length > 0)
635     {
636         StringSearch!() s;
637 
638         level = s.strEnsureLower(level);
639 
640         switch (level)
641         {
642             case "trace":
643             case "debug":
644                 log.level(ILogger.Level.Trace, propagate);
645                 break;
646 
647             case "info":
648                 log.level(ILogger.Level.Info, propagate);
649                 break;
650 
651             case "warn":
652                 log.level(ILogger.Level.Warn, propagate);
653                 break;
654 
655             case "error":
656                 log.level(ILogger.Level.Error, propagate);
657                 break;
658 
659             case "fatal":
660                 log.level(ILogger.Level.Fatal, propagate);
661                 break;
662 
663             case "none":
664             case "off":
665             case "disabled":
666                 log.level(ILogger.Level.None, propagate);
667                 break;
668 
669             default:
670                 throw new Exception(cast(istring) ("Invalid log level '"
671                                                    ~ level ~ "' " ~
672                                                    "requested for logger '"
673                                                    ~ name ~ "'"));
674         }
675     }
676 }