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 }