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 }