1 /** 2 * Copyright © DiamondMVC 2018 3 * License: MIT (https://github.com/DiamondMVC/Diamond/blob/master/LICENSE) 4 * Author: Jacob Jensen (bausshf) 5 */ 6 module diamond.core.logging; 7 8 import diamond.core.apptype; 9 10 static if (loggingEnabled) 11 { 12 import diamond.http; 13 14 /// Alias for a logging delegate. 15 private alias LogDelegate = void delegate(LogResult); 16 17 /// Wrapper around a log result. 18 final class LogResult 19 { 20 private: 21 /// Unique token for the log. 22 string _logToken; 23 /// The log type. 24 LogType _logType; 25 /// The name of the application. 26 string _applicationName; 27 /// The ip address. 28 string _ipAddress; 29 /// The request method. 30 HttpMethod _requestMethod; 31 /// The request headers. 32 string _requestHeaders; 33 /// The request body. 34 string _requestBody; 35 /// The request url. 36 string _requestUrl; 37 /// The response headers. 38 string _responseHeaders; 39 /// The response body. 40 string _responseBody; 41 /// The response status code. 42 HttpStatus _responseStatusCode; 43 /// The message. 44 string _message; 45 /// The auth token. 46 string _authToken; 47 48 /** 49 * Creates a new log result. 50 * Params: 51 * logToken = The token of the log result. 52 * logType = The type of the log. 53 * applicationName = The name of the application. 54 * ipAddress = The ip address. 55 * requestMethod = The request method. 56 * requestHeaders = The request headers. 57 * requestBody = The request body. 58 * requestUrl = The request url. 59 * responseHeaders = The response headers. 60 * responseBody = The response body. 61 * responseStatusCode The response status code. 62 * message = The message. 63 */ 64 this 65 ( 66 string logToken, 67 LogType logType, 68 string applicationName, 69 string ipAddress, 70 HttpMethod requestMethod, 71 string requestHeaders, 72 string requestBody, 73 string requestUrl, 74 string responseHeaders, 75 string responseBody, 76 HttpStatus responseStatusCode, 77 string message, 78 string authToken 79 ) 80 { 81 _logToken = logToken; 82 _logType = logType; 83 _applicationName = applicationName; 84 _ipAddress = ipAddress; 85 _requestMethod = requestMethod; 86 _requestHeaders = requestHeaders; 87 _requestBody = requestBody; 88 _requestUrl = requestUrl; 89 _responseHeaders = responseHeaders; 90 _responseBody = responseBody; 91 _responseStatusCode = responseStatusCode; 92 _message = message; 93 _authToken = authToken; 94 } 95 96 public: 97 final: 98 @property 99 { 100 /// Gets the unique token for the log result. 101 string logToken() { return _logToken; } 102 103 /// Gets the type of the log. 104 LogType logType() { return _logType; } 105 106 /// Gets the name of the application. 107 string applicationName() { return _applicationName; } 108 109 /// Gets the ip address. 110 string ipAddress() { return _ipAddress; } 111 112 /// Gets the request method. 113 HttpMethod requestMethod() { return _requestMethod; } 114 115 /// Gets the request headers. 116 string requestHeaders() { return _requestHeaders; } 117 118 /// Gets the request body. 119 string requestBody() { return _requestBody; } 120 121 /// Gets the request url. 122 string requestUrl() { return _requestUrl; } 123 124 /// Gets the response headers. 125 string responseHeaders() { return _responseHeaders; } 126 127 /// Gets the response body. 128 string responseBody() { return _responseBody; } 129 130 /// Gets the response status code. 131 HttpStatus responseStatusCode() { return _responseStatusCode; } 132 133 /// Gets the message. 134 string message() { return _message; } 135 136 /// Gets the auth token. 137 string authToken() { return _authToken; } 138 } 139 140 /// Gets the log result as a loggable string, fit for ex. file-logs. 141 override string toString() 142 { 143 import std.datetime : Clock; 144 import std..string : format; 145 146 return `------------------- 147 --------- %s ---------- 148 ------------------- 149 Token: %s 150 LogType: %s 151 App: %s, 152 IPAddress: %s 153 Method: %s 154 Status: %s 155 ReqUrl: %s 156 AuthToken: %s 157 ReqHeaders: 158 ------------------- 159 %s 160 ------------------- 161 ReqBody: 162 ------------------- 163 %s 164 ------------------- 165 ResHeaders: 166 ------------------- 167 %s 168 ------------------- 169 ResBody: 170 ------------------- 171 %s 172 ------------------- 173 Message: 174 ------------------- 175 %s 176 ------------------- 177 -------------------`.format 178 ( 179 Clock.currTime().toString(), 180 logToken, logType, applicationName, 181 ipAddress, requestMethod, 182 responseStatusCode, requestUrl, 183 authToken, 184 requestHeaders, requestBody, 185 responseHeaders, responseBody, 186 message 187 ); 188 } 189 } 190 191 package(diamond) 192 { 193 /// Collection of loggers. 194 __gshared LogDelegate[][LogType] _loggers; 195 196 /** 197 * Executes a specific type of logger. 198 * Params: 199 * logType = The type of logger to execute. 200 * client = The client to log. 201 * message = The message associated with the log. 202 */ 203 void executeLog 204 ( 205 LogType logType, 206 HttpClient client, 207 lazy string message = null 208 ) 209 { 210 auto loggers = _loggers.get(logType, null); 211 212 if (!loggers) 213 { 214 return; 215 } 216 217 import std.algorithm : canFind; 218 import std..string : format; 219 import vibe.stream.operations : readAllUTF8; 220 import diamond.core.webconfig; 221 import diamond.core.senc; 222 223 string requestHeaders; 224 225 foreach (key,value; client.headers.byKeyValue()) 226 { 227 requestHeaders ~= "%s: %s\r\n".format(key, value); 228 } 229 230 string responseHeaders; 231 232 foreach (key,value; client.rawResponse.headers) 233 { 234 responseHeaders ~= "%s: %s\r\n".format(key, value); 235 } 236 237 import std.uuid : randomUUID, sha1UUID; 238 import std.random : Xorshift192, unpredictableSeed; 239 240 Xorshift192 gen; 241 gen.seed(unpredictableSeed); 242 243 string logToken = 244 randomUUID(gen).toString() ~ "-" ~ sha1UUID(client.session.id).toString(); 245 246 auto statusCode = client.statusCode; 247 248 if (logType == LogType.error) 249 { 250 statusCode = HttpStatus.internalServerError; 251 } 252 else if (logType == LogType.notFound) 253 { 254 statusCode = HttpStatus.notFound; 255 } 256 else if (statusCode == HttpStatus.continue_) 257 { 258 statusCode = HttpStatus.ok; 259 } 260 261 auto logResult = new LogResult 262 ( 263 logToken, 264 logType, 265 webConfig.name, 266 client.ipAddress, 267 client.method, 268 requestHeaders ? requestHeaders : "", 269 client.requestStream.readAllUTF8(), 270 client.fullUrl.toString(), 271 responseHeaders, 272 client.rawResponse.contentType.canFind("text") ? 273 cast(string)client.getBody() : SENC.encode(client.getBody()), 274 statusCode, 275 message ? message : "", 276 client.cookies.hasAuthCookie() ? client.cookies.getAuthCookie() : "" 277 ); 278 279 foreach (logger; loggers) 280 { 281 logger(logResult); 282 } 283 } 284 } 285 286 /// Enumeration of log types. 287 enum LogType 288 { 289 /// An error logger. 290 error, 291 292 /// A not-found logger. 293 notFound, 294 295 /// A logger for pre-request handling. 296 before, 297 298 /// A logger for post-request handling. 299 after, 300 301 /// A logger for static files. 302 staticFile 303 } 304 305 /** 306 * Creates a logger. 307 * Params: 308 * logType = The type of the logger. 309 * logger = The logger handler. 310 */ 311 void log(LogType logType, void delegate(LogResult) logger) 312 { 313 _loggers[logType] ~= logger; 314 } 315 316 /** 317 * Creates a file logger. 318 * Params: 319 * logType = The type of the logger. 320 * file = The file append logs to. 321 * callback = An optional callback after the log has been written. 322 */ 323 void logToFile(LogType logType, string file, void delegate(LogResult) callback = null) 324 { 325 log(logType, 326 (result) 327 { 328 import std.file : append; 329 append(file, result.toString()); 330 331 if (callback !is null) 332 { 333 callback(result); 334 } 335 }); 336 } 337 338 /** 339 * Creates a database logger. 340 * The table must implement the following columns: 341 * logToken (VARCHAR) 342 * logType (ENUM ("error", "notFound", "after", "before", "staticFile")) 343 * applicationName (VARCHAR) 344 * authToken (VARCHAR) 345 * requestIPAddress (VARCHAR) 346 * requestMethod (VARCHAR) 347 * requestHeaders (TEXT) 348 * requestBody (TEXT) 349 * requestUrl (VARCHAR) 350 * responseHeaders (TEXT) 351 * responseBody (TEXT) 352 * responseStatusCode (INT) 353 * message (TEXT) 354 * timestamp (DATETIME) 355 * Params: 356 * logType = The type of the logger. 357 * table = The table to log entries to. 358 * callback = An optional callback after the log has been written. 359 * connectionString = A connection string to associate with the logging. If none is specified then it will use the default connection string. 360 */ 361 void logToDatabase(LogType logType, string table, void delegate(LogResult) callback = null, string connectionString = null) 362 { 363 log(logType, 364 (result) 365 { 366 import std..string : format; 367 368 auto sql = " 369 INSERT INTO `%s` 370 ( 371 `logToken`, 372 `logType`, 373 `applicationName`, 374 `authToken`, 375 `requestIPAddress`, `requestMethod`, `requestHeaders`, 376 `requestBody`, `requestUrl`, 377 `responseHeaders`, `responseBody`, `responseStatusCode`, 378 `message`, 379 `timestamp` 380 ) 381 VALUES 382 ( 383 @logToken, 384 @logType, 385 @applicationName, 386 @authToken, 387 @requestIPAddress, @requestMethod, @requestHeaders, 388 @requestBody, @requestUrl, 389 @responseHeaders, @responseBody, @responseStatusCode, 390 @message, 391 NOW() 392 )".format(table); 393 394 import std.conv : to; 395 import diamond.database; 396 397 auto params = getParams(); 398 params["logToken"] = result.logToken; 399 params["logType"] = to!string(result.logType); 400 params["applicationName"] = result.applicationName; 401 params["authToken"] = result.authToken; 402 params["requestIPAddress"] = result.ipAddress; 403 params["requestMethod"] = to!string(result.requestMethod); 404 params["requestHeaders"] = result.requestHeaders; 405 params["requestBody"] = result.requestBody; 406 params["requestUrl"] = result.requestUrl; 407 params["responseHeaders"] = result.responseHeaders; 408 params["responseBody"] = result.responseBody; 409 params["responseStatusCode"] = cast(int)result.responseStatusCode; 410 params["message"] = result.message; 411 412 MySql.execute(sql, params, connectionString); 413 414 if (callback !is null) 415 { 416 callback(result); 417 } 418 }); 419 } 420 }