[6a3a178] | 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 | };
|
---|