1 /**
2  * onyx-log: the generic, fast, multithreading logging library.
3  *
4  * Output Controllers implementation.
5  *
6  * Copyright: © 2015- Oleg Nykytenko
7  * License: MIT license. License terms written in "LICENSE.txt" file
8  * Authors: Oleg Nykytenko, oleg.nykytenko@gmail.com
9  */
10 
11 module onyx.core.controller;
12 
13 
14 @system:
15 package:
16 
17 import onyx.log;
18 import onyx.core.logger;
19 import onyx.bundle;
20 
21 
22 struct Controller
23 {
24     import std.stdio;
25     import std.file;
26 
27     /* Currently Logging file */
28     File activeFile;
29     /* Do file rolling */
30     Rollover rollover;
31     /* Name */
32     mixin(addVal!(immutable string, "name", "public"));
33 
34     /**
35      * Primary constructor
36      *
37      * Save config path and name
38      */
39     this(immutable Bundle bundle)
40     {
41         rollover = createRollover(bundle);
42 
43         _name = bundle.glKeys[0];
44 
45         createPath(rollover.activeFilePath());
46         activeFile = File(rollover.activeFilePath(), "a");
47     }
48 
49     /**
50      * Extract logger type from bundle
51      *
52      * Throws: BundleException, LogCreateException
53      */
54     Rollover createRollover(immutable Bundle bundle)
55     {
56         try
57         {
58             if (!bundle.isValuePresent(bundle.glKeys[0], "rolling"))
59             {
60                 return new Rollover(bundle);
61             }
62             string rollingType = bundle.value(bundle.glKeys[0], "rolling");
63             RolloverFactory f = cast(RolloverFactory)Object.factory("onyx.core.controller." ~ rollingType ~ "Factory");
64 
65             if (f is null)
66             {
67                 throw new  LogCreateException("Error create log rolling: " ~ rollingType  ~ "  is Illegal rolling type from config bundle.");
68             }
69             Rollover r = f.factory(bundle);
70             return r;
71         }
72         catch (BundleException e)
73         {
74             throw new BundleException("Error in Config bundle. [" ~ name ~ "]:" ~ e.msg);
75         }
76         catch (Exception e)
77         {
78             throw new LogCreateException("Error in creating rolling for logger: " ~ name ~ ": " ~ e.msg);
79         }
80     }
81 
82     /**
83      * Extract logger type from bundle
84      *
85      * Throws: $(D ErrnoException)
86      */
87     void saveMsg(string msg)
88     {
89         if (!activeFile.name.exists)
90         {
91             activeFile = File(rollover.activeFilePath(), "w");
92         }
93         else if (rollover.roll(msg))
94         {
95             activeFile.detach();
96             rollover.carry();
97             activeFile = File(rollover.activeFilePath(), "w");
98         }
99         else if (!activeFile.isOpen())
100         {
101             activeFile.open("a");
102         }
103         activeFile.writeln(msg);
104     }
105 
106     /**
107      * Flush log file
108      */
109     void flush()
110     {
111         activeFile.flush;
112     }
113 }
114 
115 
116 /**
117  * Create file
118  */
119 void createPath(string fileFullName)
120 {
121     import std.path:dirName;
122     import std.file:mkdirRecurse;
123     import std.file:exists;
124 
125     string dir = dirName(fileFullName);
126 
127     if ((dir.length != 0) && (!exists(dir)))
128     {
129         mkdirRecurse(dir);
130     }
131 }
132 
133 
134 
135 /**
136  * Rollover Creating interface
137  *
138  * Use by Controller for create new Rollover
139  *
140  * ====================================================================================
141  */
142 interface RolloverFactory
143 {
144     Rollover factory(immutable Bundle bundle);
145 }
146 
147 
148 /**
149  * Base rollover class
150  */
151 class Rollover
152 {
153     import std.path;
154     import std.string;
155     import std.typecons;
156 
157     /* Control of size and number of log files */
158     immutable Bundle bundle;
159     /* Path and file name template */
160     mixin(addVal!(immutable string, "path", "protected"));
161     /* Work diroctory */
162     mixin(addVal!(immutable string, "dir", "protected"));
163     /* Log file base name template */
164     mixin(addVal!(immutable string, "baseName", "protected"));
165     /* Log file extension */
166     mixin(addVal!(immutable string, "ext", "protected"));
167     /* Path to main log file */
168     mixin(addVar!(string, "activeFilePath", "protected", "protected"));
169 
170     /**
171      * Primary constructor
172      */
173     this(immutable Bundle bundle)
174     {
175         this.bundle = bundle;
176         _path = bundle.value(bundle.glKeys[0], "fileName");
177         auto fileInfo = parseConfigFilePath(path);
178         _dir = fileInfo[0];
179         _baseName = fileInfo[1];
180         _ext = fileInfo[2];
181         init();
182     }
183 
184     /**
185      * Rollover start init
186      */
187     void init()
188     {
189         activeFilePath = path;
190     }
191 
192     /**
193      * Parse configuration file path and base name and save to members
194      */
195     auto parseConfigFilePath(string rawConfigFile)
196     {
197         string configFile = buildNormalizedPath(rawConfigFile);
198 
199         immutable dir = configFile.dirName;
200         string fullBaseName = std.path.baseName(configFile);
201         auto ldotPos = fullBaseName.lastIndexOf(".");
202         immutable ext = (ldotPos > 0)?fullBaseName[ldotPos+1..$]:"log";
203         immutable baseName = (ldotPos > 0)?fullBaseName[0..ldotPos]:fullBaseName;
204 
205         return tuple(dir, baseName, ext);
206     }
207 
208     /**
209      * Do files rolling by default
210      */
211     bool roll(string msg)
212     {
213         return false;
214     }
215 
216     void carry(){}
217 }
218 
219 
220 /**
221  * Factory for SizeBasedRollover
222  *
223  * ====================================================================================
224  */
225 class SizeBasedRolloverFactory:RolloverFactory
226 {
227     override Rollover factory(immutable Bundle bundle)
228     {
229         return new SizeBasedRollover(bundle);
230     }
231 }
232 
233 
234 /**
235  * Control of size and number of log files
236  */
237 class SizeBasedRollover:Rollover
238 {
239     import std.file;
240     import std.regex;
241     import std.algorithm;
242     import std.array;
243 
244 
245     /* Max size of one file */
246     uint maxSize;
247     /* Max number of working files */
248     uint maxHistory;
249     /* Primary constructor */
250     this(immutable Bundle bundle)
251     {
252         super(bundle);
253         maxSize = extractSize(bundle.value(bundle.glKeys[0], "maxSize"));
254         maxHistory = bundle.value!int(bundle.glKeys[0], "maxHistory");
255     }
256 
257 
258     /**
259      * Extract number fron configuration data
260      *
261      * Throws: LogException
262      */
263     uint extractSize(string size)
264     {
265         import std.uni : toLower;
266         import std.uni : toUpper;
267         import std.conv;
268 
269         uint nsize = 0;
270         auto n = matchAll(size, regex(`\d*`));
271         if (!n.empty && (n.hit.length != 0))
272         {
273             nsize = to!int(n.hit);
274             auto m = matchAll(size, regex(`\D{1}`));
275             if (!m.empty && (m.hit.length != 0))
276             {
277                 switch(m.hit.toUpper)
278                 {
279                     case "K":
280                         nsize *= KB;
281                         break;
282                     case "M":
283                         nsize *= MB;
284                         break;
285                     case "G":
286                         nsize *= GB;
287                         break;
288                     case "T":
289                         nsize *= TB;
290                         break;
291                     case "P":
292                         nsize *= PB;
293                         break;
294                     default:
295                         throw new LogException("In Logger configuration uncorrect number: " ~ size);
296                 }
297             }
298         }
299         return nsize;
300     }
301 
302 
303     enum KB = 1024;
304     enum MB = KB*1024;
305     enum GB = MB*1024;
306     enum TB = GB*1024;
307     enum PB = TB*1024;
308 
309     /**
310      * Scan work directory
311      * save needed files to pool
312     	 */
313     string[] scanDir()
314     {
315         import std.algorithm.sorting:sort;
316         bool tc(string s)
317         {
318             static import std.path;
319             auto base = std.path.baseName(s);
320             auto m = matchAll(base, regex(baseName ~ `\d*\.` ~ ext));
321             if (m.empty || (m.hit != base))
322             {
323                 return false;
324             }
325             return true;
326         }
327 
328         return std.file.dirEntries(dir, SpanMode.shallow)
329             .filter!(a => a.isFile)
330             .map!(a => a.name)
331             .filter!(a => tc(a))
332             .array
333             .sort!("a < b")
334             .array;
335     }
336 
337     /**
338      * Do files rolling by size
339      */
340     override
341     bool roll(string msg)
342     {
343         auto filePool = scanDir();
344         if (filePool.length == 0)
345         {
346             return false;
347         }
348         if ((getSize(filePool[0]) + msg.length) >= maxSize)
349         {
350             //if ((filePool.front.getSize == 0) throw
351             if (filePool.length >= maxHistory)
352             {
353                 std.file.remove(filePool[$-1]);
354                 filePool = filePool[0..$-1];
355             }
356             //carry(filePool);
357             return true;
358         }
359         return false;
360     }
361 
362     /**
363      * Rename log files
364      */
365     override
366     void carry()
367     {
368         import std.conv;
369         import std.path;
370 
371         auto filePool = scanDir();
372         foreach_reverse(ref file; filePool)
373         {
374             auto newFile = dir ~ dirSeparator ~ baseName ~ to!string(extractNum(file)+1) ~ "." ~ ext;
375             std.file.rename(file, newFile);
376             file = newFile;
377         }
378     }
379 
380     /**
381      * Extract number from file name
382      */
383     uint extractNum(string file)
384     {
385         import std.conv;
386 
387         uint num = 0;
388         try
389         {
390             static import std.path;
391             import std.string;
392             auto fch = std.path.baseName(file).chompPrefix(baseName);
393             auto m = matchAll(fch, regex(`\d*`));
394 
395             if (!m.empty && m.hit.length > 0)
396             {
397                 num = to!uint(m.hit);
398             }
399         }
400         catch (Exception e)
401         {
402             throw new Exception("Uncorrect log file name: " ~ file ~ "  -> " ~ e.msg);
403         }
404         return num;
405     }
406 
407 }