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