UNPKG

10.8 kBJavaScriptView Raw
1/*!
2 * morgan
3 * Copyright(c) 2010 Sencha Inc.
4 * Copyright(c) 2011 TJ Holowaychuk
5 * Copyright(c) 2014 Jonathan Ong
6 * Copyright(c) 2014-2017 Douglas Christopher Wilson
7 * MIT Licensed
8 */
9
10'use strict'
11
12/**
13 * Module exports.
14 * @public
15 */
16
17module.exports = morgan
18module.exports.compile = compile
19module.exports.format = format
20module.exports.token = token
21
22/**
23 * Module dependencies.
24 * @private
25 */
26
27var auth = require('basic-auth')
28var debug = require('debug')('morgan')
29var deprecate = require('depd')('morgan')
30var onFinished = require('on-finished')
31var onHeaders = require('on-headers')
32
33/**
34 * Array of CLF month names.
35 * @private
36 */
37
38var CLF_MONTH = [
39 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
40 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
41]
42
43/**
44 * Default log buffer duration.
45 * @private
46 */
47
48var DEFAULT_BUFFER_DURATION = 1000
49
50/**
51 * Create a logger middleware.
52 *
53 * @public
54 * @param {String|Function} format
55 * @param {Object} [options]
56 * @return {Function} middleware
57 */
58
59function morgan (format, options) {
60 var fmt = format
61 var opts = options || {}
62
63 if (format && typeof format === 'object') {
64 opts = format
65 fmt = opts.format || 'default'
66
67 // smart deprecation message
68 deprecate('morgan(options): use morgan(' + (typeof fmt === 'string' ? JSON.stringify(fmt) : 'format') + ', options) instead')
69 }
70
71 if (fmt === undefined) {
72 deprecate('undefined format: specify a format')
73 }
74
75 // output on request instead of response
76 var immediate = opts.immediate
77
78 // check if log entry should be skipped
79 var skip = opts.skip || false
80
81 // format function
82 var formatLine = typeof fmt !== 'function'
83 ? getFormatFunction(fmt)
84 : fmt
85
86 // stream
87 var buffer = opts.buffer
88 var stream = opts.stream || process.stdout
89
90 // buffering support
91 if (buffer) {
92 deprecate('buffer option')
93
94 // flush interval
95 var interval = typeof buffer !== 'number'
96 ? DEFAULT_BUFFER_DURATION
97 : buffer
98
99 // swap the stream
100 stream = createBufferStream(stream, interval)
101 }
102
103 return function logger (req, res, next) {
104 // request data
105 req._startAt = undefined
106 req._startTime = undefined
107 req._remoteAddress = getip(req)
108
109 // response data
110 res._startAt = undefined
111 res._startTime = undefined
112
113 // record request start
114 recordStartTime.call(req)
115
116 function logRequest () {
117 if (skip !== false && skip(req, res)) {
118 debug('skip request')
119 return
120 }
121
122 var line = formatLine(morgan, req, res)
123
124 if (line == null) {
125 debug('skip line')
126 return
127 }
128
129 debug('log request')
130 stream.write(line + '\n')
131 };
132
133 if (immediate) {
134 // immediate log
135 logRequest()
136 } else {
137 // record response start
138 onHeaders(res, recordStartTime)
139
140 // log when response finished
141 onFinished(res, logRequest)
142 }
143
144 next()
145 }
146}
147
148/**
149 * Apache combined log format.
150 */
151
152morgan.format('combined', ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"')
153
154/**
155 * Apache common log format.
156 */
157
158morgan.format('common', ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length]')
159
160/**
161 * Default format.
162 */
163
164morgan.format('default', ':remote-addr - :remote-user [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"')
165deprecate.property(morgan, 'default', 'default format: use combined format')
166
167/**
168 * Short format.
169 */
170
171morgan.format('short', ':remote-addr :remote-user :method :url HTTP/:http-version :status :res[content-length] - :response-time ms')
172
173/**
174 * Tiny format.
175 */
176
177morgan.format('tiny', ':method :url :status :res[content-length] - :response-time ms')
178
179/**
180 * dev (colored)
181 */
182
183morgan.format('dev', function developmentFormatLine (tokens, req, res) {
184 // get the status code if response written
185 var status = headersSent(res)
186 ? res.statusCode
187 : undefined
188
189 // get status color
190 var color = status >= 500 ? 31 // red
191 : status >= 400 ? 33 // yellow
192 : status >= 300 ? 36 // cyan
193 : status >= 200 ? 32 // green
194 : 0 // no color
195
196 // get colored function
197 var fn = developmentFormatLine[color]
198
199 if (!fn) {
200 // compile
201 fn = developmentFormatLine[color] = compile('\x1b[0m:method :url \x1b[' +
202 color + 'm:status\x1b[0m :response-time ms - :res[content-length]\x1b[0m')
203 }
204
205 return fn(tokens, req, res)
206})
207
208/**
209 * request url
210 */
211
212morgan.token('url', function getUrlToken (req) {
213 return req.originalUrl || req.url
214})
215
216/**
217 * request method
218 */
219
220morgan.token('method', function getMethodToken (req) {
221 return req.method
222})
223
224/**
225 * response time in milliseconds
226 */
227
228morgan.token('response-time', function getResponseTimeToken (req, res, digits) {
229 if (!req._startAt || !res._startAt) {
230 // missing request and/or response start time
231 return
232 }
233
234 // calculate diff
235 var ms = (res._startAt[0] - req._startAt[0]) * 1e3 +
236 (res._startAt[1] - req._startAt[1]) * 1e-6
237
238 // return truncated value
239 return ms.toFixed(digits === undefined ? 3 : digits)
240})
241
242/**
243 * total time in milliseconds
244 */
245
246morgan.token('total-time', function getTotalTimeToken (req, res, digits) {
247 if (!req._startAt || !res._startAt) {
248 // missing request and/or response start time
249 return
250 }
251
252 // time elapsed from request start
253 var elapsed = process.hrtime(req._startAt)
254
255 // cover to milliseconds
256 var ms = (elapsed[0] * 1e3) + (elapsed[1] * 1e-6)
257
258 // return truncated value
259 return ms.toFixed(digits === undefined ? 3 : digits)
260})
261
262/**
263 * current date
264 */
265
266morgan.token('date', function getDateToken (req, res, format) {
267 var date = new Date()
268
269 switch (format || 'web') {
270 case 'clf':
271 return clfdate(date)
272 case 'iso':
273 return date.toISOString()
274 case 'web':
275 return date.toUTCString()
276 }
277})
278
279/**
280 * response status code
281 */
282
283morgan.token('status', function getStatusToken (req, res) {
284 return headersSent(res)
285 ? String(res.statusCode)
286 : undefined
287})
288
289/**
290 * normalized referrer
291 */
292
293morgan.token('referrer', function getReferrerToken (req) {
294 return req.headers.referer || req.headers.referrer
295})
296
297/**
298 * remote address
299 */
300
301morgan.token('remote-addr', getip)
302
303/**
304 * remote user
305 */
306
307morgan.token('remote-user', function getRemoteUserToken (req) {
308 // parse basic credentials
309 var credentials = auth(req)
310
311 // return username
312 return credentials
313 ? credentials.name
314 : undefined
315})
316
317/**
318 * HTTP version
319 */
320
321morgan.token('http-version', function getHttpVersionToken (req) {
322 return req.httpVersionMajor + '.' + req.httpVersionMinor
323})
324
325/**
326 * UA string
327 */
328
329morgan.token('user-agent', function getUserAgentToken (req) {
330 return req.headers['user-agent']
331})
332
333/**
334 * request header
335 */
336
337morgan.token('req', function getRequestToken (req, res, field) {
338 // get header
339 var header = req.headers[field.toLowerCase()]
340
341 return Array.isArray(header)
342 ? header.join(', ')
343 : header
344})
345
346/**
347 * response header
348 */
349
350morgan.token('res', function getResponseHeader (req, res, field) {
351 if (!headersSent(res)) {
352 return undefined
353 }
354
355 // get header
356 var header = res.getHeader(field)
357
358 return Array.isArray(header)
359 ? header.join(', ')
360 : header
361})
362
363/**
364 * Format a Date in the common log format.
365 *
366 * @private
367 * @param {Date} dateTime
368 * @return {string}
369 */
370
371function clfdate (dateTime) {
372 var date = dateTime.getUTCDate()
373 var hour = dateTime.getUTCHours()
374 var mins = dateTime.getUTCMinutes()
375 var secs = dateTime.getUTCSeconds()
376 var year = dateTime.getUTCFullYear()
377
378 var month = CLF_MONTH[dateTime.getUTCMonth()]
379
380 return pad2(date) + '/' + month + '/' + year +
381 ':' + pad2(hour) + ':' + pad2(mins) + ':' + pad2(secs) +
382 ' +0000'
383}
384
385/**
386 * Compile a format string into a function.
387 *
388 * @param {string} format
389 * @return {function}
390 * @public
391 */
392
393function compile (format) {
394 if (typeof format !== 'string') {
395 throw new TypeError('argument format must be a string')
396 }
397
398 var fmt = String(JSON.stringify(format))
399 var js = ' "use strict"\n return ' + fmt.replace(/:([-\w]{2,})(?:\[([^\]]+)\])?/g, function (_, name, arg) {
400 var tokenArguments = 'req, res'
401 var tokenFunction = 'tokens[' + String(JSON.stringify(name)) + ']'
402
403 if (arg !== undefined) {
404 tokenArguments += ', ' + String(JSON.stringify(arg))
405 }
406
407 return '" +\n (' + tokenFunction + '(' + tokenArguments + ') || "-") + "'
408 })
409
410 // eslint-disable-next-line no-new-func
411 return new Function('tokens, req, res', js)
412}
413
414/**
415 * Create a basic buffering stream.
416 *
417 * @param {object} stream
418 * @param {number} interval
419 * @public
420 */
421
422function createBufferStream (stream, interval) {
423 var buf = []
424 var timer = null
425
426 // flush function
427 function flush () {
428 timer = null
429 stream.write(buf.join(''))
430 buf.length = 0
431 }
432
433 // write function
434 function write (str) {
435 if (timer === null) {
436 timer = setTimeout(flush, interval)
437 }
438
439 buf.push(str)
440 }
441
442 // return a minimal "stream"
443 return { write: write }
444}
445
446/**
447 * Define a format with the given name.
448 *
449 * @param {string} name
450 * @param {string|function} fmt
451 * @public
452 */
453
454function format (name, fmt) {
455 morgan[name] = fmt
456 return this
457}
458
459/**
460 * Lookup and compile a named format function.
461 *
462 * @param {string} name
463 * @return {function}
464 * @public
465 */
466
467function getFormatFunction (name) {
468 // lookup format
469 var fmt = morgan[name] || name || morgan.default
470
471 // return compiled format
472 return typeof fmt !== 'function'
473 ? compile(fmt)
474 : fmt
475}
476
477/**
478 * Get request IP address.
479 *
480 * @private
481 * @param {IncomingMessage} req
482 * @return {string}
483 */
484
485function getip (req) {
486 return req.ip ||
487 req._remoteAddress ||
488 (req.connection && req.connection.remoteAddress) ||
489 undefined
490}
491
492/**
493 * Determine if the response headers have been sent.
494 *
495 * @param {object} res
496 * @returns {boolean}
497 * @private
498 */
499
500function headersSent (res) {
501 // istanbul ignore next: node.js 0.8 support
502 return typeof res.headersSent !== 'boolean'
503 ? Boolean(res._header)
504 : res.headersSent
505}
506
507/**
508 * Pad number to two digits.
509 *
510 * @private
511 * @param {number} num
512 * @return {string}
513 */
514
515function pad2 (num) {
516 var str = String(num)
517
518 // istanbul ignore next: num is current datetime
519 return (str.length === 1 ? '0' : '') + str
520}
521
522/**
523 * Record the start time.
524 * @private
525 */
526
527function recordStartTime () {
528 this._startAt = process.hrtime()
529 this._startTime = new Date()
530}
531
532/**
533 * Define a token function with the given name,
534 * and callback fn(req, res).
535 *
536 * @param {string} name
537 * @param {function} fn
538 * @public
539 */
540
541function token (name, fn) {
542 morgan[name] = fn
543 return this
544}