-
Notifications
You must be signed in to change notification settings - Fork 2
/
middleware.ts
317 lines (276 loc) · 8.69 KB
/
middleware.ts
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
/**
* Module dependencies.
* @private
*/
var fs = require("fs");
var path = require("path");
var kayvee = require("../lib/kayvee");
var KayveeLogger = require("../lib/logger/logger");
var morgan = require("morgan");
var _ = require("underscore");
/**
* all relative files path in a directory
*/
function walkDirSync(dir, files = []) {
const list = fs.readdirSync(dir);
list.forEach((file) => {
const f = path.join(dir, file);
if (fs.statSync(path.join(dir, file)).isDirectory()) {
walkDirSync(f, files);
} else {
files.push(f);
}
});
return files.map((f) => path.relative(dir, f));
}
/**
* returns a middleware function that checks if path exists in dir.
*
* Files in the directory are prefixed by base_path and compared to
* req.path
*/
function skip_path(dir, base_path = "/") {
let files = walkDirSync(dir);
files = files.map((file) => path.join(base_path, file));
console.error(`KayveeMiddleware: Skipping successful requests for files in ${dir} at ${base_path}`);
return (req, res) => _(files).contains(req.path) && res.statusCode < 400;
}
/**
* request path
*/
function getBaseUrl(req) {
var url = req.originalUrl || req.url;
var parsed = require("url").parse(url, true);
return parsed.pathname;
}
/**
* request query params
*/
function getQueryParams(req) {
var url = req.originalUrl || req.url;
var parsed = require("url").parse(url, true);
var parsedQueryString = require("qs").parse(parsed.search, {allowPrototypes: false, ignoreQueryPrefix: true});
return `?${require("qs").stringify(parsedQueryString)}`;
}
/**
* response size
*/
function getResponseSize(res) {
var result = undefined;
var headers = res.headers || res._headers;
if (headers && headers["content-length"]) {
result = Number(headers["content-length"]);
} else if (res.data) {
result = res.data.length;
}
return result;
}
/**
* response time in nanoseconds
*/
function getResponseTimeNs(req, res) {
if (!req._startAt || !res._startAt) {
// missing request and/or response start time
return undefined;
}
// calculate diff
var ns = (res._startAt[0] - req._startAt[0]) * 1e9
+ (res._startAt[1] - req._startAt[1]);
return ns;
}
/**
* IP address that sent the request.
*
* `req.ip` is defined in Express: http://expressjs.com/en/api.html#req.ip
*/
function getIp(req) {
var remoteAddress = req.connection ? req.connection.remoteAddress : undefined;
return req.ip || remoteAddress;
}
/**
* Log level
*/
function getLogLevel(req, res) {
const statusCode = res.statusCode;
let result;
if (statusCode >= 499) {
result = KayveeLogger.Error;
} else {
result = KayveeLogger.Info;
}
return result;
}
/**
* Get canary status
*/
function isCanary() {
return (process.env._CANARY === "1") || ("_POD_SHORTNAME" in process.env && process.env._POD_SHORTNAME.includes("-canary"));
}
/*
* Default handlers
*/
var defaultHandlers = [
// Request method
(req) => ({method: req.method}),
// Path (URL without query params)
(req) => ({path: getBaseUrl(req)}),
// Query params
(req) => ({params: getQueryParams(req)}),
// Response size
(req, res) => ({"response-size": getResponseSize(res)}),
// Response time (ns)
(req, res) => ({"response-time": getResponseTimeNs(req, res)}),
// Status code
(req, res) => ({"status-code": res.statusCode}),
// Ip address
(req) => ({ip: getIp(req)}),
// Via -- what library/code produced this log?
() => ({via: "kayvee-middleware"}),
// Kayvee's reserved fields
// Log level
(req, res) => ({level: getLogLevel(req, res)}),
// Source -- which app emitted this log?
// -> Gets passed in among `options` during library initialization
// Title
() => ({title: "request-finished"}),
// During the transition to pods, let's keep the canary field accurate
// whether it's in the canary pod or a canary container in homepod
() => ({canary: isCanary()}),
];
const defaultContextHandlers = [];
function handlerData(handlers, ...args) {
const data = {};
handlers.forEach((h) => {
try {
const handler_data = h(...args);
_.extend(data, handler_data);
} catch (e) {
// ignore invalid handler
}
});
return data;
}
class ContextLogger {
logger = null;
handlers = [];
args = [];
constructor(logger, handlers, ...args) {
this.logger = logger;
this.handlers = handlers;
this.args = args;
}
_contextualData(data) {
return _.extend(handlerData(this.handlers, ...this.args), data);
}
}
for (const func of KayveeLogger.LEVELS) {
ContextLogger.prototype[func] = function (title) {
this[`${func}D`](title, {});
};
ContextLogger.prototype[`${func}D`] = function (title, data) {
this.logger[`${func}D`](title, this._contextualData(data));
};
}
for (const func of KayveeLogger.METRICS) {
ContextLogger.prototype[func] = function (title, value) {
this[`${func}D`](title, value, {});
};
ContextLogger.prototype[`${func}D`] = function (title, value, data) {
this.logger[`${func}D`](title, value, this._contextualData(data));
};
}
/*
* User configuration is passed via an `options` object.
* Results from configuration are prioritized such that (`base_handlers` > `handlers` > `headers`).
*
* // `headers` - logs these request headers, if they exist
*
* headers: e.g. ['X-Request-Id', 'Host']
*
* // `handlers` - an array of functions of that return dicts to be logged.
*
* handlers: e.g. [function(request, response) { return {"key":"val"}]
*
* // `base_handlers` - an array of functions of that return dicts to be logged.
* // Barring exceptional circumstances, `base_handlers` should not be overriden by the user.
* // `base_handlers` defaults to a core set of handlers to run... see `defaultHandlers`.
* //
* // Separating `base_handlers` from `handlers` is done to ensure that reserved keys
* // don't accidentally get overriden by custom handlers. This can now only happen if
* // the user explicitly overrides `base_handlers`.
*
* base_handlers: e.g. [function(request, response) { return {"key":"val"}]
*
*/
var formatLine = (options_arg) => {
var options = options_arg || {};
// `source` is the one required field
if (!options.source) {
throw (Error("Missing required config for 'source' in Kayvee middleware 'options'"));
}
const router = KayveeLogger.getGlobalRouter();
return (tokens, req, res) => {
// Build a dict of data to log
var data = {_kvmeta: undefined}; // Adding _kvmeta here to make typescript compile happy
// Add user-configured request headers
var custom_headers = options.headers || [];
var header_data = {};
custom_headers.forEach((h) => {
// Header field names are case insensitive, so let's be consistent
var lc = h.toLowerCase();
header_data[lc] = req.headers[lc];
});
_.extend(data, header_data);
// Run user-configured handlers; add custom data
var custom_handlers = options.handlers || [];
// Allow user to override `base_handlers`; provide sane default set of handlers
var base_handlers = options.base_handlers || defaultHandlers;
base_handlers = base_handlers.concat([() => ({source: options.source})]);
// Execute custom-handlers THEN base-handlers
const all_handlers = custom_handlers.concat(base_handlers);
_.extend(data, handlerData(all_handlers, req, res));
if (router) {
data._kvmeta = router.route(data);
}
return kayvee.format(data);
};
};
const defaultContextLoggerOpts = {
enabled: true,
handlers: defaultContextHandlers,
};
/**
* Module exports.
* @public
*/
if (process.env.NODE_ENV === "test") {
module.exports = (clever_options, morgan_options = {skip: null}) => {
if (clever_options.ignore_dir) {
morgan_options.skip = skip_path(clever_options.ignore_dir.directory, clever_options.ignore_dir.path);
}
return morgan(formatLine(clever_options), morgan_options);
};
module.exports.ContextLogger = ContextLogger;
} else {
module.exports = (clever_options, context_logger_options = defaultContextLoggerOpts) => {
// `source` is the one required field
if (!clever_options.source) {
throw new Error("Missing required config for 'source' in Kayvee middleware 'options'");
}
const logger = new KayveeLogger(clever_options.source);
const morgan_options = {
stream: process.stderr,
skip: null,
};
if (clever_options.ignore_dir) {
morgan_options.skip = skip_path(clever_options.ignore_dir.directory, clever_options.ignore_dir.path);
}
const morgan_logger = morgan(formatLine(clever_options), morgan_options);
return (req, res, next) => {
if (context_logger_options.enabled) {
req.log = new ContextLogger(logger, context_logger_options.handlers, req);
}
morgan_logger(req, res, next);
};
};
}