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