1 /*
2  * Hunt - A refined core library for D programming language.
3  *
4  * Copyright (C) 2018-2019 HuntLabs
5  *
6  * Website: https://www.huntlabs.net/
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11 module hunt.logging.Logger;
12 
13 import hunt.concurrency.thread.Helper;
14 
15 import core.thread;
16 
17 import std.algorithm.iteration;
18 import std.array;
19 import std.concurrency;
20 import std.exception;
21 import std.file;
22 import std.parallelism;
23 import std.stdio;
24 import std.datetime;
25 import std.format;
26 import std.range;
27 import std.conv;
28 import std.regex;
29 import std.path;
30 import std.typecons;
31 import std.traits;
32 import std.string;
33 
34 
35 
36 private:
37 
38 class SizeBaseRollover
39 {
40 
41 	import std.path;
42 	import std.string;
43 	import std.typecons;
44 
45 	string path;
46 	string dir;
47 	string baseName;
48 	string ext;
49 	string activeFilePath;
50 
51 	/**
52 	 * Max size of one file
53 	 */
54 	uint maxSize;
55 
56 	/**
57 	 * Max number of working files
58 	 */
59 	uint maxHistory;
60 
61 	this(string fileName, string size, uint maxNum)
62 	{
63 		path = fileName;
64 		auto fileInfo = parseConfigFilePath(fileName);
65 		dir = fileInfo[0];
66 		baseName = fileInfo[1];
67 		ext = fileInfo[2];
68 
69 		activeFilePath = path;
70 		maxSize = extractSize(size);
71 
72 		maxHistory = maxNum;
73 	}
74 
75 	auto parseConfigFilePath(string rawConfigFile)
76 	{
77 		string configFile = buildNormalizedPath(rawConfigFile);
78 
79 		immutable dir = configFile.dirName;
80 		string fullBaseName = std.path.baseName(configFile);
81 		auto ldotPos = fullBaseName.lastIndexOf(".");
82 		immutable ext = (ldotPos > 0) ? fullBaseName[ldotPos + 1 .. $] : "log";
83 		immutable baseName = (ldotPos > 0) ? fullBaseName[0 .. ldotPos] : fullBaseName;
84 
85 		return tuple(dir, baseName, ext);
86 	}
87 
88 	uint extractSize(string size)
89 	{
90 		import std.uni : toLower;
91 		import std.uni : toUpper;
92 		import std.conv;
93 
94 		uint nsize = 0;
95 		auto n = matchAll(size, regex(`\d*`));
96 		if (!n.empty && (n.hit.length != 0))
97 		{
98 			nsize = to!int(n.hit);
99 			auto m = matchAll(size, regex(`\D{1}`));
100 			if (!m.empty && (m.hit.length != 0))
101 			{
102 				switch (m.hit.toUpper)
103 				{
104 				case "K":
105 					nsize *= KB;
106 					break;
107 				case "M":
108 					nsize *= MB;
109 					break;
110 				case "G":
111 					nsize *= GB;
112 					break;
113 				case "T":
114 					nsize *= TB;
115 					break;
116 				case "P":
117 					nsize *= PB;
118 					break;
119 				default:
120 					throw new Exception("In Logger configuration uncorrect number: " ~ size);
121 				}
122 			}
123 		}
124 		return nsize;
125 	}
126 
127 	enum KB = 1024;
128 	enum MB = KB * 1024;
129 	enum GB = MB * 1024;
130 	enum TB = GB * 1024;
131 	enum PB = TB * 1024;
132 
133 	/**
134 	 * Scan work directory
135 	 * save needed files to pool
136  	 */
137 	string[] scanDir()
138 	{
139 		import std.algorithm.sorting : sort;
140 		import std.algorithm;
141 
142 		bool tc(string s)
143 		{
144 			static import std.path;
145 
146 			auto base = std.path.baseName(s);
147 			auto m = matchAll(base, regex(baseName ~ `\d*\.` ~ ext));
148 			if (m.empty || (m.hit != base))
149 			{
150 				return false;
151 			}
152 			return true;
153 		}
154 
155 		return std.file.dirEntries(dir, SpanMode.shallow)
156 			.filter!(a => a.isFile).map!(a => a.name).filter!(a => tc(a))
157 			.array.sort!("a < b").array;
158 	}
159 
160 	/**
161 	 * Do files rolling by size
162 	 */
163 
164 	bool roll(string msg)
165 	{
166 		auto filePool = scanDir();
167 		if (filePool.length == 0)
168 		{
169 			return false;
170 		}
171 		if ((getSize(filePool[0]) + msg.length) >= maxSize)
172 		{
173 			//if ((filePool.front.getSize == 0) throw
174 			if (filePool.length >= maxHistory)
175 			{
176 				std.file.remove(filePool[$ - 1]);
177 				filePool = filePool[0 .. $ - 1];
178 			}
179 			//carry(filePool);
180 			return true;
181 		}
182 		return false;
183 	}
184 
185 	/**
186 	 * Rename log files
187 	 */
188 
189 	void carry()
190 	{
191 		import std.conv;
192 		import std.path;
193 
194 		auto filePool = scanDir();
195 		foreach_reverse (ref file; filePool)
196 		{
197 			auto newFile = dir ~ dirSeparator ~ baseName ~ to!string(extractNum(file) + 1)
198 				~ "." ~ ext;
199 			std.file.rename(file, newFile);
200 			file = newFile;
201 		}
202 	}
203 
204 	/**
205 	 * Extract number from file name
206 	 */
207 	uint extractNum(string file)
208 	{
209 		import std.conv;
210 
211 		uint num = 0;
212 		try
213 		{
214 			static import std.path;
215 			import std.string;
216 
217 			auto fch = std.path.baseName(file).chompPrefix(baseName);
218 			auto m = matchAll(fch, regex(`\d*`));
219 
220 			if (!m.empty && m.hit.length > 0)
221 			{
222 				num = to!uint(m.hit);
223 			}
224 		}
225 		catch (Exception e)
226 		{
227 			throw new Exception("Uncorrect log file name: " ~ file ~ "  -> " ~ e.msg);
228 		}
229 		return num;
230 	}
231 
232 }
233 
234 __gshared Logger g_logger = null;
235 
236 version (Windows)
237 {
238 	import core.sys.windows.wincon;
239 	import core.sys.windows.winbase;
240 	import core.sys.windows.windef;
241 
242 	private __gshared HANDLE g_hout;
243 	shared static this() {
244 		g_hout = GetStdHandle(STD_OUTPUT_HANDLE);
245 	}
246 }
247 
248 /**
249 */
250 class Logger
251 {
252 
253 	__gshared Logger[string] g_logger;
254 	static Logger createLogger(string name , LogConf conf)
255 	{
256 		g_logger[name] = new Logger(conf);
257 		return g_logger[name];
258 	}
259 
260 	static Logger getLogger(string name)
261 	{
262 		return g_logger[name];
263 	}
264 
265 	void log(string file = __FILE__ , size_t line = __LINE__ , string func = __FUNCTION__ , A ...)(LogLevel level , lazy A args)
266 	{
267 		write(level , toFormat(func , logFormat(args) , file , line , level));
268 	}
269 
270 	void logf(string file = __FILE__ , size_t line = __LINE__ , string func = __FUNCTION__ , A ...)(LogLevel level , lazy A args)
271 	{
272 		write(level , toFormat(func , logFormatf(args) , file , line , level));
273 	}
274 
275 	this(LogConf conf)
276 	{
277 		_conf = conf;
278 		string fileName = conf.fileName;
279 
280 		if (!fileName.empty)
281 		{
282 			if(exists(fileName) && isDir(fileName))
283 				throw new Exception("A direction has existed with the same name.");
284 			
285 			createPath(conf.fileName);
286 			_file = File(conf.fileName, "a");
287 			_rollover = new SizeBaseRollover(conf.fileName, _conf.maxSize, _conf.maxNum);
288 		}
289 
290 		immutable void* data = cast(immutable void*) this;
291 		if(!_conf.fileName.empty)
292 			_tid = spawn(&Logger.worker, data);
293 	}
294 
295 	void write(LogLevel level, string msg)
296 	{
297 		if (level >= _conf.level)
298 		{
299 			//#1 console 
300 			//check if enableConsole or appender == AppenderConsole
301 
302 			if (_conf.fileName == "" || !_conf.disableConsole)
303 			{
304 				writeFormatColor(level, msg);
305 			}
306 
307 			//#2 file
308 			if (_conf.fileName != "")
309 			{
310 				send(_tid, msg);
311 			}
312 		}
313 	}
314 
315 
316 
317 protected:
318 
319 	static void worker(immutable void* ptr)
320 	{
321 		Logger logger = cast(Logger) ptr;
322 		bool flag = true;
323 		while (flag)
324 		{
325 			receive((string msg) {
326 				logger.saveMsg(msg);
327 			}, (OwnerTerminated e) { flag = false; }, (Variant any) {  });
328 		}
329 	}
330 
331 	void saveMsg(string msg)
332 	{
333 		try
334 		{
335 
336 			if (!_file.name.exists)
337 			{
338 				_file = File(_rollover.activeFilePath, "w");
339 			}
340 			else if (_rollover.roll(msg))
341 			{
342 				_file.detach();
343 				_rollover.carry();
344 				_file = File(_rollover.activeFilePath, "w");
345 			}
346 			else if (!_file.isOpen())
347 			{
348 				_file.open("a");
349 			}
350 			_file.writeln(msg);
351 			_file.flush();
352 
353 		}
354 		catch (Throwable e)
355 		{
356 			writeln(e.toString());
357 		}
358 
359 	}
360 
361 	static void createPath(string fileFullName)
362 	{
363 		import std.path : dirName;
364 		import std.file : mkdirRecurse;
365 		import std.file : exists;
366 
367 		string dir = dirName(fileFullName);
368 		if (!exists(dir))
369 			mkdirRecurse(dir);
370 	}
371 
372 	static string toString(LogLevel level)
373 	{
374 		string l;
375 		final switch (level) with (LogLevel)
376 		{
377 		case LOG_DEBUG:
378 			l = "debug";
379 			break;
380 		case LOG_INFO:
381 			l = "info";
382 			break;
383 		case LOG_WARNING:
384 			l = "warning";
385 			break;
386 		case LOG_ERROR:
387 			l = "error";
388 			break;
389 		case LOG_FATAL:
390 			l = "fatal";
391 			break;
392 		case LOG_Off:
393 			l = "off";
394 			break;
395 		}
396 		return l;
397 	}
398 
399 	static string logFormatf(A...)(A args)
400 	{
401 		auto strings = appender!string();
402 		formattedWrite(strings, args);
403 		return strings.data;
404 	}
405 
406 	static string logFormat(A...)(A args)
407 	{
408 		auto w = appender!string();
409 		foreach (arg; args)
410 		{
411 			alias A = typeof(arg);
412 			static if (isAggregateType!A || is(A == enum))
413 			{
414 				import std.format : formattedWrite;
415 
416 				formattedWrite(w, "%s", arg);
417 			}
418 			else static if (isSomeString!A)
419 			{
420 				put(w, arg);
421 			}
422 			else static if (isIntegral!A)
423 			{
424 				import std.conv : toTextRange;
425 
426 				toTextRange(arg, w);
427 			}
428 			else static if (isBoolean!A)
429 			{
430 				put(w, arg ? "true" : "false");
431 			}
432 			else static if (isSomeChar!A)
433 			{
434 				put(w, arg);
435 			}
436 			else
437 			{
438 				import std.format : formattedWrite;
439 
440 				// Most general case
441 				formattedWrite(w, "%s", arg);
442 			}
443 		}
444 		return w.data;
445 	}
446 
447 	static string toFormat(string func, string msg, string file, size_t line, LogLevel level)
448 	{
449 		import hunt.util.DateTime;
450 		string time_prior = date("Y-m-d H:i:s");
451 
452 		string tid = to!string(getTid());
453 
454 		string[] funcs = func.split(".");
455 		string myFunc;
456 		if (funcs.length > 0)
457 			myFunc = funcs[$ - 1];
458 		else
459 			myFunc = func;
460 
461 		return time_prior ~ " (" ~ tid ~ ") [" ~ toString(
462 				level) ~ "] " ~ myFunc ~ " - " ~ msg ~ " - " ~ file ~ ":" ~ to!string(line);
463 	}
464 
465 protected:
466 
467 	LogConf _conf;
468 	Tid _tid;
469 	File _file;
470 	SizeBaseRollover _rollover;
471 	version (Posix)
472 	{
473 		enum PRINT_COLOR_NONE = "\033[m";
474 		enum PRINT_COLOR_RED = "\033[0;32;31m";
475 		enum PRINT_COLOR_GREEN = "\033[0;32;32m";
476 		enum PRINT_COLOR_YELLOW = "\033[1;33m";
477 	}
478 
479 	static void writeFormatColor(LogLevel level, string msg)
480 	{
481 		version (Posix)
482 		{
483 			string prior_color;
484 			switch (level) with (LogLevel)
485 			{
486 				case LOG_ERROR:
487 				case LOG_FATAL:
488 					prior_color = PRINT_COLOR_RED;
489 					break;
490 				case LOG_WARNING:
491 					prior_color = PRINT_COLOR_YELLOW;
492 					break;
493 				case LOG_INFO:
494 					prior_color = PRINT_COLOR_GREEN;
495 					break;
496 				default:
497 					prior_color = string.init;
498 			}
499 
500 			writeln(prior_color ~ msg ~ PRINT_COLOR_NONE);
501 		}
502 		else version (Windows)
503 		{
504 			import std.windows.charset;
505             import core.stdc.stdio;
506 
507 			enum defaultColor = FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE;
508 
509 			ushort color;
510 			switch (level) with (LogLevel)
511 			{
512 			case LOG_ERROR:
513 			case LOG_FATAL:
514 				color = FOREGROUND_RED;
515 				break;
516 			case LOG_WARNING:
517 				color = FOREGROUND_GREEN | FOREGROUND_RED;
518 				break;
519 			case LOG_INFO:
520 				color = FOREGROUND_GREEN;
521 				break;
522 			default:
523 				color = defaultColor;
524 			}
525 
526 			SetConsoleTextAttribute(g_hout, color);
527             printf("%s\n", toMBSz(msg));
528 			if(color != defaultColor)
529 				SetConsoleTextAttribute(g_hout, defaultColor);
530 		}
531 	}
532 }
533 
534 string code(string func, LogLevel level, bool f = false)()
535 {
536 	return "void " ~ func
537 		~ `(string file = __FILE__ , size_t line = __LINE__ , string func = __FUNCTION__ , A ...)(lazy A args)
538 	{
539 		if(g_logger is null)
540 			Logger.writeFormatColor(`
541 		~ level.stringof ~ ` , Logger.toFormat(func , Logger.logFormat` ~ (f
542 				? "f" : "") ~ `(args) , file , line , ` ~ level.stringof ~ `));
543 		else
544 			g_logger.write(`
545 		~ level.stringof ~ ` , Logger.toFormat(func , Logger.logFormat` ~ (f
546 				? "f" : "") ~ `(args) , file , line ,` ~ level.stringof ~ ` ));
547 	}`;
548 }
549 
550 
551 
552 public:
553 
554 enum LogLevel
555 {
556 	LOG_DEBUG = 0,
557 	LOG_INFO = 1,	
558 	LOG_WARNING = 2,
559 	LOG_ERROR = 3,
560 	LOG_FATAL = 4,
561 	LOG_Off = 5
562 }
563 
564 struct LogConf
565 {
566 	LogLevel level; // 0 debug 1 info 2 warning 3 error 4 fatal
567 	bool disableConsole;
568 	string fileName = "";
569 	string maxSize = "2MB";
570 	uint maxNum = 5;
571 }
572 
573 void logLoadConf(LogConf conf)
574 {
575 	g_logger = new Logger(conf);	
576 }
577 
578 mixin(code!("logDebug", LogLevel.LOG_DEBUG));
579 mixin(code!("logDebugf", LogLevel.LOG_DEBUG, true));
580 mixin(code!("logInfo", LogLevel.LOG_INFO));
581 mixin(code!("logInfof", LogLevel.LOG_INFO, true));
582 mixin(code!("logWarning", LogLevel.LOG_WARNING));
583 mixin(code!("logWarningf", LogLevel.LOG_WARNING, true));
584 mixin(code!("logError", LogLevel.LOG_ERROR));
585 mixin(code!("logErrorf", LogLevel.LOG_ERROR, true));
586 mixin(code!("logFatal", LogLevel.LOG_FATAL));
587 mixin(code!("logFatalf", LogLevel.LOG_FATAL, true));
588 
589 alias trace = logDebug;
590 alias tracef = logDebugf;
591 alias info = logInfo;
592 alias infof = logInfof;
593 alias warning = logWarning;
594 alias warningf = logWarningf;
595 alias error = logError;
596 alias errorf = logErrorf;
597 alias critical = logFatal;
598 alias criticalf = logFatalf;
599 
600 
601