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