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