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 }