1 | /* eslint-disable no-plusplus */
|
---|
2 |
|
---|
3 | const levels = require("./levels");
|
---|
4 |
|
---|
5 | const DEFAULT_FORMAT =
|
---|
6 | ":remote-addr - -" +
|
---|
7 | ' ":method :url HTTP/:http-version"' +
|
---|
8 | ' :status :content-length ":referrer"' +
|
---|
9 | ' ":user-agent"';
|
---|
10 |
|
---|
11 | /**
|
---|
12 | * Return request url path,
|
---|
13 | * adding this function prevents the Cyclomatic Complexity,
|
---|
14 | * for the assemble_tokens function at low, to pass the tests.
|
---|
15 | *
|
---|
16 | * @param {IncomingMessage} req
|
---|
17 | * @return {string}
|
---|
18 | * @api private
|
---|
19 | */
|
---|
20 | function getUrl(req) {
|
---|
21 | return req.originalUrl || req.url;
|
---|
22 | }
|
---|
23 |
|
---|
24 | /**
|
---|
25 | * Adds custom {token, replacement} objects to defaults,
|
---|
26 | * overwriting the defaults if any tokens clash
|
---|
27 | *
|
---|
28 | * @param {IncomingMessage} req
|
---|
29 | * @param {ServerResponse} res
|
---|
30 | * @param {Array} customTokens
|
---|
31 | * [{ token: string-or-regexp, replacement: string-or-replace-function }]
|
---|
32 | * @return {Array}
|
---|
33 | */
|
---|
34 | function assembleTokens(req, res, customTokens) {
|
---|
35 | const arrayUniqueTokens = array => {
|
---|
36 | const a = array.concat();
|
---|
37 | for (let i = 0; i < a.length; ++i) {
|
---|
38 | for (let j = i + 1; j < a.length; ++j) {
|
---|
39 | // not === because token can be regexp object
|
---|
40 | /* eslint eqeqeq:0 */
|
---|
41 | if (a[i].token == a[j].token) {
|
---|
42 | a.splice(j--, 1);
|
---|
43 | }
|
---|
44 | }
|
---|
45 | }
|
---|
46 | return a;
|
---|
47 | };
|
---|
48 |
|
---|
49 | const defaultTokens = [];
|
---|
50 | defaultTokens.push({ token: ":url", replacement: getUrl(req) });
|
---|
51 | defaultTokens.push({ token: ":protocol", replacement: req.protocol });
|
---|
52 | defaultTokens.push({ token: ":hostname", replacement: req.hostname });
|
---|
53 | defaultTokens.push({ token: ":method", replacement: req.method });
|
---|
54 | defaultTokens.push({
|
---|
55 | token: ":status",
|
---|
56 | replacement: res.__statusCode || res.statusCode
|
---|
57 | });
|
---|
58 | defaultTokens.push({
|
---|
59 | token: ":response-time",
|
---|
60 | replacement: res.responseTime
|
---|
61 | });
|
---|
62 | defaultTokens.push({ token: ":date", replacement: new Date().toUTCString() });
|
---|
63 | defaultTokens.push({
|
---|
64 | token: ":referrer",
|
---|
65 | replacement: req.headers.referer || req.headers.referrer || ""
|
---|
66 | });
|
---|
67 | defaultTokens.push({
|
---|
68 | token: ":http-version",
|
---|
69 | replacement: `${req.httpVersionMajor}.${req.httpVersionMinor}`
|
---|
70 | });
|
---|
71 | defaultTokens.push({
|
---|
72 | token: ":remote-addr",
|
---|
73 | replacement:
|
---|
74 | req.headers["x-forwarded-for"] ||
|
---|
75 | req.ip ||
|
---|
76 | req._remoteAddress ||
|
---|
77 | (req.socket &&
|
---|
78 | (req.socket.remoteAddress ||
|
---|
79 | (req.socket.socket && req.socket.socket.remoteAddress)))
|
---|
80 | });
|
---|
81 | defaultTokens.push({
|
---|
82 | token: ":user-agent",
|
---|
83 | replacement: req.headers["user-agent"]
|
---|
84 | });
|
---|
85 | defaultTokens.push({
|
---|
86 | token: ":content-length",
|
---|
87 | replacement:
|
---|
88 | res.getHeader("content-length") ||
|
---|
89 | (res.__headers && res.__headers["Content-Length"]) ||
|
---|
90 | "-"
|
---|
91 | });
|
---|
92 | defaultTokens.push({
|
---|
93 | token: /:req\[([^\]]+)]/g,
|
---|
94 | replacement(_, field) {
|
---|
95 | return req.headers[field.toLowerCase()];
|
---|
96 | }
|
---|
97 | });
|
---|
98 | defaultTokens.push({
|
---|
99 | token: /:res\[([^\]]+)]/g,
|
---|
100 | replacement(_, field) {
|
---|
101 | return (
|
---|
102 | res.getHeader(field.toLowerCase()) ||
|
---|
103 | (res.__headers && res.__headers[field])
|
---|
104 | );
|
---|
105 | }
|
---|
106 | });
|
---|
107 |
|
---|
108 | return arrayUniqueTokens(customTokens.concat(defaultTokens));
|
---|
109 | }
|
---|
110 |
|
---|
111 | /**
|
---|
112 | * Return formatted log line.
|
---|
113 | *
|
---|
114 | * @param {string} str
|
---|
115 | * @param {Array} tokens
|
---|
116 | * @return {string}
|
---|
117 | * @api private
|
---|
118 | */
|
---|
119 | function format(str, tokens) {
|
---|
120 | for (let i = 0; i < tokens.length; i++) {
|
---|
121 | str = str.replace(tokens[i].token, tokens[i].replacement);
|
---|
122 | }
|
---|
123 | return str;
|
---|
124 | }
|
---|
125 |
|
---|
126 | /**
|
---|
127 | * Return RegExp Object about nolog
|
---|
128 | *
|
---|
129 | * @param {(string|Array)} nolog
|
---|
130 | * @return {RegExp}
|
---|
131 | * @api private
|
---|
132 | *
|
---|
133 | * syntax
|
---|
134 | * 1. String
|
---|
135 | * 1.1 "\\.gif"
|
---|
136 | * NOT LOGGING http://example.com/hoge.gif and http://example.com/hoge.gif?fuga
|
---|
137 | * LOGGING http://example.com/hoge.agif
|
---|
138 | * 1.2 in "\\.gif|\\.jpg$"
|
---|
139 | * NOT LOGGING http://example.com/hoge.gif and
|
---|
140 | * http://example.com/hoge.gif?fuga and http://example.com/hoge.jpg?fuga
|
---|
141 | * LOGGING http://example.com/hoge.agif,
|
---|
142 | * http://example.com/hoge.ajpg and http://example.com/hoge.jpg?hoge
|
---|
143 | * 1.3 in "\\.(gif|jpe?g|png)$"
|
---|
144 | * NOT LOGGING http://example.com/hoge.gif and http://example.com/hoge.jpeg
|
---|
145 | * LOGGING http://example.com/hoge.gif?uid=2 and http://example.com/hoge.jpg?pid=3
|
---|
146 | * 2. RegExp
|
---|
147 | * 2.1 in /\.(gif|jpe?g|png)$/
|
---|
148 | * SAME AS 1.3
|
---|
149 | * 3. Array
|
---|
150 | * 3.1 ["\\.jpg$", "\\.png", "\\.gif"]
|
---|
151 | * SAME AS "\\.jpg|\\.png|\\.gif"
|
---|
152 | */
|
---|
153 | function createNoLogCondition(nolog) {
|
---|
154 | let regexp = null;
|
---|
155 |
|
---|
156 | if (nolog instanceof RegExp) {
|
---|
157 | regexp = nolog;
|
---|
158 | }
|
---|
159 |
|
---|
160 | if (typeof nolog === "string") {
|
---|
161 | regexp = new RegExp(nolog);
|
---|
162 | }
|
---|
163 |
|
---|
164 | if (Array.isArray(nolog)) {
|
---|
165 | // convert to strings
|
---|
166 | const regexpsAsStrings = nolog.map(reg => (reg.source ? reg.source : reg));
|
---|
167 | regexp = new RegExp(regexpsAsStrings.join("|"));
|
---|
168 | }
|
---|
169 |
|
---|
170 | return regexp;
|
---|
171 | }
|
---|
172 |
|
---|
173 | /**
|
---|
174 | * Allows users to define rules around status codes to assign them to a specific
|
---|
175 | * logging level.
|
---|
176 | * There are two types of rules:
|
---|
177 | * - RANGE: matches a code within a certain range
|
---|
178 | * E.g. { 'from': 200, 'to': 299, 'level': 'info' }
|
---|
179 | * - CONTAINS: matches a code to a set of expected codes
|
---|
180 | * E.g. { 'codes': [200, 203], 'level': 'debug' }
|
---|
181 | * Note*: Rules are respected only in order of prescendence.
|
---|
182 | *
|
---|
183 | * @param {Number} statusCode
|
---|
184 | * @param {Level} currentLevel
|
---|
185 | * @param {Object} ruleSet
|
---|
186 | * @return {Level}
|
---|
187 | * @api private
|
---|
188 | */
|
---|
189 | function matchRules(statusCode, currentLevel, ruleSet) {
|
---|
190 | let level = currentLevel;
|
---|
191 |
|
---|
192 | if (ruleSet) {
|
---|
193 | const matchedRule = ruleSet.find(rule => {
|
---|
194 | let ruleMatched = false;
|
---|
195 | if (rule.from && rule.to) {
|
---|
196 | ruleMatched = statusCode >= rule.from && statusCode <= rule.to;
|
---|
197 | } else {
|
---|
198 | ruleMatched = rule.codes.indexOf(statusCode) !== -1;
|
---|
199 | }
|
---|
200 | return ruleMatched;
|
---|
201 | });
|
---|
202 | if (matchedRule) {
|
---|
203 | level = levels.getLevel(matchedRule.level, level);
|
---|
204 | }
|
---|
205 | }
|
---|
206 | return level;
|
---|
207 | }
|
---|
208 |
|
---|
209 | /**
|
---|
210 | * Log requests with the given `options` or a `format` string.
|
---|
211 | *
|
---|
212 | * Options:
|
---|
213 | *
|
---|
214 | * - `format` Format string, see below for tokens
|
---|
215 | * - `level` A log4js levels instance. Supports also 'auto'
|
---|
216 | * - `nolog` A string or RegExp to exclude target logs
|
---|
217 | * - `statusRules` A array of rules for setting specific logging levels base on status codes
|
---|
218 | * - `context` Whether to add a response of express to the context
|
---|
219 | *
|
---|
220 | * Tokens:
|
---|
221 | *
|
---|
222 | * - `:req[header]` ex: `:req[Accept]`
|
---|
223 | * - `:res[header]` ex: `:res[Content-Length]`
|
---|
224 | * - `:http-version`
|
---|
225 | * - `:response-time`
|
---|
226 | * - `:remote-addr`
|
---|
227 | * - `:date`
|
---|
228 | * - `:method`
|
---|
229 | * - `:url`
|
---|
230 | * - `:referrer`
|
---|
231 | * - `:user-agent`
|
---|
232 | * - `:status`
|
---|
233 | *
|
---|
234 | * @return {Function}
|
---|
235 | * @param logger4js
|
---|
236 | * @param options
|
---|
237 | * @api public
|
---|
238 | */
|
---|
239 | module.exports = function getLogger(logger4js, options) {
|
---|
240 | /* eslint no-underscore-dangle:0 */
|
---|
241 | if (typeof options === "string" || typeof options === "function") {
|
---|
242 | options = { format: options };
|
---|
243 | } else {
|
---|
244 | options = options || {};
|
---|
245 | }
|
---|
246 |
|
---|
247 | const thisLogger = logger4js;
|
---|
248 | let level = levels.getLevel(options.level, levels.INFO);
|
---|
249 | const fmt = options.format || DEFAULT_FORMAT;
|
---|
250 | const nolog = createNoLogCondition(options.nolog);
|
---|
251 |
|
---|
252 | return (req, res, next) => {
|
---|
253 | // mount safety
|
---|
254 | if (req._logging) return next();
|
---|
255 |
|
---|
256 | // nologs
|
---|
257 | if (nolog && nolog.test(req.originalUrl)) return next();
|
---|
258 |
|
---|
259 | if (thisLogger.isLevelEnabled(level) || options.level === "auto") {
|
---|
260 | const start = new Date();
|
---|
261 | const { writeHead } = res;
|
---|
262 |
|
---|
263 | // flag as logging
|
---|
264 | req._logging = true;
|
---|
265 |
|
---|
266 | // proxy for statusCode.
|
---|
267 | res.writeHead = (code, headers) => {
|
---|
268 | res.writeHead = writeHead;
|
---|
269 | res.writeHead(code, headers);
|
---|
270 |
|
---|
271 | res.__statusCode = code;
|
---|
272 | res.__headers = headers || {};
|
---|
273 | };
|
---|
274 |
|
---|
275 | // hook on end request to emit the log entry of the HTTP request.
|
---|
276 | res.on("finish", () => {
|
---|
277 | res.responseTime = new Date() - start;
|
---|
278 | // status code response level handling
|
---|
279 | if (res.statusCode && options.level === "auto") {
|
---|
280 | level = levels.INFO;
|
---|
281 | if (res.statusCode >= 300) level = levels.WARN;
|
---|
282 | if (res.statusCode >= 400) level = levels.ERROR;
|
---|
283 | }
|
---|
284 | level = matchRules(res.statusCode, level, options.statusRules);
|
---|
285 |
|
---|
286 | const combinedTokens = assembleTokens(req, res, options.tokens || []);
|
---|
287 |
|
---|
288 | if (options.context) thisLogger.addContext("res", res);
|
---|
289 | if (typeof fmt === "function") {
|
---|
290 | const line = fmt(req, res, str => format(str, combinedTokens));
|
---|
291 | if (line) thisLogger.log(level, line);
|
---|
292 | } else {
|
---|
293 | thisLogger.log(level, format(fmt, combinedTokens));
|
---|
294 | }
|
---|
295 | if (options.context) thisLogger.removeContext("res");
|
---|
296 | });
|
---|
297 | }
|
---|
298 |
|
---|
299 | // ensure next gets always called
|
---|
300 | return next();
|
---|
301 | };
|
---|
302 | };
|
---|