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 }