1 /** 
2  * Module containing the default logger factory and associated components.
3  */
4 module slf4d.default_provider.factory;
5 
6 import slf4d;
7 import std.regex;
8 import core.sync.rwmutex;
9 
10 /** 
11  * A basic LoggerFactory implementation that just creates a `Logger` with a
12  * handler and pre-set logging level. It also includes methods for defining
13  * custom logging levels for individual module patterns, so that you can, for
14  * example, only show debug messages from a single module.
15  */
16 shared class DefaultLoggerFactory : LoggerFactory {
17     private LogHandler handler;
18     private Level rootLoggingLevel;
19     private ModuleLoggingLevelMapping[] moduleMappings;
20     private ReadWriteMutex mutex;
21 
22     /** 
23      * Constructs the factory with the given handler, and optionally a root
24      * logging level.
25      * Params:
26      *   handler = The handler that will handle all log messages.
27      *   rootLoggingLevel = The root logging level, which is the default level
28      *   assigned to all Loggers produced by this factory, unless a module
29      *   specific level is set.
30      */
31     public this(shared LogHandler handler, Level rootLoggingLevel = Levels.INFO) {
32         this.handler = handler;
33         this.rootLoggingLevel = rootLoggingLevel;
34         this.mutex = new shared ReadWriteMutex();
35     }
36 
37     /** 
38      * Sets the root logging level for this factory.
39      * Params:
40      *   level = The root logging level.
41      */
42     public void setRootLevel(Level level) {
43         synchronized(this.mutex.writer) {
44             this.rootLoggingLevel = level;
45         }
46     }
47 
48     /** 
49      * Sets the logging level for a given module pattern regular expression.
50      * When this factory prepares a logger for a given module (or logger name
51      * if the developer has defined a custom name), it will give that logger a
52      * logging level according to the configured `rootLoggingLevel` of this
53      * factory. Then, if it finds a module pattern set using this function, it
54      * will use the declared level instead. Module patterns are searched in the
55      * order that they're defined.
56      * Params:
57      *   modulePattern = The module pattern to match against. This is compiled
58      *                   as a regular expression.
59      *   level = The logging level to apply to Loggers whose name matches the
60      *           given module pattern.
61      */
62     public void setModuleLevel(string modulePattern, Level level) {
63         synchronized(this.mutex.writer) {
64             auto safeUnsharedMappings = cast(ModuleLoggingLevelMapping[]*) &this.moduleMappings;
65             *safeUnsharedMappings ~= ModuleLoggingLevelMapping(
66                 regex(modulePattern),
67                 level
68             );
69         }
70     }
71 
72     /** 
73      * Sets the logging level for a given module prefix. This is a convenience
74      * wrapper around `setModuleLevel`, which simply prepares a regular
75      * expression that matches the beginning of a logger's module (or name)
76      * with the given `modulePrefix`.
77      * Params:
78      *   modulePrefix = The module's prefix.
79      *   level = The logging level to apply to Loggers whose name matches the
80      *           given module prefix.
81      */
82     public void setModuleLevelPrefix(string modulePrefix, Level level) {
83         string pattern = "^" ~ replaceAll(modulePrefix, regex("\\."), "\\.") ~ ".*";
84         this.setModuleLevel(pattern, level);
85     }
86 
87     /** 
88      * Gets a Logger. The Logger's level is set according to the root logging
89      * level, unless there exists a module-specific level that was set via
90      * `setModuleLevel`.
91      * Params:
92      *   name = The logger's name, which defaults to the current module name.
93      * Returns: The Logger.
94      */
95     public Logger getLogger(string name = __MODULE__) shared {
96         import std.algorithm : startsWith;
97         synchronized(this.mutex.reader) {
98             Level level = this.rootLoggingLevel;
99             auto safeUnsharedMappings = cast(ModuleLoggingLevelMapping[]*) &this.moduleMappings;
100             foreach (mapping; *safeUnsharedMappings) {
101                 if (matchFirst(name, mapping.modulePattern)) {
102                     level = mapping.level;
103                 }
104             }
105             return Logger(this.handler, level, name);
106         }
107     }
108 }
109 
110 /** 
111  * A simple mapping struct that maps a module pattern string to a particular
112  * logging level, used by the DefaultLoggerFactory to configure module-specific
113  * logging levels.
114  */
115 package struct ModuleLoggingLevelMapping {
116     private Regex!char modulePattern;
117     private immutable Level level;
118 }
119 
120 unittest {
121     import slf4d.handler;
122     auto handler = new CachingLogHandler();
123 
124     auto f1 = new DefaultLoggerFactory(handler, Levels.INFO);
125     Logger log1 = f1.getLogger();
126     log1.debug_("Testing");
127     assert(handler.messageCount() == 0);
128     log1.warn("Testing");
129     assert(handler.messageCount() == 1);
130     handler.reset();
131 
132     f1.setRootLevel(Levels.TRACE);
133     Logger log2 = f1.getLogger();
134     log2.debug_("Testing");
135     assert(handler.messageCount() == 1);
136     handler.reset();
137 
138     // Test setModuleLevel regular expression matching.
139     auto f2 = new DefaultLoggerFactory(handler, Levels.WARN);
140     f2.setModuleLevel("^my_module\\.a$", Levels.DEBUG);
141     f2.setModuleLevel("^my_module\\.b$", Levels.TRACE);
142     Logger log3 = f2.getLogger("my_module.a"); // First a logger that matches the first module level.
143     log3.info("Testing");
144     assert(handler.messageCount() == 1);
145     log3.debug_("Testing debug");
146     assert(handler.messageCount() == 2);
147     log3.trace("Testing trace. This should not be added to the messages.");
148     assert(handler.messageCount() == 2);
149     handler.reset();
150 
151     Logger log4 = f2.getLogger("my_module.b"); // Then a logger that matches the second module level.
152     log4.info("Testing");
153     assert(handler.messageCount() == 1);
154     log4.info("Testing trace");
155     assert(handler.messageCount() == 2);
156     handler.reset();
157 
158     Logger log5 = f2.getLogger("other_module"); // Finally, a logger that shouldn't match either, and uses root level.
159     log5.debug_("This should not appear.");
160     assert(handler.messageCount() == 0);
161     log5.trace("This should also not appear.");
162     assert(handler.messageCount() == 0);
163     log5.warn("This should appear!");
164     assert(handler.messageCount() == 1);
165     handler.reset();
166 
167     // Test setModuleLevelPrefix matching.
168     auto f3 = new DefaultLoggerFactory(handler, Levels.WARN);
169     f3.setModuleLevelPrefix("first_mod.second_mod", Levels.TRACE);
170     f3.setModuleLevelPrefix("first_mod.third_mod.", Levels.DEBUG);
171     Logger log6 = f3.getLogger("first_mod.second_mod"); // A logger that matches the first module level.
172     log6.trace("This should appear!");
173     assert(handler.messageCount() == 1);
174     handler.reset();
175 
176     Logger log7 = f3.getLogger("first_mod.second_mod.another"); // A logger that still matches the first module.
177     log7.trace("This should appear!");
178     assert(handler.messageCount() == 1);
179     handler.reset();
180 
181     Logger log8 = f3.getLogger("first_mod.third_mod.a"); // A logger that matches the second module.
182     log8.trace("This should not appear.");
183     assert(handler.messageCount() == 0);
184     log8.debug_("This should appear!");
185     assert(handler.messageCount() == 1);
186     handler.reset();
187 
188     Logger log9 = f3.getLogger("other_module"); // A logger that doesn't match any module levels.
189     log9.trace("This should not appear.");
190     log9.info("This should also not appear.");
191     assert(handler.messageCount() == 0);
192     log9.error("This should appear!");
193     assert(handler.messageCount() == 1);
194     handler.reset();
195 }