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