UNPKG

10.2 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 * current date
244 */
245
246morgan.token('date', function getDateToken (req, res, format) {
247 var date = new Date()
248
249 switch (format || 'web') {
250 case 'clf':
251 return clfdate(date)
252 case 'iso':
253 return date.toISOString()
254 case 'web':
255 return date.toUTCString()
256 }
257})
258
259/**
260 * response status code
261 */
262
263morgan.token('status', function getStatusToken (req, res) {
264 return headersSent(res)
265 ? String(res.statusCode)
266 : undefined
267})
268
269/**
270 * normalized referrer
271 */
272
273morgan.token('referrer', function getReferrerToken (req) {
274 return req.headers['referer'] || req.headers['referrer']
275})
276
277/**
278 * remote address
279 */
280
281morgan.token('remote-addr', getip)
282
283/**
284 * remote user
285 */
286
287morgan.token('remote-user', function getRemoteUserToken (req) {
288 // parse basic credentials
289 var credentials = auth(req)
290
291 // return username
292 return credentials
293 ? credentials.name
294 : undefined
295})
296
297/**
298 * HTTP version
299 */
300
301morgan.token('http-version', function getHttpVersionToken (req) {
302 return req.httpVersionMajor + '.' + req.httpVersionMinor
303})
304
305/**
306 * UA string
307 */
308
309morgan.token('user-agent', function getUserAgentToken (req) {
310 return req.headers['user-agent']
311})
312
313/**
314 * request header
315 */
316
317morgan.token('req', function getRequestToken (req, res, field) {
318 // get header
319 var header = req.headers[field.toLowerCase()]
320
321 return Array.isArray(header)
322 ? header.join(', ')
323 : header
324})
325
326/**
327 * response header
328 */
329
330morgan.token('res', function getResponseHeader (req, res, field) {
331 if (!headersSent(res)) {
332 return undefined
333 }
334
335 // get header
336 var header = res.getHeader(field)
337
338 return Array.isArray(header)
339 ? header.join(', ')
340 : header
341})
342
343/**
344 * Format a Date in the common log format.
345 *
346 * @private
347 * @param {Date} dateTime
348 * @return {string}
349 */
350
351function clfdate (dateTime) {
352 var date = dateTime.getUTCDate()
353 var hour = dateTime.getUTCHours()
354 var mins = dateTime.getUTCMinutes()
355 var secs = dateTime.getUTCSeconds()
356 var year = dateTime.getUTCFullYear()
357
358 var month = CLF_MONTH[dateTime.getUTCMonth()]
359
360 return pad2(date) + '/' + month + '/' + year +
361 ':' + pad2(hour) + ':' + pad2(mins) + ':' + pad2(secs) +
362 ' +0000'
363}
364
365/**
366 * Compile a format string into a function.
367 *
368 * @param {string} format
369 * @return {function}
370 * @public
371 */
372
373function compile (format) {
374 if (typeof format !== 'string') {
375 throw new TypeError('argument format must be a string')
376 }
377
378 var fmt = String(JSON.stringify(format))
379 var js = ' "use strict"\n return ' + fmt.replace(/:([-\w]{2,})(?:\[([^\]]+)\])?/g, function (_, name, arg) {
380 var tokenArguments = 'req, res'
381 var tokenFunction = 'tokens[' + String(JSON.stringify(name)) + ']'
382
383 if (arg !== undefined) {
384 tokenArguments += ', ' + String(JSON.stringify(arg))
385 }
386
387 return '" +\n (' + tokenFunction + '(' + tokenArguments + ') || "-") + "'
388 })
389
390 // eslint-disable-next-line no-new-func
391 return new Function('tokens, req, res', js)
392}
393
394/**
395 * Create a basic buffering stream.
396 *
397 * @param {object} stream
398 * @param {number} interval
399 * @public
400 */
401
402function createBufferStream (stream, interval) {
403 var buf = []
404 var timer = null
405
406 // flush function
407 function flush () {
408 timer = null
409 stream.write(buf.join(''))
410 buf.length = 0
411 }
412
413 // write function
414 function write (str) {
415 if (timer === null) {
416 timer = setTimeout(flush, interval)
417 }
418
419 buf.push(str)
420 }
421
422 // return a minimal "stream"
423 return { write: write }
424}
425
426/**
427 * Define a format with the given name.
428 *
429 * @param {string} name
430 * @param {string|function} fmt
431 * @public
432 */
433
434function format (name, fmt) {
435 morgan[name] = fmt
436 return this
437}
438
439/**
440 * Lookup and compile a named format function.
441 *
442 * @param {string} name
443 * @return {function}
444 * @public
445 */
446
447function getFormatFunction (name) {
448 // lookup format
449 var fmt = morgan[name] || name || morgan.default
450
451 // return compiled format
452 return typeof fmt !== 'function'
453 ? compile(fmt)
454 : fmt
455}
456
457/**
458 * Get request IP address.
459 *
460 * @private
461 * @param {IncomingMessage} req
462 * @return {string}
463 */
464
465function getip (req) {
466 return req.ip ||
467 req._remoteAddress ||
468 (req.connection && req.connection.remoteAddress) ||
469 undefined
470}
471
472/**
473 * Determine if the response headers have been sent.
474 *
475 * @param {object} res
476 * @returns {boolean}
477 * @private
478 */
479
480function headersSent (res) {
481 return typeof res.headersSent !== 'boolean'
482 ? Boolean(res._header)
483 : res.headersSent
484}
485
486/**
487 * Pad number to two digits.
488 *
489 * @private
490 * @param {number} num
491 * @return {string}
492 */
493
494function pad2 (num) {
495 var str = String(num)
496
497 return (str.length === 1 ? '0' : '') + str
498}
499
500/**
501 * Record the start time.
502 * @private
503 */
504
505function recordStartTime () {
506 this._startAt = process.hrtime()
507 this._startTime = new Date()
508}
509
510/**
511 * Define a token function with the given name,
512 * and callback fn(req, res).
513 *
514 * @param {string} name
515 * @param {function} fn
516 * @public
517 */
518
519function token (name, fn) {
520 morgan[name] = fn
521 return this
522}