1 module logger.extendedfilelogger;
2 
3 import std.experimental.logger;
4 import std.stdio;
5 
6 import std.concurrency : Tid;
7 import std.datetime.systime : SysTime;
8 import std.range : isOutputRange, isInputRange;
9 
10 private enum PatternToken = '%';
11 
12 /// A simple pattern -  020-06-08T17:50:25.673 info     :
13 enum string simplePattern = "%d %-8p: ";
14 
15 /// Interface of all log patterns
16 interface ILogPattern
17 {
18     /**
19      * Writes on a outputRange of chars, the logging pattern
20      * Params:
21      *  outputFile = output File where where tow rite the logging request.
22      *  file = file name where the logging request was issued.
23      *  line = line number from where the logging request was issued.
24      *  funcName = the function or method name where the logging request was issued.
25      *  prettyFuncName = the function or method prettyfied name where the logging request was issued.
26      *  moduleName = the module name where the logging request was issued.
27      *  logLevel = the actual log level of the accepter logging request.
28      *  threadId = the thread id where the logging request was issued.
29      *  timestamp = the timestamp of the logging request.
30      *  timestamp = the timestamp of when the logger was created.
31      */
32     void applyPattern (File outputFile, string file, int line, string funcName,
33         string prettyFuncName, string moduleName, LogLevel logLevel, Tid threadId, SysTime timestamp,
34         SysTime startTimeStamp ) @trusted;
35 
36 }
37 
38 /// Simple loging pattern that outputs the same pattern that classic FileLogger
39 class SimpleLogPattern : ILogPattern
40 {
41     /**
42      * Writes on a outputRange of chars, the logging pattern
43      * Params:
44      *  outputFile = output File where where tow rite the logging request.
45      *  file = file name where the logging request was issued.
46      *  line = line number from where the logging request was issued.
47      *  funcName = the function or method name where the logging request was issued.
48      *  prettyFuncName = the function or method prettyfied name where the logging request was issued.
49      *  moduleName = the module name where the logging request was issued.
50      *  logLevel = the actual log level of the accepter logging request.
51      *  threadId = the thread id where the logging request was issued.
52      *  timestamp = the timestamp of the logging request.
53      *  timestamp = the timestamp of when the logger was created.
54      */
55     void applyPattern (File outputFile, string file, int line, string funcName,
56         string prettyFuncName, string moduleName, LogLevel logLevel, Tid threadId, SysTime timestamp,
57         SysTime startTimeStamp ) @trusted
58     {
59         auto lt = outputFile.lockingTextWriter();
60         import std.experimental.logger.core : systimeToISOString;
61         systimeToISOString(lt, timestamp);
62 
63         import std.conv : to;
64         import std.format : formattedWrite;
65         formattedWrite(lt, " [%s] %s:%u:%s ", logLevel.to!string, file, line, funcName);
66     }
67 }
68 
69 /**
70  * Configurable logging pattern that parses a string pattern
71  *
72  * The pattern it's based of log4j pattern layout format:
73  * | Conversion Character | Effect |
74  * | -------------------- | -----: |
75  * | m | Module name |
76  * | d | Used to output the date of the logging event. Actually only outputs on ISO format |
77  * | F | Used to output the file name where the logging request was issued. |
78  * | L | Used to output the line number from where the logging request was issued. |
79  * | M | Used to output the function or method name where the logging request was issued. |
80  * | p | Used to output the priority of the logging event. |
81  * | r | Used to output number of milliseconds elapsed from the construction of this logger until the logging event. |
82  * | t | Used to output the thread that generated the logging event. |
83  */
84 class ConfigurableLogPattern : ILogPattern
85 {
86     private string logPattern;
87 
88     /**
89      * Builds a configurable log pattern
90      * Params:
91      *  logPattern = String pattern to apply. By default uses `simplePattern`
92      */
93     this(string logPattern = simplePattern) @safe
94     {
95         this.logPattern = logPattern;
96     }
97 
98     /**
99      * Writes on a outputRange of chars, the logging pattern
100      * Params:
101      *  outputFile = output File where where tow rite the logging request.
102      *  file = file name where the logging request was issued.
103      *  line = line number from where the logging request was issued.
104      *  funcName = the function or method name where the logging request was issued.
105      *  prettyFuncName = the function or method prettyfied name where the logging request was issued.
106      *  moduleName = the module name where the logging request was issued.
107      *  logLevel = the actual log level of the accepter logging request.
108      *  threadId = the thread id where the logging request was issued.
109      *  timestamp = the timestamp of the logging request.
110      *  timestamp = the timestamp of when the logger was created.
111      */
112     void applyPattern (File outputFile, string file, int line, string funcName,
113         string prettyFuncName, string moduleName, LogLevel logLevel, Tid threadId, SysTime timestamp,
114         SysTime startTimeStamp ) @trusted
115     {
116         auto lt = outputFile.lockingTextWriter();
117 
118         import std.uni : byCodePoint;
119         this.parsePattern(lt, this.logPattern.byCodePoint, file, line, funcName, prettyFuncName, moduleName,
120             logLevel, threadId, timestamp, startTimeStamp);
121     }
122 
123     private void parsePattern(OutputRange, InputRange)(OutputRange outputRange, InputRange patternRange, string file,
124         int line, string funcName, string prettyFuncName, string moduleName, LogLevel logLevel, Tid threadId,
125         SysTime timestamp, SysTime startTimeStamp ) @trusted
126     if (isOutputRange!(OutputRange, char) && isInputRange!(InputRange))
127     {
128         // Outputs anything before finding the '%' format special character
129         while(!patternRange.empty && patternRange.front != PatternToken) {
130             outputRange.put(patternRange.front);
131             patternRange.popFront;
132         }
133         if (patternRange.empty) {
134             return;
135         }
136         patternRange.popFront; // And consumes the '%'
137 
138         auto modifier = this.formatModifier!(InputRange)(patternRange);
139 
140         // Value stores the output string
141         import std.conv : to;
142         switch (patternRange.front) {
143             case '%': // The sequence %% outputs a single percent sign.
144                 outputRange.put('%');
145                 break;
146 
147             case 'm': // Module name
148                 outputRange.writeWithModifier(moduleName, modifier[0], modifier[1]);
149                 break;
150 
151             case 'd': // Date on ISO format
152             outputRange.systimeToISOString(timestamp);
153             break;
154 
155             case 'p': // priority of log level
156             outputRange.put(this.logLevelPrefix(logLevel));
157             outputRange.writeWithModifier(logLevel.to!string, modifier[0], modifier[1]);
158             outputRange.put(this.logLevelPostfix(logLevel));
159             break;
160 
161             case 'F': // Filename
162             outputRange.writeWithModifier(file, modifier[0], modifier[1]);
163             break;
164 
165             case 'L': // Lines
166             outputRange.writeWithModifier(line.to!string, modifier[0], modifier[1]);
167             break;
168 
169             case 'M': // Function/Method where the logging was issued
170             outputRange.writeWithModifier(funcName, modifier[0], modifier[1]);
171             break;
172 
173             case 't': // name of the thread that generated the logging event.
174             outputRange.writeWithModifier(threadId.to!string, modifier[0], modifier[1]);
175             break;
176 
177             case 'r': // number of milliseconds elapsed from the construction of this logger until the logging event.
178             import std.datetime : Clock;
179             const delta = (Clock.currTime() - startTimeStamp).total!"msecs";
180             outputRange.writeWithModifier(delta.to!string, modifier[0], modifier[1]);
181             break;
182 
183             default: // unsuported formats are simply ignored
184 
185         }
186         patternRange.popFront;
187 
188         // Iterate for the next pattern token
189         if (!patternRange.empty) {
190             this.parsePattern(outputRange, patternRange, file, line, funcName, prettyFuncName, moduleName,
191                 logLevel, threadId, timestamp, startTimeStamp);
192         }
193     }
194 
195     private auto formatModifier(InputRange)(ref InputRange patternRange) @safe pure
196         if (isInputRange!(InputRange))
197     {
198         import std.ascii : isDigit;
199         import std.typecons : Tuple, tuple;
200         Tuple!(int, size_t) modifier = tuple(0, size_t.max);
201 
202         // Get the padding modifier
203         if (patternRange.front == '+' || patternRange.front == '-' || patternRange.front.isDigit) {
204             import std.conv : parse, ConvException;
205             try {
206                 modifier[0] = patternRange.parse!int;
207             } catch (ConvException ex) {
208                 // We ignore it silently
209             }
210         }
211         // Get the truncate modifier
212         if (patternRange.front == '.') {
213             patternRange.popFront; // Consume the '.' separator
214             if (patternRange.front.isDigit) {
215                 import std.conv : parse, ConvException;
216                 try {
217                     modifier[1] = patternRange.parse!size_t;
218                 } catch (ConvException ex) {
219                     // Silently we ignore it
220                 }
221             }
222         }
223         return modifier;
224     }
225 
226     /// string preceding loglevel. Usefull to colorize it, with ANSI
227     protected string logLevelPrefix(const LogLevel logLevel) {
228         return "";
229     }
230 
231     /// string following loglevel. Usefull to colorize it, with ANSI
232     protected string logLevelPostfix(const LogLevel logLevel) {
233         return "";
234     }
235 }
236 
237 import std.range : isOutputRange;
238 
239 // writes to the output the value with the padding and truncation
240 private void writeWithModifier(OutputRange)(OutputRange outputRange, string value, int padding, size_t truncate) @safe
241     if (isOutputRange!(OutputRange, char))
242 {
243     if (padding > 0) {
244         // left padd
245         padding -= value.length;
246         while(padding > 0) {
247             outputRange.put(' ');
248             padding--;
249         }
250     }
251     import std.format : formattedWrite;
252     if (truncate != size_t.max && truncate < value.length) {
253         outputRange.formattedWrite("%s", value[0..truncate]);
254     } else {
255         outputRange.formattedWrite("%s", value);
256     }
257     if (padding < 0) {
258         // right padd
259         padding += value.length;
260         while(padding < 0) {
261             outputRange.put(' ');
262             padding++;
263         }
264     }
265 }
266 
267 /**
268  * Extends FileLogger to support configurable pattern
269  */
270 class ExtendedFileLogger : FileLogger
271 {
272     import std.concurrency : Tid;
273     import std.datetime.systime : SysTime;
274     import std.format : formattedWrite;
275 
276     /** A constructor for the `ExtendedFileLogger` Logger.
277     Params:
278       fn = The filename of the output file of the `ExtendedFileLogger`. If that
279       file can not be opened for writting an exception will be thrown.
280       lv = The `LogLevel` for the `ExtendedFileLogger`. By default the
281       logPattern = An implementation of the `ILogPattern`. By default uses SimpleLogPattern.
282     Example:
283     -------------
284     auto l1 = new ExtendedFileLogger("logFile");
285     auto l2 = new ExtendedFileLogger("logFile", LogLevel.fatal);
286     auto l3 = new ExtendedFileLogger("logFile", LogLevel.fatal, new ConfigurableLogPattern("%m %-5p %d - "));
287     -------------
288     */
289     this(const string fn, const LogLevel lv = LogLevel.all, ILogPattern logPattern = new SimpleLogPattern()) @safe
290     {
291          this(fn, lv, logPattern, CreateFolder.yes);
292     }
293 
294     /** A constructor for the `ExtendedFileLogger` Logger that takes a reference to a `File`.
295     The `File` passed must be open for all the log call to the `ExtendedFileLogger`. If the `File`
296     gets closed, using the `ExtendedFileLogger` for logging will result in undefined behaviour.
297     Params:
298       fn = The file used for logging.
299       lv = The `LogLevel` for the `ExtendedFileLogger`. By default the `LogLevel` for
300       `ExtendedFileLogger` is `LogLevel.all`.
301       createFileNameFolder = if yes and fn contains a folder name, this folder will be created.
302       logPattern = An implementation of the `ILogPattern`.
303     Example:
304     -------------
305     auto file = File("logFile.log", "w");
306     auto l1 = new ExtendedFileLogger(file);
307     auto l2 = new ExtendedFileLogger(file, LogLevel.fatal);
308     -------------
309     */
310     this(const string fn, const LogLevel lv, ILogPattern logPattern, CreateFolder createFileNameFolder) @safe
311     {
312         import std.datetime : Clock;
313         super(fn, lv, createFileNameFolder);
314         this.logPattern = logPattern;
315         this.startTimeStamp =  Clock.currTime();
316     }
317 
318     /** A constructor for the `ExtendedFileLogger` Logger that takes a reference to a `File`.
319     The `File` passed must be open for all the log call to the `ExtendedFileLogger`. If
320     the `File` gets closed, using the `ExtendedFileLogger` for logging will result in
321     undefined behaviour.
322     Params:
323       file = The file used for logging.
324       lv = The `LogLevel` for the `ExtendedFileLogger`. By default the `LogLevel` for
325       `ExtendedFileLogger` is `LogLevel.all`.
326       logPattern = An implementation of the `ILogPattern`. By default uses SimpleLogPattern.
327     Example:
328     -------------
329     auto file = File("logFile.log", "w");
330     auto l1 = new ExtendedFileLogger(file);
331     auto l2 = new ExtendedFileLogger(file, LogLevel.fatal);
332     -------------
333     */
334     this(File file, const LogLevel lv = LogLevel.all, ILogPattern logPattern = new SimpleLogPattern()) @safe
335     {
336         import std.datetime : Clock;
337         super(file, lv);
338         this.logPattern = logPattern;
339         this.startTimeStamp =  Clock.currTime();
340     }
341 
342     /// This method overrides the base class method in order to call the log pattern
343     override protected void beginLogMsg(string file, int line, string funcName,
344         string prettyFuncName, string moduleName, LogLevel logLevel,
345         Tid threadId, SysTime timestamp, Logger logger)
346         @safe
347     {
348         import std.string : lastIndexOf;
349         ptrdiff_t fnIdx = file.lastIndexOf('/') + 1;
350         ptrdiff_t funIdx = funcName.lastIndexOf('.') + 1;
351 
352         this.logPattern.applyPattern(this.file, file[fnIdx..$], line, funcName[funIdx..$], prettyFuncName, moduleName,
353             logLevel, threadId, timestamp, this.startTimeStamp);
354     }
355 
356     protected ILogPattern logPattern;
357     protected SysTime startTimeStamp;
358 }
359