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