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