300 lines
7.9 KiB
JavaScript
300 lines
7.9 KiB
JavaScript
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
|
|
|
|
'use strict';
|
|
|
|
var Stream = require('stream').Stream;
|
|
var util = require('util');
|
|
|
|
var assert = require('assert-plus');
|
|
var bunyan = require('bunyan');
|
|
var LRU = require('lru-cache');
|
|
var uuid = require('uuid');
|
|
|
|
///--- Globals
|
|
|
|
var sprintf = util.format;
|
|
var DEFAULT_REQ_ID = uuid.v4();
|
|
var STR_FMT = '[object %s<level=%d, limit=%d, maxRequestIds=%d>]';
|
|
|
|
///--- Helpers
|
|
|
|
/**
|
|
* Appends streams
|
|
*
|
|
* @private
|
|
* @function appendStream
|
|
* @param {Stream} streams - the stream to append to
|
|
* @param {Stream} s - the stream to append
|
|
* @returns {undefined} no return value
|
|
*/
|
|
function appendStream(streams, s) {
|
|
assert.arrayOfObject(streams, 'streams');
|
|
assert.object(s, 'stream');
|
|
|
|
if (s instanceof Stream) {
|
|
streams.push({
|
|
raw: false,
|
|
stream: s
|
|
});
|
|
} else {
|
|
assert.optionalBool(s.raw, 'stream.raw');
|
|
assert.object(s.stream, 'stream.stream');
|
|
streams.push(s);
|
|
}
|
|
}
|
|
|
|
///--- API
|
|
/**
|
|
* A Bunyan stream to capture records in a ring buffer and only pass through
|
|
* on a higher-level record. E.g. buffer up all records but only dump when
|
|
* getting a WARN or above.
|
|
*
|
|
* @public
|
|
* @class
|
|
* @param {Object} opts - contains the parameters:
|
|
* @param {Object} opts.stream - The stream to which to write when
|
|
* dumping captured records.
|
|
* One of `stream`
|
|
* or `streams` must be specified.
|
|
* @param {Array} opts.streams - One of `stream` or `streams` must be
|
|
* specified.
|
|
* @param {Number | String} opts.level - The level at which to trigger dumping
|
|
* captured records. Defaults to
|
|
* bunyan.WARN.
|
|
* @param {Number} opts.maxRecords - Number of records to capture. Default
|
|
* 100.
|
|
* @param {Number} opts.maxRequestIds - Number of simultaneous request id
|
|
* capturing buckets to maintain. Default
|
|
* 1000.
|
|
* @param {Boolean} opts.dumpDefault - If true, then dump captured records on
|
|
* the *default* request id when dumping.
|
|
* I.e. dump records logged without
|
|
* "req_id" field. Default false.
|
|
*/
|
|
function RequestCaptureStream(opts) {
|
|
assert.object(opts, 'options');
|
|
assert.optionalObject(opts.stream, 'options.stream');
|
|
assert.optionalArrayOfObject(opts.streams, 'options.streams');
|
|
assert.optionalNumber(opts.level, 'options.level');
|
|
assert.optionalNumber(opts.maxRecords, 'options.maxRecords');
|
|
assert.optionalNumber(opts.maxRequestIds, 'options.maxRequestIds');
|
|
assert.optionalBool(opts.dumpDefault, 'options.dumpDefault');
|
|
|
|
var self = this;
|
|
Stream.call(this);
|
|
|
|
this.level = opts.level ? bunyan.resolveLevel(opts.level) : bunyan.WARN;
|
|
this.limit = opts.maxRecords || 100;
|
|
this.maxRequestIds = opts.maxRequestIds || 1000;
|
|
// eslint-disable-next-line new-cap
|
|
// need to initialize using `new`
|
|
this.requestMap = new LRU({
|
|
max: self.maxRequestIds
|
|
});
|
|
this.dumpDefault = opts.dumpDefault;
|
|
|
|
this._offset = -1;
|
|
this._rings = [];
|
|
|
|
this.streams = [];
|
|
|
|
if (opts.stream) {
|
|
appendStream(this.streams, opts.stream);
|
|
}
|
|
|
|
if (opts.streams) {
|
|
opts.streams.forEach(appendStream.bind(null, this.streams));
|
|
}
|
|
|
|
this.haveNonRawStreams = false;
|
|
|
|
for (var i = 0; i < this.streams.length; i++) {
|
|
if (!this.streams[i].raw) {
|
|
this.haveNonRawStreams = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
util.inherits(RequestCaptureStream, Stream);
|
|
|
|
/**
|
|
* Write to the stream
|
|
*
|
|
* @public
|
|
* @function write
|
|
* @param {Object} record - a bunyan log record
|
|
* @returns {undefined} no return value
|
|
*/
|
|
RequestCaptureStream.prototype.write = function write(record) {
|
|
var req_id = record.req_id || DEFAULT_REQ_ID;
|
|
var ring;
|
|
var self = this;
|
|
|
|
if (!(ring = this.requestMap.get(req_id))) {
|
|
if (++this._offset > this.maxRequestIds) {
|
|
this._offset = 0;
|
|
}
|
|
|
|
if (this._rings.length <= this._offset) {
|
|
this._rings.push(
|
|
new bunyan.RingBuffer({
|
|
limit: self.limit
|
|
})
|
|
);
|
|
}
|
|
|
|
ring = this._rings[this._offset];
|
|
ring.records.length = 0;
|
|
this.requestMap.set(req_id, ring);
|
|
}
|
|
|
|
assert.ok(ring, 'no ring found');
|
|
|
|
// write the record to the ring.
|
|
ring.write(record);
|
|
// triger dumping of all the records
|
|
if (record.level >= this.level) {
|
|
var i, r, ser;
|
|
|
|
for (i = 0; i < ring.records.length; i++) {
|
|
r = ring.records[i];
|
|
|
|
if (this.haveNonRawStreams) {
|
|
ser = JSON.stringify(r, bunyan.safeCycles()) + '\n';
|
|
}
|
|
self.streams.forEach(function forEach(s) {
|
|
s.stream.write(s.raw ? r : ser);
|
|
});
|
|
}
|
|
ring.records.length = 0;
|
|
|
|
if (this.dumpDefault) {
|
|
var defaultRing = self.requestMap.get(DEFAULT_REQ_ID);
|
|
|
|
for (i = 0; i < defaultRing.records.length; i++) {
|
|
r = defaultRing.records[i];
|
|
|
|
if (this.haveNonRawStreams) {
|
|
ser = JSON.stringify(r, bunyan.safeCycles()) + '\n';
|
|
}
|
|
self.streams.forEach(function forEach(s) {
|
|
s.stream.write(s.raw ? r : ser);
|
|
});
|
|
}
|
|
defaultRing.records.length = 0;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* toString() serialization
|
|
*
|
|
* @public
|
|
* @function toString
|
|
* @returns {String} stringified instance
|
|
*/
|
|
RequestCaptureStream.prototype.toString = function toString() {
|
|
return sprintf(
|
|
STR_FMT,
|
|
this.constructor.name,
|
|
this.level,
|
|
this.limit,
|
|
this.maxRequestIds
|
|
);
|
|
};
|
|
|
|
///--- Serializers
|
|
|
|
var SERIALIZERS = {
|
|
err: bunyan.stdSerializers.err,
|
|
req: bunyan.stdSerializers.req,
|
|
res: bunyan.stdSerializers.res,
|
|
client_req: clientReq,
|
|
client_res: clientRes
|
|
};
|
|
|
|
/**
|
|
* A request serializer. returns a stripped down object for logging.
|
|
*
|
|
* @private
|
|
* @function clientReq
|
|
* @param {Object} req - the request object
|
|
* @returns {Object} serialized req
|
|
*/
|
|
function clientReq(req) {
|
|
if (!req) {
|
|
return req;
|
|
}
|
|
|
|
var host;
|
|
|
|
try {
|
|
host = req.host.split(':')[0];
|
|
} catch (e) {
|
|
host = false;
|
|
}
|
|
|
|
return {
|
|
method: req ? req.method : false,
|
|
url: req ? req.path : false,
|
|
address: host,
|
|
port: req ? req.port : false,
|
|
headers: req ? req.headers : false
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A response serializer. returns a stripped down object for logging.
|
|
*
|
|
* @private
|
|
* @function clientRes
|
|
* @param {Object} res - the response object
|
|
* @returns {Object} serialized response
|
|
*/
|
|
function clientRes(res) {
|
|
if (!res || !res.statusCode) {
|
|
return res;
|
|
}
|
|
|
|
return {
|
|
statusCode: res.statusCode,
|
|
headers: res.headers
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a bunyan logger.
|
|
*
|
|
* @public
|
|
* @function createLogger
|
|
* @param {String} name - of the logger
|
|
* @returns {Object} bunyan logger
|
|
*/
|
|
function createLogger(name) {
|
|
return bunyan.createLogger({
|
|
name: name,
|
|
serializers: SERIALIZERS,
|
|
streams: [
|
|
{
|
|
level: 'warn',
|
|
stream: process.stderr
|
|
},
|
|
{
|
|
level: 'debug',
|
|
type: 'raw',
|
|
stream: new RequestCaptureStream({
|
|
stream: process.stderr
|
|
})
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
///--- Exports
|
|
|
|
module.exports = {
|
|
RequestCaptureStream: RequestCaptureStream,
|
|
serializers: SERIALIZERS,
|
|
createLogger: createLogger
|
|
};
|