1 /++ 2 + Adds support for logging std.logger messages to HTML files. 3 + Authors: Cameron "Herringway" Ross 4 + Copyright: Copyright Cameron Ross 2016 5 + License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 6 +/ 7 module htmllog; 8 import std.algorithm : among; 9 import std.array; 10 import std.conv : to; 11 import std.exception : assumeWontThrow; 12 import std.experimental.logger; 13 import std.format : format, formattedWrite; 14 import std.range : isOutputRange, NullSink, put; 15 import std.stdio : File; 16 import std.traits : EnumMembers; 17 import std.typecons : tuple; 18 /++ 19 + Logs messages to a .html file. When viewed in a browser, it provides an 20 + easily-searchable and filterable view of logged messages. 21 +/ 22 public class HTMLLogger : Logger { 23 ///File handle being written to. 24 private File handle; 25 26 /++ 27 + Creates a new log file with the specified path and filename. 28 + Params: 29 + logpath = Full path and filename for the log file 30 + lv = Minimum message level to write to the log 31 + defaultMinDisplayLevel = Minimum message level visible by default 32 +/ 33 this(string logpath, LogLevel lv = LogLevel.all, LogLevel defaultMinDisplayLevel = LogLevel.all) @safe { 34 super(lv); 35 handle.open(logpath, "w"); 36 writeHeader(defaultMinDisplayLevel); 37 } 38 /++ 39 + Writes a log file using an already-opened handle. Note that having 40 + pre-existing data in the file will likely cause display errors. 41 + Params: 42 + file = Prepared file handle to write log to 43 + lv = Minimum message level to write to the log 44 + defaultMinDisplayLevel = Minimum message level visible by default 45 +/ 46 this(File file, LogLevel lv = LogLevel.all, LogLevel defaultMinDisplayLevel = LogLevel.all) @safe in { 47 assert(file.isOpen); 48 } body { 49 super(lv); 50 handle = file; 51 writeHeader(defaultMinDisplayLevel); 52 } 53 ~this() @safe { 54 if (handle.isOpen) { 55 writeFmt(HTMLTemplate.footer); 56 handle.close(); 57 } 58 } 59 /++ 60 + Writes a log message. For internal use by std.experimental.logger. 61 + Params: 62 + payLoad = Data for the log entry being written 63 + See_Also: $(LINK https://dlang.org/library/std/experimental/logger.html) 64 +/ 65 override public void writeLogMsg(ref LogEntry payLoad) @safe { 66 if (payLoad.logLevel >= logLevel) 67 writeFmt(HTMLTemplate.entry, payLoad.logLevel, payLoad.timestamp.toISOExtString(), payLoad.timestamp.toSimpleString(), payLoad.moduleName, payLoad.line, payLoad.threadId, HtmlEscaper(payLoad.msg)); 68 } 69 /++ 70 + Initializes log file by writing header tags, etc. 71 + Params: 72 + minDisplayLevel = Minimum message level visible by default 73 +/ 74 private void writeHeader(LogLevel minDisplayLevel) @safe { 75 static bool initialized = false; 76 if (initialized) 77 return; 78 writeFmt(HTMLTemplate.header, minDisplayLevel.among!(EnumMembers!LogLevel)-1); 79 initialized = true; 80 } 81 /++ 82 + Safe wrapper around handle.lockingTextWriter(). 83 + Params: 84 + fmt = Format of string to write 85 + args = Values to place into formatted string 86 +/ 87 private void writeFmt(T...)(string fmt, T args) @trusted { 88 formattedWrite(handle.lockingTextWriter(), fmt, args); 89 handle.flush(); 90 } 91 } 92 /// 93 @safe unittest { 94 auto logger = new HTMLLogger("test.html", LogLevel.trace); 95 logger.fatalHandler = () {}; 96 foreach (i; 0..100) { //Log one hundred of each king of message 97 logger.trace("Example - Trace"); 98 logger.info("Example - Info"); 99 logger.warning("Example - Warning"); 100 logger.error("Example - Error"); 101 logger.critical("Example - Critical"); 102 logger.fatal("Example - Fatal"); 103 } 104 } 105 /++ 106 + Escapes special HTML characters. Avoids allocating where possible. 107 +/ 108 private struct HtmlEscaper { 109 ///String to escape 110 string data; 111 /+ 112 + Converts data to escaped HTML string. Outputs to an output range to avoid 113 + unnecessary allocation. 114 +/ 115 void toString(T)(auto ref T sink) const if (isOutputRange!(T, immutable char)) { 116 foreach (character; data) { 117 switch (character) { 118 default: put(sink, character); break; 119 case 0: .. case 9: 120 case 11: .. case 12: 121 case 14: .. case 31: 122 put(sink, "&#"); 123 //since we're guaranteed to have a 1 or 2 digit number, this works 124 put(sink, cast(char)('0'+(character/10))); 125 put(sink, cast(char)('0'+(character%10))); 126 break; 127 case '\n', '\r': put(sink, "<br/>"); break; 128 case '&': put(sink, "&"); break; 129 case '<': put(sink, "<"); break; 130 case '>': put(sink, ">"); break; 131 } 132 } 133 } 134 } 135 @safe pure @nogc unittest { 136 import std.conv : text; 137 struct StaticBuf(size_t Size) { 138 char[Size] data; 139 size_t offset; 140 void put(immutable char character) @nogc { 141 data[offset] = character; 142 offset++; 143 } 144 } 145 { 146 auto buf = StaticBuf!0(); 147 HtmlEscaper("").toString(buf); 148 assert(buf.data == ""); 149 } 150 { 151 auto buf = StaticBuf!5(); 152 HtmlEscaper("\n").toString(buf); 153 assert(buf.data == "<br/>"); 154 } 155 { 156 auto buf = StaticBuf!4(); 157 HtmlEscaper("\x1E").toString(buf); 158 assert(buf.data == ""); 159 } 160 } 161 ///Template components for log file 162 private enum HTMLTemplate = tuple!("header", "entry", "footer")( 163 `<!DOCTYPE html> 164 <html> 165 <head> 166 <title>HTML Log</title> 167 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 168 <style content="text/css"> 169 .trace { color: lightgray; } 170 .info { color: black; } 171 .warning { color: darkorange; } 172 .error { color: darkred; } 173 .critical { color: crimson; } 174 .fatal { color: red; } 175 176 body { font-size: 10pt; margin: 0px; } 177 .logmessage { font-family: monospace; margin-left: 10pt; margin-right: 10pt; } 178 .log { margin-top: 15pt; margin-bottom: 15pt; } 179 180 time, div.time { 181 display: inline-block; 182 width: 180pt; 183 } 184 div.source { 185 display: inline-block; 186 width: 200pt; 187 } 188 div.threadName { 189 display: inline-block; 190 width: 100pt; 191 } 192 div.message { 193 display: inline-block; 194 width: calc(100%% - 500pt); 195 } 196 header, footer { 197 position: fixed; 198 width: 100%%; 199 height: 15pt; 200 z-index: 1; 201 } 202 footer { 203 bottom: 0px; 204 background-color: lightgray; 205 } 206 header { 207 top: 0px; 208 background-color: white; 209 } 210 </style> 211 <script language="JavaScript"> 212 function updateLevels(i){ 213 var style = document.styleSheets[0].cssRules[i].style; 214 if (event.target.checked) 215 style.display = ""; 216 else 217 style.display = "none"; 218 } 219 </script> 220 </head> 221 <body> 222 <header class="logmessage"> 223 <div class="time">Time</div> 224 <div class="source">Source</div> 225 <div class="threadName">Thread</div> 226 <div class="message">Message</div> 227 </header> 228 <footer> 229 <form class="menubar"> 230 <input type="checkbox" id="level0" onChange="updateLevels(0)" checked> <label for="level0">Trace</label> 231 <input type="checkbox" id="level1" onChange="updateLevels(1)" checked> <label for="level1">Info</label> 232 <input type="checkbox" id="level2" onChange="updateLevels(2)" checked> <label for="level2">Warning</label> 233 <input type="checkbox" id="level3" onChange="updateLevels(3)" checked> <label for="level3">Error</label> 234 <input type="checkbox" id="level4" onChange="updateLevels(4)" checked> <label for="level4">Critical</label> 235 <input type="checkbox" id="level5" onChange="updateLevels(5)" checked> <label for="level5">Fatal</label> 236 </form> 237 </footer> 238 <script language="JavaScript"> 239 for (var i = 0; i < %s; i++) { 240 document.styleSheets[0].cssRules[i].style.display = "none"; 241 document.getElementById("level" + i).checked = false; 242 } 243 </script> 244 <div class="log">`, 245 ` 246 <div class="%s logmessage"> 247 <time datetime="%s">%s</time> 248 <div class="source">%s:%s</div> 249 <div class="threadName">%s</div> 250 <div class="message">%s</div> 251 </div>`, 252 ` 253 </div> 254 </body> 255 </html>`);