1 /** 2 Central logging facility for vibe. 3 4 Copyright: © 2012-2014 Sönke Ludwig 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig 7 */ 8 module vibe.core.log; 9 10 import vibe.core.args; 11 import vibe.core.concurrency : ScopedLock, lock; 12 import vibe.core.sync; 13 14 import std.algorithm; 15 import std.array; 16 import std.datetime; 17 import std.format; 18 import std.stdio; 19 import core.atomic; 20 import core.thread; 21 22 import std.traits : isSomeString; 23 import std.range.primitives : isInputRange, isOutputRange; 24 25 /** 26 Sets the minimum log level to be printed using the default console logger. 27 28 This level applies to the default stdout/stderr logger only. 29 */ 30 void setLogLevel(LogLevel level) 31 nothrow @safe { 32 if (ss_stdoutLogger) 33 ss_stdoutLogger.lock().minLevel = level; 34 } 35 36 37 /** 38 Sets the log format used for the default console logger. 39 40 This level applies to the default stdout/stderr logger only. 41 42 Params: 43 fmt = The log format for the stderr (default is `FileLogger.Format.thread`) 44 infoFmt = The log format for the stdout (default is `FileLogger.Format.plain`) 45 */ 46 void setLogFormat(FileLogger.Format fmt, FileLogger.Format infoFmt = FileLogger.Format.plain) 47 nothrow @safe { 48 if (ss_stdoutLogger) { 49 auto l = ss_stdoutLogger.lock(); 50 l.format = fmt; 51 l.infoFormat = infoFmt; 52 } 53 } 54 55 56 /** 57 Sets a log file for disk file logging. 58 59 Multiple calls to this function will register multiple log 60 files for output. 61 */ 62 void setLogFile(string filename, LogLevel min_level = LogLevel.error) 63 { 64 auto logger = cast(shared)new FileLogger(filename); 65 { 66 auto l = logger.lock(); 67 l.minLevel = min_level; 68 l.format = FileLogger.Format.threadTime; 69 } 70 registerLogger(logger); 71 } 72 73 74 /** 75 Registers a new logger instance. 76 77 The specified Logger will receive all log messages in its Logger.log 78 method after it has been registered. 79 80 Examples: 81 --- 82 auto logger = cast(shared)new HTMLLogger("log.html"); 83 logger.lock().format = FileLogger.Format.threadTime; 84 registerLogger(logger); 85 --- 86 87 See_Also: deregisterLogger 88 */ 89 void registerLogger(shared(Logger) logger) 90 nothrow { 91 ss_loggers ~= logger; 92 } 93 94 95 /** 96 Deregisters an active logger instance. 97 98 See_Also: registerLogger 99 */ 100 void deregisterLogger(shared(Logger) logger) 101 nothrow { 102 for (size_t i = 0; i < ss_loggers.length; ) { 103 if (ss_loggers[i] !is logger) i++; 104 else ss_loggers = ss_loggers[0 .. i] ~ ss_loggers[i+1 .. $]; 105 } 106 } 107 108 109 /** 110 Logs a message. 111 112 Params: 113 level = The log level for the logged message 114 fmt = See http://dlang.org/phobos/std_format.html#format-string 115 args = Any input values needed for formatting 116 */ 117 void log(LogLevel level, /*string mod = __MODULE__, string func = __FUNCTION__,*/ string file = __FILE__, int line = __LINE__, S, T...)(S fmt, lazy T args) 118 nothrow if (isSomeString!S && level != LogLevel.none) 119 { 120 doLog(level, null, null, file, line, fmt, args); 121 } 122 /// ditto 123 void logTrace(/*string mod = __MODULE__, string func = __FUNCTION__,*/ string file = __FILE__, int line = __LINE__, S, T...)(S fmt, lazy T args) nothrow { doLog(LogLevel.trace, null, null/*, mod, func*/, file, line, fmt, args); } 124 /// ditto 125 void logDebugV(/*string mod = __MODULE__, string func = __FUNCTION__,*/ string file = __FILE__, int line = __LINE__, S, T...)(S fmt, lazy T args) nothrow { doLog(LogLevel.debugV, null, null/*, mod, func*/, file, line, fmt, args); } 126 /// ditto 127 void logDebug(/*string mod = __MODULE__, string func = __FUNCTION__,*/ string file = __FILE__, int line = __LINE__, S, T...)(S fmt, lazy T args) nothrow { doLog(LogLevel.debug_, null, null/*, mod, func*/, file, line, fmt, args); } 128 /// ditto 129 void logDiagnostic(/*string mod = __MODULE__, string func = __FUNCTION__,*/ string file = __FILE__, int line = __LINE__, S, T...)(S fmt, lazy T args) nothrow { doLog(LogLevel.diagnostic, null, null/*, mod, func*/, file, line, fmt, args); } 130 /// ditto 131 void logInfo(/*string mod = __MODULE__, string func = __FUNCTION__,*/ string file = __FILE__, int line = __LINE__, S, T...)(S fmt, lazy T args) nothrow { doLog(LogLevel.info, null, null/*, mod, func*/, file, line, fmt, args); } 132 /// ditto 133 void logWarn(/*string mod = __MODULE__, string func = __FUNCTION__,*/ string file = __FILE__, int line = __LINE__, S, T...)(S fmt, lazy T args) nothrow { doLog(LogLevel.warn, null, null/*, mod, func*/, file, line, fmt, args); } 134 /// ditto 135 void logError(/*string mod = __MODULE__, string func = __FUNCTION__,*/ string file = __FILE__, int line = __LINE__, S, T...)(S fmt, lazy T args) nothrow { doLog(LogLevel.error, null, null/*, mod, func*/, file, line, fmt, args); } 136 /// ditto 137 void logCritical(/*string mod = __MODULE__, string func = __FUNCTION__,*/ string file = __FILE__, int line = __LINE__, S, T...)(S fmt, lazy T args) nothrow { doLog(LogLevel.critical, null, null/*, mod, func*/, file, line, fmt, args); } 138 /// ditto 139 void logFatal(string file = __FILE__, int line = __LINE__, S, T...)(S fmt, lazy T args) nothrow { doLog(LogLevel.fatal, null, null, file, line, fmt, args); } 140 141 /// 142 @safe unittest { 143 void test() nothrow 144 { 145 logInfo("Hello, World!"); 146 logWarn("This may not be %s.", "good"); 147 log!(LogLevel.info)("This is a %s.", "test"); 148 } 149 } 150 151 152 /** Logs an exception, including a debug stack trace. 153 */ 154 void logException(LogLevel level = LogLevel.error, string file = __FILE__, 155 int line = __LINE__)(Throwable exception, string error_description) 156 @safe nothrow { 157 log!(level, file, line)("%s: %s", error_description, exception.msg); 158 try logDebug!(file, line)("Full exception: %s", () @trusted { return exception.toString(); } ()); 159 catch (Exception e) logDebug("Failed to print full exception: %s", e.msg); 160 } 161 162 /// 163 unittest { 164 void test() nothrow 165 { 166 try { 167 throw new Exception("Something failed!"); 168 } catch (Exception e) { 169 logException(e, "Failed to carry out some operation"); 170 } 171 } 172 } 173 174 175 /// Specifies the log level for a particular log message. 176 enum LogLevel { 177 trace, /// Developer information for locating events when no useful stack traces are available 178 debugV, /// Developer information useful for algorithm debugging - for verbose output 179 debug_, /// Developer information useful for algorithm debugging 180 diagnostic, /// Extended user information (e.g. for more detailed error information) 181 info, /// Informational message for normal user education 182 warn, /// Unexpected condition that could indicate an error but has no direct consequences 183 error, /// Normal error that is handled gracefully 184 critical, /// Error that severely influences the execution of the application 185 fatal, /// Error that forces the application to terminate 186 none, /// Special value used to indicate no logging when set as the minimum log level 187 188 verbose1 = diagnostic, /// Alias for diagnostic messages 189 verbose2 = debug_, /// Alias for debug messages 190 verbose3 = debugV, /// Alias for verbose debug messages 191 verbose4 = trace, /// Alias for trace messages 192 } 193 194 /// Represents a single logged line 195 struct LogLine { 196 string mod; 197 string func; 198 string file; 199 int line; 200 LogLevel level; 201 Thread thread; 202 string threadName; 203 uint threadID; 204 Fiber fiber; 205 uint fiberID; 206 SysTime time; 207 string text; /// Legacy field used in `Logger.log` 208 } 209 210 /// Abstract base class for all loggers 211 class Logger { 212 LogLevel minLevel = LogLevel.min; 213 214 /** Whether the logger can handle multiple lines in a single beginLine/endLine. 215 216 By default log text with newlines gets split into multiple log lines. 217 */ 218 protected bool multilineLogger = false; 219 220 private { 221 LogLine m_curLine; 222 Appender!string m_curLineText; 223 } 224 225 final bool acceptsLevel(LogLevel value) nothrow pure @safe { return value >= this.minLevel; } 226 227 /** Legacy logging interface relying on dynamic memory allocation. 228 229 Override `beginLine`, `put`, `endLine` instead for a more efficient and 230 possibly allocation-free implementation. 231 */ 232 void log(ref LogLine line) @safe {} 233 234 /// Starts a new log line. 235 void beginLine(ref LogLine line_info) 236 @safe { 237 m_curLine = line_info; 238 m_curLineText = appender!string(); 239 } 240 241 /// Writes part of a log line message. 242 void put(scope const(char)[] text) 243 @safe { 244 m_curLineText.put(text); 245 } 246 247 /// Finalizes a log line. 248 void endLine() 249 @safe { 250 m_curLine.text = m_curLineText.data; 251 log(m_curLine); 252 m_curLine.text = null; 253 m_curLineText = Appender!string.init; 254 } 255 } 256 257 258 /** 259 Plain-text based logger for logging to regular files or stdout/stderr 260 */ 261 final class FileLogger : Logger { 262 /// The log format used by the FileLogger 263 enum Format { 264 plain, /// Output only the plain log message 265 thread, /// Prefix "[thread-id:fiber-id loglevel]" 266 threadTime /// Prefix "[thread-id:fiber-id timestamp loglevel]" 267 } 268 269 private { 270 File m_infoFile; 271 File m_diagFile; 272 File m_curFile; 273 } 274 275 Format format = Format.thread; 276 Format infoFormat = Format.thread; 277 278 /** Use escape sequences to color log output. 279 280 Note that the terminal must support 256-bit color codes. 281 */ 282 bool useColors = false; 283 284 this(File info_file, File diag_file) 285 { 286 m_infoFile = info_file; 287 m_diagFile = diag_file; 288 } 289 290 this(string filename) 291 { 292 auto f = File(filename, "ab"); 293 this(f, f); 294 } 295 296 override void beginLine(ref LogLine msg) 297 @trusted // FILE isn't @safe (as of DMD 2.065) 298 { 299 string pref; 300 final switch (msg.level) { 301 case LogLevel.trace: pref = "trc"; m_curFile = m_diagFile; break; 302 case LogLevel.debugV: pref = "dbv"; m_curFile = m_diagFile; break; 303 case LogLevel.debug_: pref = "dbg"; m_curFile = m_diagFile; break; 304 case LogLevel.diagnostic: pref = "dia"; m_curFile = m_diagFile; break; 305 case LogLevel.info: pref = "INF"; m_curFile = m_infoFile; break; 306 case LogLevel.warn: pref = "WRN"; m_curFile = m_diagFile; break; 307 case LogLevel.error: pref = "ERR"; m_curFile = m_diagFile; break; 308 case LogLevel.critical: pref = "CRITICAL"; m_curFile = m_diagFile; break; 309 case LogLevel.fatal: pref = "FATAL"; m_curFile = m_diagFile; break; 310 case LogLevel.none: assert(false); 311 } 312 313 auto fmt = (m_curFile is m_diagFile) ? this.format : this.infoFormat; 314 315 auto dst = m_curFile.lockingTextWriter; 316 317 if (this.useColors) { 318 version (Posix) { 319 final switch (msg.level) { 320 case LogLevel.trace: dst.put("\x1b[49;38;5;243m"); break; 321 case LogLevel.debugV: dst.put("\x1b[49;38;5;245m"); break; 322 case LogLevel.debug_: dst.put("\x1b[49;38;5;180m"); break; 323 case LogLevel.diagnostic: dst.put("\x1b[49;38;5;143m"); break; 324 case LogLevel.info: dst.put("\x1b[49;38;5;29m"); break; 325 case LogLevel.warn: dst.put("\x1b[49;38;5;220m"); break; 326 case LogLevel.error: dst.put("\x1b[49;38;5;9m"); break; 327 case LogLevel.critical: dst.put("\x1b[41;38;5;15m"); break; 328 case LogLevel.fatal: dst.put("\x1b[48;5;9;30m"); break; 329 case LogLevel.none: assert(false); 330 } 331 } 332 } 333 334 final switch (fmt) { 335 case Format.plain: break; 336 case Format.thread: 337 dst.put('['); 338 if (msg.threadName.length) dst.put(msg.threadName); 339 else dst.formattedWrite("%08X", msg.threadID); 340 dst.put('('); 341 import vibe.core.task : Task; 342 Task.getThis().getDebugID(dst); 343 dst.formattedWrite(") %s] ", pref); 344 break; 345 case Format.threadTime: 346 dst.put('['); 347 auto tm = msg.time; 348 static if (is(typeof(tm.fracSecs))) auto msecs = tm.fracSecs.total!"msecs"; // 2.069 has deprecated "fracSec" 349 else auto msecs = tm.fracSec.msecs; 350 m_curFile.writef("%d-%02d-%02d %02d:%02d:%02d.%03d ", tm.year, tm.month, tm.day, tm.hour, tm.minute, tm.second, msecs); 351 352 if (msg.threadName.length) dst.put(msg.threadName); 353 else dst.formattedWrite("%08X", msg.threadID); 354 dst.put('('); 355 import vibe.core.task : Task; 356 Task.getThis().getDebugID(dst); 357 dst.formattedWrite(") %s] ", pref); 358 break; 359 } 360 } 361 362 override void put(scope const(char)[] text) 363 { 364 static if (__VERSION__ <= 2066) 365 () @trusted { m_curFile.write(text); } (); 366 else m_curFile.write(text); 367 } 368 369 override void endLine() 370 { 371 if (useColors) { 372 version (Posix) { 373 m_curFile.write("\x1b[0m"); 374 } 375 } 376 377 static if (__VERSION__ <= 2066) 378 () @trusted { m_curFile.writeln(); } (); 379 else m_curFile.writeln(); 380 m_curFile.flush(); 381 } 382 } 383 384 /** 385 Logger implementation for logging to an HTML file with dynamic filtering support. 386 */ 387 final class HTMLLogger : Logger { 388 private { 389 File m_logFile; 390 } 391 392 this(string filename = "log.html") 393 { 394 m_logFile = File(filename, "wt"); 395 writeHeader(); 396 } 397 398 ~this() 399 { 400 //version(FinalizerDebug) writeln("HtmlLogWritet.~this"); 401 writeFooter(); 402 m_logFile.close(); 403 //version(FinalizerDebug) writeln("HtmlLogWritet.~this out"); 404 } 405 406 @property void minLogLevel(LogLevel value) pure nothrow @safe { this.minLevel = value; } 407 408 override void beginLine(ref LogLine msg) 409 @trusted // FILE isn't @safe (as of DMD 2.065) 410 { 411 if( !m_logFile.isOpen ) return; 412 413 final switch (msg.level) { 414 case LogLevel.none: assert(false); 415 case LogLevel.trace: m_logFile.write(`<div class="trace">`); break; 416 case LogLevel.debugV: m_logFile.write(`<div class="debugv">`); break; 417 case LogLevel.debug_: m_logFile.write(`<div class="debug">`); break; 418 case LogLevel.diagnostic: m_logFile.write(`<div class="diagnostic">`); break; 419 case LogLevel.info: m_logFile.write(`<div class="info">`); break; 420 case LogLevel.warn: m_logFile.write(`<div class="warn">`); break; 421 case LogLevel.error: m_logFile.write(`<div class="error">`); break; 422 case LogLevel.critical: m_logFile.write(`<div class="critical">`); break; 423 case LogLevel.fatal: m_logFile.write(`<div class="fatal">`); break; 424 } 425 m_logFile.writef(`<div class="timeStamp">%s</div>`, msg.time.toISOExtString()); 426 if (msg.thread) 427 m_logFile.writef(`<div class="threadName">%s</div>`, msg.thread.name); 428 if (msg.fiber) 429 m_logFile.writef(`<div class="taskID">%s</div>`, msg.fiberID); 430 m_logFile.write(`<div class="message">`); 431 } 432 433 override void put(scope const(char)[] text) 434 { 435 auto dst = () @trusted { return m_logFile.lockingTextWriter(); } (); // LockingTextWriter not @safe for DMD 2.066 436 while (!text.empty && (text.front == ' ' || text.front == '\t')) { 437 foreach (i; 0 .. text.front == ' ' ? 1 : 4) 438 () @trusted { dst.put(" "); } (); // LockingTextWriter not @safe for DMD 2.066 439 text.popFront(); 440 } 441 () @trusted { filterHTMLEscape(dst, text); } (); // LockingTextWriter not @safe for DMD 2.066 442 } 443 444 override void endLine() 445 { 446 () @trusted { // not @safe for DMD 2.066 447 m_logFile.write(`</div>`); 448 m_logFile.writeln(`</div>`); 449 } (); 450 m_logFile.flush(); 451 } 452 453 private void writeHeader(){ 454 if( !m_logFile.isOpen ) return; 455 456 m_logFile.writeln( 457 `<html> 458 <head> 459 <title>HTML Log</title> 460 <style content="text/css"> 461 .trace { position: relative; color: #E0E0E0; font-size: 9pt; } 462 .debugv { position: relative; color: #E0E0E0; font-size: 9pt; } 463 .debug { position: relative; color: #808080; } 464 .diagnostic { position: relative; color: #808080; } 465 .info { position: relative; color: black; } 466 .warn { position: relative; color: #E08000; } 467 .error { position: relative; color: red; } 468 .critical { position: relative; color: red; background-color: black; } 469 .fatal { position: relative; color: red; background-color: black; } 470 471 .log { margin-left: 10pt; } 472 .code { 473 font-family: "Courier New"; 474 background-color: #F0F0F0; 475 border: 1px solid gray; 476 margin-bottom: 10pt; 477 margin-left: 30pt; 478 margin-right: 10pt; 479 padding-left: 0pt; 480 } 481 482 div.timeStamp { 483 position: absolute; 484 width: 150pt; 485 } 486 div.threadName { 487 position: absolute; 488 top: 0pt; 489 left: 150pt; 490 width: 100pt; 491 } 492 div.taskID { 493 position: absolute; 494 top: 0pt; 495 left: 250pt; 496 width: 70pt; 497 } 498 div.message { 499 position: relative; 500 top: 0pt; 501 left: 320pt; 502 } 503 body { 504 font-family: Tahoma, Arial, sans-serif; 505 font-size: 10pt; 506 } 507 </style> 508 <script language="JavaScript"> 509 function enableStyle(i){ 510 var style = document.styleSheets[0].cssRules[i].style; 511 style.display = "block"; 512 } 513 514 function disableStyle(i){ 515 var style = document.styleSheets[0].cssRules[i].style; 516 style.display = "none"; 517 } 518 519 function updateLevels(){ 520 var sel = document.getElementById("Level"); 521 var level = sel.value; 522 for( i = 0; i < level; i++ ) disableStyle(i); 523 for( i = level; i < 5; i++ ) enableStyle(i); 524 } 525 </script> 526 </head> 527 <body style="padding: 0px; margin: 0px;" onLoad="updateLevels(); updateCode();"> 528 <div style="position: fixed; z-index: 100; padding: 4pt; width:100%; background-color: lightgray; border-bottom: 1px solid black;"> 529 <form style="margin: 0px;"> 530 Minimum Log Level: 531 <select id="Level" onChange="updateLevels()"> 532 <option value="0">Trace</option> 533 <option value="1">Verbose</option> 534 <option value="2">Debug</option> 535 <option value="3">Diagnostic</option> 536 <option value="4">Info</option> 537 <option value="5">Warn</option> 538 <option value="6">Error</option> 539 <option value="7">Critical</option> 540 <option value="8">Fatal</option> 541 </select> 542 </form> 543 </div> 544 <div style="height: 30pt;"></div> 545 <div class="log">`); 546 m_logFile.flush(); 547 } 548 549 private void writeFooter(){ 550 if( !m_logFile.isOpen ) return; 551 552 m_logFile.writeln( 553 ` </div> 554 </body> 555 </html>`); 556 m_logFile.flush(); 557 } 558 559 private static void filterHTMLEscape(R, S)(ref R dst, S str) 560 { 561 for (;!str.empty;str.popFront()) { 562 auto ch = str.front; 563 switch (ch) { 564 default: dst.put(ch); break; 565 case '<': dst.put("<"); break; 566 case '>': dst.put(">"); break; 567 case '&': dst.put("&"); break; 568 } 569 } 570 } 571 } 572 573 574 import std.conv; 575 /** 576 A logger that logs in syslog format according to RFC 5424. 577 578 Messages can be logged to files (via file streams) or over the network (via 579 TCP or SSL streams). 580 581 Standards: Conforms to RFC 5424. 582 */ 583 final class SyslogLogger(OutputStream) : Logger { 584 private { 585 string m_hostName; 586 string m_appName; 587 OutputStream m_ostream; 588 Facility m_facility; 589 } 590 591 deprecated("Use `SyslogFacility` instead.") 592 alias Facility = SyslogFacility; 593 594 /// Severities 595 private enum Severity { 596 emergency, /// system is unusable 597 alert, /// action must be taken immediately 598 critical, /// critical conditions 599 error, /// error conditions 600 warning, /// warning conditions 601 notice, /// normal but significant condition 602 info, /// informational messages 603 debug_, /// debug-level messages 604 } 605 606 /// syslog message format (version 1) 607 /// see section 6 in RFC 5424 608 private enum SYSLOG_MESSAGE_FORMAT_VERSION1 = "<%.3s>1 %s %.255s %.48s %.128s %.32s %s %s"; 609 /// 610 private enum NILVALUE = "-"; 611 /// 612 private enum BOM = hexString!"EFBBBF"; 613 614 /** 615 Construct a SyslogLogger. 616 617 The log messages are sent to the given OutputStream stream using the given 618 Facility facility.Optionally the appName and hostName can be set. The 619 appName defaults to null. The hostName defaults to hostName(). 620 621 Note that the passed stream's write function must not use logging with 622 a level for that this Logger's acceptsLevel returns true. Because this 623 Logger uses the stream's write function when it logs and would hence 624 log forevermore. 625 */ 626 this(OutputStream stream, Facility facility, string appName = null, string hostName = hostName()) 627 { 628 m_hostName = hostName != "" ? hostName : NILVALUE; 629 m_appName = appName != "" ? appName : NILVALUE; 630 m_ostream = stream; 631 m_facility = facility; 632 this.minLevel = LogLevel.debug_; 633 } 634 635 /** 636 Logs the given LogLine msg. 637 638 It uses the msg's time, level, and text field. 639 */ 640 override void beginLine(ref LogLine msg) 641 @trusted { // OutputStream isn't @safe 642 auto tm = msg.time; 643 import core.time; 644 // at most 6 digits for fractional seconds according to RFC 645 static if (is(typeof(tm.fracSecs))) tm.fracSecs = tm.fracSecs.total!"usecs".dur!"usecs"; 646 else tm.fracSec = FracSec.from!"usecs"(tm.fracSec.usecs); 647 auto timestamp = tm.toISOExtString(); 648 649 Severity syslogSeverity; 650 // map LogLevel to syslog's severity 651 final switch(msg.level) { 652 case LogLevel.none: assert(false); 653 case LogLevel.trace: return; 654 case LogLevel.debugV: return; 655 case LogLevel.debug_: syslogSeverity = Severity.debug_; break; 656 case LogLevel.diagnostic: syslogSeverity = Severity.info; break; 657 case LogLevel.info: syslogSeverity = Severity.notice; break; 658 case LogLevel.warn: syslogSeverity = Severity.warning; break; 659 case LogLevel.error: syslogSeverity = Severity.error; break; 660 case LogLevel.critical: syslogSeverity = Severity.critical; break; 661 case LogLevel.fatal: syslogSeverity = Severity.alert; break; 662 } 663 664 assert(msg.level >= LogLevel.debug_); 665 import std.conv : to; // temporary workaround for issue 1016 (DMD cross-module template overloads error out before second attempted module) 666 auto priVal = m_facility * 8 + syslogSeverity; 667 668 alias procId = NILVALUE; 669 alias msgId = NILVALUE; 670 alias structuredData = NILVALUE; 671 672 auto text = msg.text; 673 import std.format : formattedWrite; 674 auto str = StreamOutputRange!OutputStream(m_ostream); 675 str.formattedWrite(SYSLOG_MESSAGE_FORMAT_VERSION1, priVal, 676 timestamp, m_hostName, BOM ~ m_appName, procId, msgId, 677 structuredData, BOM); 678 } 679 680 override void put(scope const(char)[] text) 681 @trusted { 682 m_ostream.write(text); 683 } 684 685 override void endLine() 686 @trusted { 687 m_ostream.write("\n"); 688 m_ostream.flush(); 689 } 690 691 private struct StreamOutputRange(OutputStream) 692 { 693 private { 694 OutputStream m_stream; 695 size_t m_fill = 0; 696 char[256] m_data = void; 697 } 698 699 @safe: 700 701 @disable this(this); 702 703 this(OutputStream stream) { m_stream = stream; } 704 ~this() { flush(); } 705 void flush() 706 { 707 if (m_fill == 0) return; 708 m_stream.write(m_data[0 .. m_fill]); 709 m_fill = 0; 710 } 711 712 void put(char bt) 713 { 714 m_data[m_fill++] = bt; 715 if (m_fill >= m_data.length) flush(); 716 } 717 718 void put(const(char)[] bts) 719 { 720 // avoid writing more chunks than necessary 721 if (bts.length + m_fill >= m_data.length * 2) { 722 flush(); 723 m_stream.write(bts); 724 return; 725 } 726 727 while (bts.length) { 728 auto len = min(m_data.length - m_fill, bts.length); 729 m_data[m_fill .. m_fill + len] = bts[0 .. len]; 730 m_fill += len; 731 bts = bts[len .. $]; 732 if (m_fill >= m_data.length) flush(); 733 } 734 } 735 } 736 } 737 738 /// Syslog facilities 739 enum SyslogFacility { 740 kern, /// kernel messages 741 user, /// user-level messages 742 mail, /// mail system 743 daemon, /// system daemons 744 auth, /// security/authorization messages 745 syslog, /// messages generated internally by syslogd 746 lpr, /// line printer subsystem 747 news, /// network news subsystem 748 uucp, /// UUCP subsystem 749 clockDaemon, /// clock daemon 750 authpriv, /// security/authorization messages 751 ftp, /// FTP daemon 752 ntp, /// NTP subsystem 753 logAudit, /// log audit 754 logAlert, /// log alert 755 cron, /// clock daemon 756 local0, /// local use 0 757 local1, /// local use 1 758 local2, /// local use 2 759 local3, /// local use 3 760 local4, /// local use 4 761 local5, /// local use 5 762 local6, /// local use 6 763 local7, /// local use 7 764 } 765 766 unittest 767 { 768 import vibe.core.file; 769 auto fstream = createTempFile(); 770 auto logger = new SyslogLogger!FileStream(fstream, SyslogFacility.local1, "appname", null); 771 LogLine msg; 772 import std.datetime; 773 import core.thread; 774 static if (is(typeof(SysTime.init.fracSecs))) auto fs = 1.dur!"usecs"; 775 else auto fs = FracSec.from!"usecs"(1); 776 msg.time = SysTime(DateTime(0, 1, 1, 0, 0, 0), fs); 777 778 foreach (lvl; [LogLevel.debug_, LogLevel.diagnostic, LogLevel.info, LogLevel.warn, LogLevel.error, LogLevel.critical, LogLevel.fatal]) { 779 msg.level = lvl; 780 logger.beginLine(msg); 781 logger.put("αβγ"); 782 logger.endLine(); 783 } 784 auto path = fstream.path; 785 fstream.close(); 786 787 import std.file; 788 import std..string; 789 auto lines = splitLines(readText(path.toString()), KeepTerminator.yes); 790 alias BOM = SyslogLogger!FileStream.BOM; 791 assert(lines.length == 7); 792 assert(lines[0] == "<143>1 0000-01-01T00:00:00.000001 - " ~ BOM ~ "appname - - - " ~ BOM ~ "αβγ\n"); 793 assert(lines[1] == "<142>1 0000-01-01T00:00:00.000001 - " ~ BOM ~ "appname - - - " ~ BOM ~ "αβγ\n"); 794 assert(lines[2] == "<141>1 0000-01-01T00:00:00.000001 - " ~ BOM ~ "appname - - - " ~ BOM ~ "αβγ\n"); 795 assert(lines[3] == "<140>1 0000-01-01T00:00:00.000001 - " ~ BOM ~ "appname - - - " ~ BOM ~ "αβγ\n"); 796 assert(lines[4] == "<139>1 0000-01-01T00:00:00.000001 - " ~ BOM ~ "appname - - - " ~ BOM ~ "αβγ\n"); 797 assert(lines[5] == "<138>1 0000-01-01T00:00:00.000001 - " ~ BOM ~ "appname - - - " ~ BOM ~ "αβγ\n"); 798 assert(lines[6] == "<137>1 0000-01-01T00:00:00.000001 - " ~ BOM ~ "appname - - - " ~ BOM ~ "αβγ\n"); 799 removeFile(path.toString()); 800 } 801 802 803 /// Returns: this host's host name. 804 /// 805 /// If the host name cannot be determined the function returns null. 806 private string hostName() 807 { 808 string hostName; 809 810 version (Posix) { 811 import core.sys.posix.sys.utsname : uname, utsname; 812 utsname name; 813 if (uname(&name)) return hostName; 814 hostName = name.nodename.ptr.to!string; 815 816 import std.socket; 817 auto ih = new InternetHost; 818 if (!ih.getHostByName(hostName)) return hostName; 819 hostName = ih.name; 820 } 821 // TODO: determine proper host name on windows 822 823 return hostName; 824 } 825 826 private { 827 __gshared shared(Logger)[] ss_loggers; 828 shared(FileLogger) ss_stdoutLogger; 829 } 830 831 /** 832 Returns a list of all registered loggers. 833 */ 834 shared(Logger)[] getLoggers() nothrow @trusted { return ss_loggers; } 835 836 package void initializeLogModule() 837 { 838 version (Windows) { 839 version (VibeWinrtDriver) enum disable_stdout = true; 840 else { 841 enum disable_stdout = false; 842 if (!GetStdHandle(STD_OUTPUT_HANDLE) || !GetStdHandle(STD_ERROR_HANDLE)) return; 843 } 844 } else enum disable_stdout = false; 845 846 static if (!disable_stdout) { 847 auto stdoutlogger = new FileLogger(stdout, stderr); 848 version (Posix) { 849 import core.sys.posix.unistd : isatty; 850 if (isatty(stdout.fileno)) 851 stdoutlogger.useColors = true; 852 } 853 stdoutlogger.minLevel = LogLevel.info; 854 stdoutlogger.format = FileLogger.Format.plain; 855 ss_stdoutLogger = cast(shared)stdoutlogger; 856 857 registerLogger(ss_stdoutLogger); 858 859 bool[4] verbose; 860 version (VibeNoDefaultArgs) {} 861 else { 862 readOption("verbose|v" , &verbose[0], "Enables diagnostic messages (verbosity level 1)."); 863 readOption("vverbose|vv", &verbose[1], "Enables debugging output (verbosity level 2)."); 864 readOption("vvv" , &verbose[2], "Enables high frequency debugging output (verbosity level 3)."); 865 readOption("vvvv" , &verbose[3], "Enables high frequency trace output (verbosity level 4)."); 866 } 867 868 foreach_reverse (i, v; verbose) 869 if (v) { 870 setLogFormat(FileLogger.Format.thread); 871 setLogLevel(cast(LogLevel)(LogLevel.diagnostic - i)); 872 break; 873 } 874 875 if (verbose[3]) setLogFormat(FileLogger.Format.threadTime, FileLogger.Format.threadTime); 876 } 877 } 878 879 private nothrow void doLog(S, T...)(LogLevel level, string mod, string func, string file, int line, S fmt, lazy T args) 880 { 881 try { 882 static if(T.length != 0) 883 auto args_copy = args; 884 885 foreach (l; getLoggers()) 886 if (l.minLevel <= level) { // WARNING: TYPE SYSTEM HOLE: accessing field of shared class! 887 auto ll = l.lock(); 888 auto rng = LogOutputRange(ll, file, line, level); 889 static if(T.length != 0) 890 /*() @trusted {*/ rng.formattedWrite(fmt, args_copy); //} (); // formattedWrite is not @safe at least up to 2.068.0 891 else 892 rng.put(fmt); 893 rng.finalize(); 894 } 895 } catch(Exception e) debug assert(false, e.msg); 896 } 897 898 private struct LogOutputRange { 899 LogLine info; 900 ScopedLock!Logger* logger; 901 902 @safe: 903 904 this(ref ScopedLock!Logger logger, string file, int line, LogLevel level) 905 { 906 () @trusted { this.logger = &logger; } (); 907 try { 908 () @trusted { this.info.time = Clock.currTime(UTC()); }(); // not @safe as of 2.065 909 //this.info.mod = mod; 910 //this.info.func = func; 911 this.info.file = file; 912 this.info.line = line; 913 this.info.level = level; 914 this.info.thread = () @trusted { return Thread.getThis(); }(); // not @safe as of 2.065 915 this.info.threadID = makeid(this.info.thread); 916 this.info.threadName = () @trusted { return this.info.thread ? this.info.thread.name : ""; } (); 917 this.info.fiber = () @trusted { return Fiber.getThis(); }(); // not @safe as of 2.065 918 this.info.fiberID = makeid(this.info.fiber); 919 } catch (Exception e) { 920 try { 921 () @trusted { writefln("Error during logging: %s", e.toString()); }(); // not @safe as of 2.065 922 } catch(Exception) {} 923 assert(false, "Exception during logging: "~e.msg); 924 } 925 926 this.logger.beginLine(info); 927 } 928 929 void finalize() 930 { 931 logger.endLine(); 932 } 933 934 void put(scope const(char)[] text) 935 { 936 if (text.empty) 937 return; 938 939 if (logger.multilineLogger) 940 logger.put(text); 941 else 942 { 943 auto rng = text.splitter('\n'); 944 logger.put(rng.front); 945 rng.popFront; 946 foreach (line; rng) 947 { 948 logger.endLine(); 949 logger.beginLine(info); 950 logger.put(line); 951 } 952 } 953 } 954 955 void put(char ch) @trusted { put((&ch)[0 .. 1]); } 956 957 void put(dchar ch) 958 { 959 static import std.utf; 960 if (ch < 128) put(cast(char)ch); 961 else { 962 char[4] buf; 963 auto len = std.utf.encode(buf, ch); 964 put(buf[0 .. len]); 965 } 966 } 967 968 private uint makeid(T)(T ptr) @trusted { return (cast(ulong)cast(void*)ptr & 0xFFFFFFFF) ^ (cast(ulong)cast(void*)ptr >> 32); } 969 } 970 971 private version (Windows) { 972 import core.sys.windows.windows; 973 enum STD_OUTPUT_HANDLE = cast(DWORD)-11; 974 enum STD_ERROR_HANDLE = cast(DWORD)-12; 975 extern(System) HANDLE GetStdHandle(DWORD nStdHandle); 976 } 977 978 unittest 979 { 980 static class TestLogger : Logger 981 { 982 string[] lines; 983 override void beginLine(ref LogLine msg) { lines.length += 1; } 984 override void put(scope const(char)[] text) { lines[$-1] ~= text; } 985 override void endLine() { } 986 } 987 auto logger = new TestLogger; 988 auto ll = (cast(shared(Logger))logger).lock(); 989 auto rng = LogOutputRange(ll, __FILE__, __LINE__, LogLevel.info); 990 rng.formattedWrite("text\nwith\nnewlines"); 991 rng.finalize(); 992 993 assert(logger.lines == ["text", "with", "newlines"]); 994 logger.lines = null; 995 logger.multilineLogger = true; 996 997 rng = LogOutputRange(ll, __FILE__, __LINE__, LogLevel.info); 998 rng.formattedWrite("text\nwith\nnewlines"); 999 rng.finalize(); 1000 assert(logger.lines == ["text\nwith\nnewlines"]); 1001 } 1002 1003 unittest { // make sure the default logger doesn't allocate/is usable within finalizers 1004 bool destroyed = false; 1005 1006 class Test { 1007 ~this() 1008 { 1009 logInfo("logInfo doesn't allocate."); 1010 destroyed = true; 1011 } 1012 } 1013 1014 auto t = new Test; 1015 destroy(t); 1016 assert(destroyed); 1017 }