/
audit.js
322 lines (296 loc) · 9.86 KB
/
audit.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var assert = require('assert-plus');
var bunyan = require('bunyan');
var HttpError = require('restify-errors').HttpError;
var VError = require('verror');
var hrTimeDurationInMs = require('./utils/hrTimeDurationInMs');
/**
* Utility to get response headers from a given response.
* Manually generates a POJO from `res.getHeaderNames` and `res.getHeader`,
* if available, falling back to deprecated `res._headers`, otherwise.
* Intentionally does not use `res.getHeaders` to avoid deserialization
* issues with object returned by that method.
*
* @param {http.ServerResponse} res - the OutgoingMessage
* @private
* @function getResponseHeaders
* @returns {object} map from header name to header value
* @see https://github.com/restify/node-restify/issues/1370
*/
function getResponseHeaders(res) {
if (res.getHeaderNames && res.getHeader) {
return res.getHeaderNames().reduce(function reduce(prev, curr) {
var header = {};
header[curr] = res.getHeader(curr);
return Object.assign({}, prev, header);
}, {});
}
return res._headers;
}
///--- API
/**
* @public
* @function auditLogger
* @param {Object} opts - The options object.
* @param {Object} opts.log - The logger.
* @param {String} opts.event - The event from the server which initiates the
* log, one of 'pre', 'routed', or 'after'
* @param {Function} [opts.context] - The optional context function of signature
* f(req, res, route, err). Invoked each time an audit log is generated. This
* function can return an object that customizes the format of anything off the
* req, res, route, and err objects. The output of this function will be
* available on the `context` key in the audit object.
* @param {Object} [opts.server] - The restify server, used to emit
* the audit log object programmatically
* @param {boolean} [opts.printLog=true] - Whether to print the log
* via the logger.
* @param {Object} [opts.serializers] - Override the default logger serializers
* for err, req and res
* @returns {Function} Handler
* @fires audit when an audit log has been generated
* @example
* <caption>
* Audit logging is a special plugin, as you don't use it with `.use()`
* but with the `after` event:
* </caption>
*
* server.on('after', restify.plugins.auditLogger({
* log: bunyan.createLogger({
* name: 'audit',
* stream: process.stdout
* }),
* event: 'after',
* server: SERVER,
* logMetrics : logBuffer,
* printLog : true
* }));
*
* @example
* <caption>
* You pass in the auditor a bunyan logger, optionally server object,
* Ringbuffer and a flag printLog indicate if log needs to be print out at info
* level or not. By default, without specify printLog flag, it will write out
* record lookling like this:
* </caption>
*
* {
* "name": "audit",
* "hostname": "your.host.name",
* "audit": true,
* "remoteAddress": "127.0.0.1",
* "remotePort": 57692,
* "req_id": "ed634c3e-1af0-40e4-ad1e-68c2fb67c8e1",
* "req": {
* "method": "GET",
* "url": "/foo",
* "headers": {
* "authorization": "Basic YWRtaW46am95cGFzczEyMw==",
* "user-agent": "curl/7.19.7 (universal-apple-darwin10.0)
* libcurl/7.19.7 OpenSSL/0.9.8r zlib/1.2.3",
* "host": "localhost:8080",
* "accept": "application/json"
* },
* "httpVersion": "1.1",
* "query": {
* "foo": "bar"
* },
* "trailers": {},
* "version": "*",
* "timers": {
* "bunyan": 52,
* "saveAction": 8,
* "reqResTracker": 213,
* "addContext": 8,
* "addModels": 4,
* "resNamespaces": 5,
* "parseQueryString": 11,
* "instanceHeaders": 20,
* "xForwardedProto": 7,
* "httpsRedirector": 14,
* "readBody": 21,
* "parseBody": 6,
* "xframe": 7,
* "restifyCookieParser": 15,
* "fooHandler": 23,
* "barHandler": 14,
* "carHandler": 14
* }
* },
* "res": {
* "statusCode": 200,
* "headers": {
* "access-control-allow-origin": "*",
* "access-control-allow-headers": "Accept, Accept-Version,
* Content-Length, Content-MD5, Content-Type, Date, Api-Version",
* "access-control-expose-headers": "Api-Version, Request-Id,
* Response-Time",
* "server": "Joyent SmartDataCenter 7.0.0",
* "x-request-id": "ed634c3e-1af0-40e4-ad1e-68c2fb67c8e1",
* "access-control-allow-methods": "GET",
* "x-api-version": "1.0.0",
* "connection": "close",
* "content-length": 158,
* "content-md5": "zkiRn2/k3saflPhxXI7aXA==",
* "content-type": "application/json",
* "date": "Tue, 07 Feb 2012 20:30:31 GMT",
* "x-response-time": 1639
* },
* "trailer": false
* },
* "route": {
* "name": "GetFoo",
* "version": ["1.0.0"]
* },
* "secure": false,
* "level": 30,
* "msg": "GetFoo handled: 200",
* "time": "2012-02-07T20:30:31.896Z",
* "v": 0
* }
*
* @example
* <caption>
* The `timers` field shows the time each handler took to run in microseconds.
* Restify by default will record this information for every handler for each
* route. However, if you decide to include nested handlers, you can track the
* timing yourself by utilizing the Request
* [startHandlerTimer](#starthandlertimerhandlername) and
* [endHandlerTimer](#endhandlertimerhandlername) API.
* You can also listen to auditlog event and get same above log object when
* log event emits. For example
* </caption>
* SERVER.on('auditlog', function (data) {
* //do some process with log
* });
*
*/
function auditLogger(opts) {
assert.object(opts, 'opts');
assert.object(opts.log, 'opts.log');
assert.string(opts.event, 'opts.event');
assert.optionalFunc(opts.context, 'opts.context');
assert.optionalObject(opts.server, 'opts.server');
assert.optionalBool(opts.printLog, 'opts.printLog');
assert.optionalObject(opts.serializers, 'opts.serializers');
if (
opts.event !== 'after' &&
opts.event !== 'pre' &&
opts.event !== 'routed'
) {
throw new VError(
'opts.event must be %s, %s, or %s, but is %s',
'pre',
'routed',
'after',
opts.event
);
}
var server = opts.server;
var printLog = opts.printLog;
if (typeof printLog === 'undefined') {
printLog = true;
}
var errSerializer = bunyan.stdSerializers.err;
// don't break legacy use, where there was no top level opts.serializer
if (opts.log.serializers && opts.log.serializers.err) {
errSerializer = opts.log.serializers.err;
}
var DEFAULT_SERIALIZERS = {
err: errSerializer,
req: function auditRequestSerializer(req) {
if (!req) {
return false;
}
var timers = {};
(req.timers || []).forEach(function forEach(time) {
var t = time.time;
var _t = Math.floor(1000000 * t[0] + t[1] / 1000);
timers[time.name] = (timers[time.name] || 0) + _t;
});
return {
// account for native and queryParser plugin usage
query:
typeof req.query === 'function' ? req.query() : req.query,
method: req.method,
url: req.url,
headers: req.headers,
httpVersion: req.httpVersion,
trailers: req.trailers,
version: req.version(),
body: opts.body === true ? req.body : undefined,
timers: timers,
connectionState: req.connectionState && req.connectionState()
};
},
res: function auditResponseSerializer(res) {
if (!res) {
return false;
}
var body;
if (opts.body === true) {
if (res._body instanceof HttpError) {
body = res._body.body;
} else {
body = res._body;
}
}
return {
statusCode: res.statusCode,
headers: getResponseHeaders(res),
trailer: res._trailer || false,
body: body
};
}
};
var serializers = Object.assign({}, DEFAULT_SERIALIZERS, opts.serializers);
var log = opts.log.child({
audit: true,
component: opts.event,
serializers: serializers
});
function audit(req, res, route, err) {
var latency = res.get('Response-Time');
if (typeof latency !== 'number') {
latency = hrTimeDurationInMs(req._timeStart, req._timeFlushed);
}
var obj = {
remoteAddress: req.connection.remoteAddress,
remotePort: req.connection.remotePort,
req_id: req.getId(),
req: req,
res: res,
err: err,
latency: latency,
secure: req.secure,
_audit: true,
event: opts.event
};
// run through the custom context function
if (opts.context) {
obj.context = opts.context(req, res, route, err);
}
if (printLog) {
switch (opts.event) {
case 'after':
log.info(obj, 'handled: %d', res.statusCode);
break;
case 'pre':
log.info(obj, 'pre');
break;
case 'routed':
log.info(obj, 'routed');
break;
default:
throw new Error('Unexpected audit event: ' + opts.event);
}
}
if (server) {
server.emit('audit', obj);
}
return true;
}
return audit;
}
///-- Exports
module.exports = auditLogger;