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