UNPKG

8.11 kBJavaScriptView Raw
1// Copyright 2012 Mark Cavage, Inc. All rights reserved.
2
3'use strict';
4
5var Stream = require('stream').Stream;
6var util = require('util');
7
8var assert = require('assert-plus');
9var bunyan = require('bunyan');
10var LRU = require('lru-cache');
11var uuid = require('uuid');
12
13///--- Globals
14
15var sprintf = util.format;
16var DEFAULT_REQ_ID = uuid.v4();
17var STR_FMT = '[object %s<level=%d, limit=%d, maxRequestIds=%d>]';
18
19///--- Helpers
20
21/**
22 * Appends streams
23 *
24 * @private
25 * @function appendStream
26 * @param {Stream} streams - the stream to append to
27 * @param {Stream} s - the stream to append
28 * @returns {undefined} no return value
29 */
30function appendStream(streams, s) {
31 assert.arrayOfObject(streams, 'streams');
32 assert.object(s, 'stream');
33
34 if (s instanceof Stream) {
35 streams.push({
36 raw: false,
37 stream: s
38 });
39 } else {
40 assert.optionalBool(s.raw, 'stream.raw');
41 assert.object(s.stream, 'stream.stream');
42 streams.push(s);
43 }
44}
45
46///--- API
47/**
48 * A Bunyan stream to capture records in a ring buffer and only pass through
49 * on a higher-level record. E.g. buffer up all records but only dump when
50 * getting a WARN or above.
51 *
52 * @public
53 * @class
54 * @param {Object} opts - contains the parameters:
55 * @param {Object} opts.stream - The stream to which to write when
56 * dumping captured records.
57 * One of `stream`
58 * or `streams` must be specified.
59 * @param {Array} opts.streams - One of `stream` or `streams` must be
60 * specified.
61 * @param {Number | String} opts.level - The level at which to trigger dumping
62 * captured records. Defaults to
63 * bunyan.WARN.
64 * @param {Number} opts.maxRecords - Number of records to capture. Default
65 * 100.
66 * @param {Number} opts.maxRequestIds - Number of simultaneous request id
67 * capturing buckets to maintain. Default
68 * 1000.
69 * @param {Boolean} opts.dumpDefault - If true, then dump captured records on
70 * the *default* request id when dumping.
71 * I.e. dump records logged without
72 * "req_id" field. Default false.
73 */
74function RequestCaptureStream(opts) {
75 assert.object(opts, 'options');
76 assert.optionalObject(opts.stream, 'options.stream');
77 assert.optionalArrayOfObject(opts.streams, 'options.streams');
78 assert.optionalNumber(opts.level, 'options.level');
79 assert.optionalNumber(opts.maxRecords, 'options.maxRecords');
80 assert.optionalNumber(opts.maxRequestIds, 'options.maxRequestIds');
81 assert.optionalBool(opts.dumpDefault, 'options.dumpDefault');
82
83 var self = this;
84 Stream.call(this);
85
86 this.level = opts.level ? bunyan.resolveLevel(opts.level) : bunyan.WARN;
87 this.limit = opts.maxRecords || 100;
88 this.maxRequestIds = opts.maxRequestIds || 1000;
89 // eslint-disable-next-line new-cap
90 // need to initialize using `new`
91 this.requestMap = new LRU({
92 max: self.maxRequestIds
93 });
94 this.dumpDefault = opts.dumpDefault;
95
96 this._offset = -1;
97 this._rings = [];
98
99 this.streams = [];
100
101 if (opts.stream) {
102 appendStream(this.streams, opts.stream);
103 }
104
105 if (opts.streams) {
106 opts.streams.forEach(appendStream.bind(null, this.streams));
107 }
108
109 this.haveNonRawStreams = false;
110
111 for (var i = 0; i < this.streams.length; i++) {
112 if (!this.streams[i].raw) {
113 this.haveNonRawStreams = true;
114 break;
115 }
116 }
117}
118util.inherits(RequestCaptureStream, Stream);
119
120/**
121 * Write to the stream
122 *
123 * @public
124 * @function write
125 * @param {Object} record - a bunyan log record
126 * @returns {undefined} no return value
127 */
128RequestCaptureStream.prototype.write = function write(record) {
129 var req_id = record.req_id || DEFAULT_REQ_ID;
130 var ring;
131 var self = this;
132
133 if (!(ring = this.requestMap.get(req_id))) {
134 if (++this._offset > this.maxRequestIds) {
135 this._offset = 0;
136 }
137
138 if (this._rings.length <= this._offset) {
139 this._rings.push(
140 new bunyan.RingBuffer({
141 limit: self.limit
142 })
143 );
144 }
145
146 ring = this._rings[this._offset];
147 ring.records.length = 0;
148 this.requestMap.set(req_id, ring);
149 }
150
151 assert.ok(ring, 'no ring found');
152
153 // write the record to the ring.
154 ring.write(record);
155 // triger dumping of all the records
156 if (record.level >= this.level) {
157 var i, r, ser;
158
159 for (i = 0; i < ring.records.length; i++) {
160 r = ring.records[i];
161
162 if (this.haveNonRawStreams) {
163 ser = JSON.stringify(r, bunyan.safeCycles()) + '\n';
164 }
165 self.streams.forEach(function forEach(s) {
166 s.stream.write(s.raw ? r : ser);
167 });
168 }
169 ring.records.length = 0;
170
171 if (this.dumpDefault) {
172 var defaultRing = self.requestMap.get(DEFAULT_REQ_ID);
173
174 for (i = 0; i < defaultRing.records.length; i++) {
175 r = defaultRing.records[i];
176
177 if (this.haveNonRawStreams) {
178 ser = JSON.stringify(r, bunyan.safeCycles()) + '\n';
179 }
180 self.streams.forEach(function forEach(s) {
181 s.stream.write(s.raw ? r : ser);
182 });
183 }
184 defaultRing.records.length = 0;
185 }
186 }
187};
188
189/**
190 * toString() serialization
191 *
192 * @public
193 * @function toString
194 * @returns {String} stringified instance
195 */
196RequestCaptureStream.prototype.toString = function toString() {
197 return sprintf(
198 STR_FMT,
199 this.constructor.name,
200 this.level,
201 this.limit,
202 this.maxRequestIds
203 );
204};
205
206///--- Serializers
207
208var SERIALIZERS = {
209 err: bunyan.stdSerializers.err,
210 req: bunyan.stdSerializers.req,
211 res: bunyan.stdSerializers.res,
212 client_req: clientReq,
213 client_res: clientRes
214};
215
216/**
217 * A request serializer. returns a stripped down object for logging.
218 *
219 * @private
220 * @function clientReq
221 * @param {Object} req - the request object
222 * @returns {Object} serialized req
223 */
224function clientReq(req) {
225 if (!req) {
226 return req;
227 }
228
229 var host;
230
231 try {
232 host = req.host.split(':')[0];
233 } catch (e) {
234 host = false;
235 }
236
237 return {
238 method: req ? req.method : false,
239 url: req ? req.path : false,
240 address: host,
241 port: req ? req.port : false,
242 headers: req ? req.headers : false
243 };
244}
245
246/**
247 * A response serializer. returns a stripped down object for logging.
248 *
249 * @private
250 * @function clientRes
251 * @param {Object} res - the response object
252 * @returns {Object} serialized response
253 */
254function clientRes(res) {
255 if (!res || !res.statusCode) {
256 return res;
257 }
258
259 return {
260 statusCode: res.statusCode,
261 headers: res.headers
262 };
263}
264
265/**
266 * Create a bunyan logger.
267 *
268 * @public
269 * @function createLogger
270 * @param {String} name - of the logger
271 * @returns {Object} bunyan logger
272 */
273function createLogger(name) {
274 return bunyan.createLogger({
275 name: name,
276 serializers: SERIALIZERS,
277 streams: [
278 {
279 level: 'warn',
280 stream: process.stderr
281 },
282 {
283 level: 'debug',
284 type: 'raw',
285 stream: new RequestCaptureStream({
286 stream: process.stderr
287 })
288 }
289 ]
290 });
291}
292
293///--- Exports
294
295module.exports = {
296 RequestCaptureStream: RequestCaptureStream,
297 serializers: SERIALIZERS,
298 createLogger: createLogger
299};