1 /** 2 A HTTP 1.1/1.0 server implementation. 3 4 Copyright: © 2012-2013 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, Jan Krüger 7 */ 8 module vibe.http.log; 9 10 import vibe.core.file; 11 import vibe.core.log; 12 import vibe.core.sync : InterruptibleTaskMutex, performLocked; 13 import vibe.http.server; 14 import vibe.utils.array : FixedAppender; 15 16 import std.array; 17 import std.conv; 18 import std.exception; 19 import std.string; 20 21 22 class HTTPLogger { 23 @safe: 24 25 private { 26 string m_format; 27 const(HTTPServerSettings) m_settings; 28 InterruptibleTaskMutex m_mutex; 29 Appender!(char[]) m_lineAppender; 30 } 31 32 this(const HTTPServerSettings settings, string format) 33 { 34 m_format = format; 35 m_settings = settings; 36 m_mutex = new InterruptibleTaskMutex; 37 m_lineAppender.reserve(2048); 38 } 39 40 void close() {} 41 42 final void log(scope HTTPServerRequest req, scope HTTPServerResponse res) 43 { 44 m_mutex.performLocked!(() @safe { 45 m_lineAppender.clear(); 46 formatApacheLog(m_lineAppender, m_format, req, res, m_settings); 47 writeLine(m_lineAppender.data); 48 }); 49 } 50 51 protected abstract void writeLine(const(char)[] ln); 52 } 53 54 55 final class HTTPConsoleLogger : HTTPLogger { 56 @safe: 57 58 this(HTTPServerSettings settings, string format) 59 { 60 super(settings, format); 61 } 62 63 protected override void writeLine(const(char)[] ln) 64 { 65 logInfo("%s", ln); 66 } 67 } 68 69 70 final class HTTPFileLogger : HTTPLogger { 71 @safe: 72 73 private { 74 FileStream m_stream; 75 } 76 77 this(HTTPServerSettings settings, string format, string filename) 78 { 79 m_stream = openFile(filename, FileMode.append); 80 super(settings, format); 81 } 82 83 override void close() 84 { 85 m_stream.close(); 86 m_stream = FileStream.init; 87 } 88 89 protected override void writeLine(const(char)[] ln) 90 { 91 assert(!!m_stream); 92 m_stream.write(ln); 93 m_stream.write("\n"); 94 m_stream.flush(); 95 } 96 } 97 98 void formatApacheLog(R)(ref R ln, string format, scope HTTPServerRequest req, scope HTTPServerResponse res, in HTTPServerSettings settings) 99 @safe { 100 import std.format : formattedWrite; 101 enum State {Init, Directive, Status, Key, Command} 102 103 State state = State.Init; 104 bool conditional = false; 105 bool negate = false; 106 bool match = false; 107 string statusStr; 108 string key = ""; 109 while( format.length > 0 ) { 110 final switch(state) { 111 case State.Init: 112 auto idx = format.indexOf('%'); 113 if( idx < 0 ) { 114 ln.put( format ); 115 format = ""; 116 } else { 117 ln.put( format[0 .. idx] ); 118 format = format[idx+1 .. $]; 119 120 state = State.Directive; 121 } 122 break; 123 case State.Directive: 124 if( format[0] == '!' ) { 125 conditional = true; 126 negate = true; 127 format = format[1 .. $]; 128 state = State.Status; 129 } else if( format[0] == '%' ) { 130 ln.put("%"); 131 format = format[1 .. $]; 132 state = State.Init; 133 } else if( format[0] == '{' ) { 134 format = format[1 .. $]; 135 state = State.Key; 136 } else if( format[0] >= '0' && format[0] <= '9' ) { 137 conditional = true; 138 state = State.Status; 139 } else { 140 state = State.Command; 141 } 142 break; 143 case State.Status: 144 if( format[0] >= '0' && format[0] <= '9' ) { 145 statusStr ~= format[0]; 146 format = format[1 .. $]; 147 } else if( format[0] == ',' ) { 148 statusStr = ""; 149 format = format[1 .. $]; 150 } else if( format[0] == '{' ) { 151 format = format[1 .. $]; 152 state = State.Key; 153 } else { 154 state = State.Command; 155 } 156 if (statusStr.length == 3 && !match) { 157 auto status = parse!int(statusStr); 158 match = status == res.statusCode; 159 } 160 break; 161 case State.Key: 162 auto idx = format.indexOf('}'); 163 enforce(idx > -1, "Missing '}'"); 164 key = format[0 .. idx]; 165 format = format[idx+1 .. $]; 166 state = State.Command; 167 break; 168 case State.Command: 169 if( conditional && negate == match ) { 170 ln.put('-'); 171 format = format[1 .. $]; 172 state = State.Init; 173 break; 174 } 175 switch(format[0]) { 176 case 'a': //Remote IP-address 177 ln.put(req.peer); 178 break; 179 //TODO case 'A': //Local IP-address 180 //case 'B': //Size of Response in bytes, excluding headers 181 case 'b': //same as 'B' but a '-' is written if no bytes where sent 182 if (!res.bytesWritten) ln.put('-'); 183 else formattedWrite(() @trusted { return &ln; } (), "%s", res.bytesWritten); 184 break; 185 case 'C': //Cookie content {cookie} 186 import std.algorithm : joiner; 187 enforce(key != "", "cookie name missing"); 188 auto values = req.cookies.getAll(key); 189 if (values.length) ln.formattedWrite("%s", values.joiner(";")); 190 else ln.put("-"); 191 break; 192 case 'D': //The time taken to serve the request 193 auto d = res.timeFinalized - req.timeCreated; 194 formattedWrite(() @trusted { return &ln; } (), "%s", d.total!"msecs"()); 195 break; 196 //case 'e': //Environment variable {variable} 197 //case 'f': //Filename 198 case 'h': //Remote host 199 ln.put(req.peer); 200 break; 201 case 'H': //The request protocol 202 ln.put("HTTP"); 203 break; 204 case 'i': //Request header {header} 205 enforce(key != "", "header name missing"); 206 if (auto pv = key in req.headers) ln.put(*pv); 207 else ln.put("-"); 208 break; 209 case 'm': //Request method 210 ln.put(httpMethodString(req.method)); 211 break; 212 case 'o': //Response header {header} 213 enforce(key != "", "header name missing"); 214 if( auto pv = key in res.headers ) ln.put(*pv); 215 else ln.put("-"); 216 break; 217 case 'p': //port 218 formattedWrite(() @trusted { return &ln; } (), "%s", settings.port); 219 break; 220 //case 'P': //Process ID 221 case 'q': //query string (with prepending '?') 222 ln.put("?"); 223 ln.put(req.queryString); 224 break; 225 case 'r': //First line of Request 226 ln.put(httpMethodString(req.method)); 227 ln.put(' '); 228 ln.put(req.requestURL); 229 ln.put(' '); 230 ln.put(getHTTPVersionString(req.httpVersion)); 231 break; 232 case 's': //Status 233 formattedWrite(() @trusted { return &ln; } (), "%s", res.statusCode); 234 break; 235 case 't': //Time the request was received {format} 236 ln.put(req.timeCreated.toSimpleString()); 237 break; 238 case 'T': //Time taken to server the request in seconds 239 auto d = res.timeFinalized - req.timeCreated; 240 formattedWrite(() @trusted { return &ln; } (), "%s", d.total!"seconds"); 241 break; 242 case 'u': //Remote user 243 ln.put(req.username.length ? req.username : "-"); 244 break; 245 case 'U': //The URL path without query string 246 ln.put(req.requestPath.toString()); 247 break; 248 case 'v': //Server name 249 ln.put(req.host.length ? req.host : "-"); 250 break; 251 default: 252 throw new Exception("Unknown directive '" ~ format[0] ~ "' in log format string"); 253 } 254 state = State.Init; 255 format = format[1 .. $]; 256 break; 257 } 258 } 259 }