|
|
/*! * morgan * Copyright(c) 2010 Sencha Inc. * Copyright(c) 2011 TJ Holowaychuk * Copyright(c) 2014 Jonathan Ong * Copyright(c) 2014-2017 Douglas Christopher Wilson * MIT Licensed */
'use strict'
/** * Module exports. * @public */
module.exports = morgan module.exports.compile = compile module.exports.format = format module.exports.token = token
/** * Module dependencies. * @private */
var auth = require('basic-auth') var debug = require('debug')('morgan') var deprecate = require('depd')('morgan') var onFinished = require('on-finished') var onHeaders = require('on-headers')
/** * Array of CLF month names. * @private */
var CLF_MONTH = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
/** * Default log buffer duration. * @private */
var DEFAULT_BUFFER_DURATION = 1000
/** * Create a logger middleware. * * @public * @param {String|Function} format * @param {Object} [options] * @return {Function} middleware */
function morgan (format, options) { var fmt = format var opts = options || {}
if (format && typeof format === 'object') { opts = format fmt = opts.format || 'default'
// smart deprecation message
deprecate('morgan(options): use morgan(' + (typeof fmt === 'string' ? JSON.stringify(fmt) : 'format') + ', options) instead') }
if (fmt === undefined) { deprecate('undefined format: specify a format') }
// output on request instead of response
var immediate = opts.immediate
// check if log entry should be skipped
var skip = opts.skip || false
// format function
var formatLine = typeof fmt !== 'function' ? getFormatFunction(fmt) : fmt
// stream
var buffer = opts.buffer var stream = opts.stream || process.stdout
// buffering support
if (buffer) { deprecate('buffer option')
// flush interval
var interval = typeof buffer !== 'number' ? DEFAULT_BUFFER_DURATION : buffer
// swap the stream
stream = createBufferStream(stream, interval) }
return function logger (req, res, next) { // request data
req._startAt = undefined req._startTime = undefined req._remoteAddress = getip(req)
// response data
res._startAt = undefined res._startTime = undefined
// record request start
recordStartTime.call(req)
function logRequest () { if (skip !== false && skip(req, res)) { debug('skip request') return }
var line = formatLine(morgan, req, res)
if (line == null) { debug('skip line') return }
debug('log request') stream.write(line + '\n') };
if (immediate) { // immediate log
logRequest() } else { // record response start
onHeaders(res, recordStartTime)
// log when response finished
onFinished(res, logRequest) }
next() } }
/** * Apache combined log format. */
morgan.format('combined', ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"')
/** * Apache common log format. */
morgan.format('common', ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length]')
/** * Default format. */
morgan.format('default', ':remote-addr - :remote-user [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"') deprecate.property(morgan, 'default', 'default format: use combined format')
/** * Short format. */
morgan.format('short', ':remote-addr :remote-user :method :url HTTP/:http-version :status :res[content-length] - :response-time ms')
/** * Tiny format. */
morgan.format('tiny', ':method :url :status :res[content-length] - :response-time ms')
/** * dev (colored) */
morgan.format('dev', function developmentFormatLine (tokens, req, res) { // get the status code if response written
var status = headersSent(res) ? res.statusCode : undefined
// get status color
var color = status >= 500 ? 31 // red
: status >= 400 ? 33 // yellow
: status >= 300 ? 36 // cyan
: status >= 200 ? 32 // green
: 0 // no color
// get colored function
var fn = developmentFormatLine[color]
if (!fn) { // compile
fn = developmentFormatLine[color] = compile('\x1b[0m:method :url \x1b[' + color + 'm:status \x1b[0m:response-time ms - :res[content-length]\x1b[0m') }
return fn(tokens, req, res) })
/** * request url */
morgan.token('url', function getUrlToken (req) { return req.originalUrl || req.url })
/** * request method */
morgan.token('method', function getMethodToken (req) { return req.method })
/** * response time in milliseconds */
morgan.token('response-time', function getResponseTimeToken (req, res, digits) { if (!req._startAt || !res._startAt) { // missing request and/or response start time
return }
// calculate diff
var ms = (res._startAt[0] - req._startAt[0]) * 1e3 + (res._startAt[1] - req._startAt[1]) * 1e-6
// return truncated value
return ms.toFixed(digits === undefined ? 3 : digits) })
/** * current date */
morgan.token('date', function getDateToken (req, res, format) { var date = new Date()
switch (format || 'web') { case 'clf': return clfdate(date) case 'iso': return date.toISOString() case 'web': return date.toUTCString() } })
/** * response status code */
morgan.token('status', function getStatusToken (req, res) { return headersSent(res) ? String(res.statusCode) : undefined })
/** * normalized referrer */
morgan.token('referrer', function getReferrerToken (req) { return req.headers['referer'] || req.headers['referrer'] })
/** * remote address */
morgan.token('remote-addr', getip)
/** * remote user */
morgan.token('remote-user', function getRemoteUserToken (req) { // parse basic credentials
var credentials = auth(req)
// return username
return credentials ? credentials.name : undefined })
/** * HTTP version */
morgan.token('http-version', function getHttpVersionToken (req) { return req.httpVersionMajor + '.' + req.httpVersionMinor })
/** * UA string */
morgan.token('user-agent', function getUserAgentToken (req) { return req.headers['user-agent'] })
/** * request header */
morgan.token('req', function getRequestToken (req, res, field) { // get header
var header = req.headers[field.toLowerCase()]
return Array.isArray(header) ? header.join(', ') : header })
/** * response header */
morgan.token('res', function getResponseHeader (req, res, field) { if (!headersSent(res)) { return undefined }
// get header
var header = res.getHeader(field)
return Array.isArray(header) ? header.join(', ') : header })
/** * Format a Date in the common log format. * * @private * @param {Date} dateTime * @return {string} */
function clfdate (dateTime) { var date = dateTime.getUTCDate() var hour = dateTime.getUTCHours() var mins = dateTime.getUTCMinutes() var secs = dateTime.getUTCSeconds() var year = dateTime.getUTCFullYear()
var month = CLF_MONTH[dateTime.getUTCMonth()]
return pad2(date) + '/' + month + '/' + year + ':' + pad2(hour) + ':' + pad2(mins) + ':' + pad2(secs) + ' +0000' }
/** * Compile a format string into a function. * * @param {string} format * @return {function} * @public */
function compile (format) { if (typeof format !== 'string') { throw new TypeError('argument format must be a string') }
var fmt = String(JSON.stringify(format)) var js = ' "use strict"\n return ' + fmt.replace(/:([-\w]{2,})(?:\[([^\]]+)\])?/g, function (_, name, arg) { var tokenArguments = 'req, res' var tokenFunction = 'tokens[' + String(JSON.stringify(name)) + ']'
if (arg !== undefined) { tokenArguments += ', ' + String(JSON.stringify(arg)) }
return '" +\n (' + tokenFunction + '(' + tokenArguments + ') || "-") + "' })
// eslint-disable-next-line no-new-func
return new Function('tokens, req, res', js) }
/** * Create a basic buffering stream. * * @param {object} stream * @param {number} interval * @public */
function createBufferStream (stream, interval) { var buf = [] var timer = null
// flush function
function flush () { timer = null stream.write(buf.join('')) buf.length = 0 }
// write function
function write (str) { if (timer === null) { timer = setTimeout(flush, interval) }
buf.push(str) }
// return a minimal "stream"
return { write: write } }
/** * Define a format with the given name. * * @param {string} name * @param {string|function} fmt * @public */
function format (name, fmt) { morgan[name] = fmt return this }
/** * Lookup and compile a named format function. * * @param {string} name * @return {function} * @public */
function getFormatFunction (name) { // lookup format
var fmt = morgan[name] || name || morgan.default
// return compiled format
return typeof fmt !== 'function' ? compile(fmt) : fmt }
/** * Get request IP address. * * @private * @param {IncomingMessage} req * @return {string} */
function getip (req) { return req.ip || req._remoteAddress || (req.connection && req.connection.remoteAddress) || undefined }
/** * Determine if the response headers have been sent. * * @param {object} res * @returns {boolean} * @private */
function headersSent (res) { return typeof res.headersSent !== 'boolean' ? Boolean(res._header) : res.headersSent }
/** * Pad number to two digits. * * @private * @param {number} num * @return {string} */
function pad2 (num) { var str = String(num)
return (str.length === 1 ? '0' : '') + str }
/** * Record the start time. * @private */
function recordStartTime () { this._startAt = process.hrtime() this._startTime = new Date() }
/** * Define a token function with the given name, * and callback fn(req, res). * * @param {string} name * @param {function} fn * @public */
function token (name, fn) { morgan[name] = fn return this }
|