UNPKG

7.94 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 Douglas Christopher Wilson
7 * MIT Licensed
8 */
9
10/**
11 * Module dependencies.
12 * @private
13 */
14
15var auth = require('basic-auth')
16var debug = require('debug')('morgan')
17var deprecate = require('depd')('morgan')
18var onFinished = require('on-finished')
19
20/**
21 * Array of CLF month names.
22 * @private
23 */
24
25var clfmonth = [
26 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
27 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
28]
29
30/**
31 * Default log buffer duration.
32 * @private
33 */
34
35var defaultBufferDuration = 1000;
36
37/**
38 * Create a logger middleware.
39 *
40 * @public
41 * @param {String|Function} format
42 * @param {Object} [options]
43 * @return {Function} middleware
44 */
45
46exports = module.exports = function morgan(format, options) {
47 if (typeof format === 'object') {
48 options = format
49 format = options.format || 'default'
50
51 // smart deprecation message
52 deprecate('morgan(options): use morgan(' + (typeof format === 'string' ? JSON.stringify(format) : 'format') + ', options) instead')
53 }
54
55 if (format === undefined) {
56 deprecate('undefined format: specify a format')
57 }
58
59 options = options || {}
60
61 // output on request instead of response
62 var immediate = options.immediate;
63
64 // check if log entry should be skipped
65 var skip = options.skip || function () { return false; };
66
67 // format function
68 var fmt = compile(exports[format] || format || exports.default)
69
70 // steam
71 var buffer = options.buffer
72 var stream = options.stream || process.stdout
73
74 // buffering support
75 if (buffer) {
76 deprecate('buffer option')
77
78 var realStream = stream
79 var buf = []
80 var timer = null
81 var interval = 'number' == typeof buffer
82 ? buffer
83 : defaultBufferDuration
84
85 // flush function
86 var flush = function(){
87 timer = null
88
89 if (buf.length) {
90 realStream.write(buf.join(''));
91 buf.length = 0;
92 }
93 }
94
95 // swap the stream
96 stream = {
97 write: function(str){
98 if (timer === null) {
99 timer = setTimeout(flush, interval)
100 }
101
102 buf.push(str);
103 }
104 };
105 }
106
107 return function logger(req, res, next) {
108 req._startAt = process.hrtime();
109 req._startTime = new Date;
110 req._remoteAddress = getip(req);
111
112 function logRequest(){
113 if (skip(req, res)) {
114 debug('skip request')
115 return
116 }
117
118 var line = fmt(exports, req, res)
119
120 if (null == line) {
121 debug('skip line')
122 return
123 }
124
125 debug('log request')
126 stream.write(line + '\n')
127 };
128
129 // immediate
130 if (immediate) {
131 logRequest();
132 } else {
133 onFinished(res, logRequest)
134 }
135
136 next();
137 };
138};
139
140/**
141 * Compile `format` into a function.
142 *
143 * @private
144 * @param {Function|String} format
145 * @return {Function}
146 */
147
148function compile(format) {
149 if (typeof format === 'function') {
150 // already compiled
151 return format
152 }
153
154 if (typeof format !== 'string') {
155 throw new TypeError('argument format must be a function or string')
156 }
157
158 var fmt = format.replace(/"/g, '\\"')
159 var js = ' return "' + fmt.replace(/:([-\w]{2,})(?:\[([^\]]+)\])?/g, function(_, name, arg){
160 return '"\n + (tokens["' + name + '"](req, res, ' + String(JSON.stringify(arg)) + ') || "-") + "';
161 }) + '";'
162
163 return new Function('tokens, req, res', js);
164};
165
166/**
167 * Define a token function with the given `name`,
168 * and callback `fn(req, res)`.
169 *
170 * @public
171 * @param {String} name
172 * @param {Function} fn
173 * @return {Object} exports for chaining
174 */
175
176exports.token = function(name, fn) {
177 exports[name] = fn;
178 return this;
179};
180
181/**
182 * Define a `fmt` with the given `name`.
183 *
184 * @public
185 * @param {String} name
186 * @param {String|Function} fmt
187 * @return {Object} exports for chaining
188 */
189
190exports.format = function(name, fmt){
191 exports[name] = fmt;
192 return this;
193};
194
195/**
196 * Apache combined log format.
197 */
198
199exports.format('combined', ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"')
200
201/**
202 * Apache common log format.
203 */
204
205exports.format('common', ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length]')
206
207/**
208 * Default format.
209 */
210
211exports.format('default', ':remote-addr - :remote-user [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"');
212deprecate.property(exports, 'default', 'default format: use combined format')
213
214/**
215 * Short format.
216 */
217
218exports.format('short', ':remote-addr :remote-user :method :url HTTP/:http-version :status :res[content-length] - :response-time ms');
219
220/**
221 * Tiny format.
222 */
223
224exports.format('tiny', ':method :url :status :res[content-length] - :response-time ms');
225
226/**
227 * dev (colored)
228 */
229
230exports.format('dev', function(tokens, req, res){
231 var color = 32; // green
232 var status = res.statusCode;
233
234 if (status >= 500) color = 31; // red
235 else if (status >= 400) color = 33; // yellow
236 else if (status >= 300) color = 36; // cyan
237
238 var fn = compile('\x1b[0m:method :url \x1b[' + color + 'm:status \x1b[0m:response-time ms - :res[content-length]\x1b[0m');
239
240 return fn(tokens, req, res);
241});
242
243/**
244 * request url
245 */
246
247exports.token('url', function(req){
248 return req.originalUrl || req.url;
249});
250
251/**
252 * request method
253 */
254
255exports.token('method', function(req){
256 return req.method;
257});
258
259/**
260 * response time in milliseconds
261 */
262
263exports.token('response-time', function(req, res){
264 if (!res._header || !req._startAt) return '';
265 var diff = process.hrtime(req._startAt);
266 var ms = diff[0] * 1e3 + diff[1] * 1e-6;
267 return ms.toFixed(3);
268});
269
270/**
271 * current date
272 */
273
274exports.token('date', function(req, res, format){
275 format = format || 'web'
276
277 var date = new Date()
278
279 switch (format) {
280 case 'clf':
281 return clfdate(date)
282 case 'iso':
283 return date.toISOString()
284 case 'web':
285 return date.toUTCString()
286 }
287});
288
289/**
290 * response status code
291 */
292
293exports.token('status', function(req, res){
294 return res._header ? res.statusCode : null;
295});
296
297/**
298 * normalized referrer
299 */
300
301exports.token('referrer', function(req){
302 return req.headers['referer'] || req.headers['referrer'];
303});
304
305/**
306 * remote address
307 */
308
309exports.token('remote-addr', getip);
310
311/**
312 * remote user
313 */
314
315exports.token('remote-user', function (req) {
316 var creds = auth(req)
317 var user = (creds && creds.name) || '-'
318 return user;
319})
320
321/**
322 * HTTP version
323 */
324
325exports.token('http-version', function(req){
326 return req.httpVersionMajor + '.' + req.httpVersionMinor;
327});
328
329/**
330 * UA string
331 */
332
333exports.token('user-agent', function(req){
334 return req.headers['user-agent'];
335});
336
337/**
338 * request header
339 */
340
341exports.token('req', function(req, res, field){
342 return req.headers[field.toLowerCase()];
343});
344
345/**
346 * response header
347 */
348
349exports.token('res', function(req, res, field){
350 return (res._headers || {})[field.toLowerCase()];
351});
352
353/**
354 * Format a Date in the common log format.
355 *
356 * @private
357 * @param {Date} dateTime
358 * @return {string}
359 */
360
361function clfdate(dateTime) {
362 var date = dateTime.getUTCDate()
363 var hour = dateTime.getUTCHours()
364 var mins = dateTime.getUTCMinutes()
365 var secs = dateTime.getUTCSeconds()
366 var year = dateTime.getUTCFullYear()
367
368 var month = clfmonth[dateTime.getUTCMonth()]
369
370 return pad2(date) + '/' + month + '/' + year
371 + ':' + pad2(hour) + ':' + pad2(mins) + ':' + pad2(secs)
372 + ' +0000'
373}
374
375/**
376 * Get request IP address.
377 *
378 * @private
379 * @param {IncomingMessage} req
380 * @return {string}
381 */
382
383function getip(req) {
384 return req.ip
385 || req._remoteAddress
386 || (req.connection && req.connection.remoteAddress)
387 || undefined;
388}
389
390/**
391 * Pad number to two digits.
392 *
393 * @private
394 * @param {number} num
395 * @return {string}
396 */
397
398function pad2(num) {
399 var str = String(num)
400
401 return (str.length === 1 ? '0' : '')
402 + str
403}