UNPKG

relu-core

Version:
299 lines (246 loc) 7.77 kB
// 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('node-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} */ 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; this.requestMap = 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} */ 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'); 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 (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 (s) { s.stream.write(s.raw ? r : ser); }); } defaultRing.records.length = 0; } } else { ring.write(record); } }; /** * toString() serialization * @public * @function toString * @returns {String} */ 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} */ 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} */ 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 };