connect-logger.js 8.6 KB

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