connect-logger.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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._headers && res._headers['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._headers ?
  88. (res._headers[field.toLowerCase()] || res.__headers[field])
  89. : (res.__headers && res.__headers[field]);
  90. }
  91. });
  92. return arrayUniqueTokens(customTokens.concat(defaultTokens));
  93. }
  94. /**
  95. * Return formatted log line.
  96. *
  97. * @param {String} str
  98. * @param {Array} tokens
  99. * @return {String}
  100. * @api private
  101. */
  102. function format(str, tokens) {
  103. for (let i = 0; i < tokens.length; i++) {
  104. str = str.replace(tokens[i].token, tokens[i].replacement);
  105. }
  106. return str;
  107. }
  108. /**
  109. * Return RegExp Object about nolog
  110. *
  111. * @param {String|Array} nolog
  112. * @return {RegExp}
  113. * @api private
  114. *
  115. * syntax
  116. * 1. String
  117. * 1.1 "\\.gif"
  118. * NOT LOGGING http://example.com/hoge.gif and http://example.com/hoge.gif?fuga
  119. * LOGGING http://example.com/hoge.agif
  120. * 1.2 in "\\.gif|\\.jpg$"
  121. * NOT LOGGING http://example.com/hoge.gif and
  122. * http://example.com/hoge.gif?fuga and http://example.com/hoge.jpg?fuga
  123. * LOGGING http://example.com/hoge.agif,
  124. * http://example.com/hoge.ajpg and http://example.com/hoge.jpg?hoge
  125. * 1.3 in "\\.(gif|jpe?g|png)$"
  126. * NOT LOGGING http://example.com/hoge.gif and http://example.com/hoge.jpeg
  127. * LOGGING http://example.com/hoge.gif?uid=2 and http://example.com/hoge.jpg?pid=3
  128. * 2. RegExp
  129. * 2.1 in /\.(gif|jpe?g|png)$/
  130. * SAME AS 1.3
  131. * 3. Array
  132. * 3.1 ["\\.jpg$", "\\.png", "\\.gif"]
  133. * SAME AS "\\.jpg|\\.png|\\.gif"
  134. */
  135. function createNoLogCondition(nolog) {
  136. let regexp = null;
  137. if (nolog) {
  138. if (nolog instanceof RegExp) {
  139. regexp = nolog;
  140. }
  141. if (typeof nolog === 'string') {
  142. regexp = new RegExp(nolog);
  143. }
  144. if (Array.isArray(nolog)) {
  145. // convert to strings
  146. const regexpsAsStrings = nolog.map(reg => (reg.source ? reg.source : reg));
  147. regexp = new RegExp(regexpsAsStrings.join('|'));
  148. }
  149. }
  150. return regexp;
  151. }
  152. /**
  153. * Log requests with the given `options` or a `format` string.
  154. *
  155. * Options:
  156. *
  157. * - `format` Format string, see below for tokens
  158. * - `level` A log4js levels instance. Supports also 'auto'
  159. * - `nolog` A string or RegExp to exclude target logs
  160. *
  161. * Tokens:
  162. *
  163. * - `:req[header]` ex: `:req[Accept]`
  164. * - `:res[header]` ex: `:res[Content-Length]`
  165. * - `:http-version`
  166. * - `:response-time`
  167. * - `:remote-addr`
  168. * - `:date`
  169. * - `:method`
  170. * - `:url`
  171. * - `:referrer`
  172. * - `:user-agent`
  173. * - `:status`
  174. *
  175. * @return {Function}
  176. * @param logger4js
  177. * @param options
  178. * @api public
  179. */
  180. module.exports = function getLogger(logger4js, options) {
  181. /* eslint no-underscore-dangle:0 */
  182. if (typeof options === 'object') {
  183. options = options || {};
  184. } else if (options) {
  185. options = { format: options };
  186. } else {
  187. options = {};
  188. }
  189. const thisLogger = logger4js;
  190. let level = levels.getLevel(options.level, levels.INFO);
  191. const fmt = options.format || DEFAULT_FORMAT;
  192. const nolog = options.nolog ? createNoLogCondition(options.nolog) : null;
  193. return (req, res, next) => {
  194. // mount safety
  195. if (req._logging) return next();
  196. // nologs
  197. if (nolog && nolog.test(req.originalUrl)) return next();
  198. if (thisLogger.isLevelEnabled(level) || options.level === 'auto') {
  199. const start = new Date();
  200. const writeHead = res.writeHead;
  201. // flag as logging
  202. req._logging = true;
  203. // proxy for statusCode.
  204. res.writeHead = (code, headers) => {
  205. res.writeHead = writeHead;
  206. res.writeHead(code, headers);
  207. res.__statusCode = code;
  208. res.__headers = headers || {};
  209. // status code response level handling
  210. if (options.level === 'auto') {
  211. level = levels.INFO;
  212. if (code >= 300) level = levels.WARN;
  213. if (code >= 400) level = levels.ERROR;
  214. } else {
  215. level = levels.getLevel(options.level, levels.INFO);
  216. }
  217. };
  218. // hook on end request to emit the log entry of the HTTP request.
  219. res.on('finish', () => {
  220. res.responseTime = new Date() - start;
  221. // status code response level handling
  222. if (res.statusCode && options.level === 'auto') {
  223. level = levels.INFO;
  224. if (res.statusCode >= 300) level = levels.WARN;
  225. if (res.statusCode >= 400) level = levels.ERROR;
  226. }
  227. if (thisLogger.isLevelEnabled(level)) {
  228. const combinedTokens = assembleTokens(req, res, options.tokens || []);
  229. if (typeof fmt === 'function') {
  230. const line = fmt(req, res, str => format(str, combinedTokens));
  231. if (line) thisLogger.log(level, line);
  232. } else {
  233. thisLogger.log(level, format(fmt, combinedTokens));
  234. }
  235. }
  236. });
  237. }
  238. // ensure next gets always called
  239. return next();
  240. };
  241. };