-
Notifications
You must be signed in to change notification settings - Fork 19
/
Bristol.js
367 lines (350 loc) · 13.1 KB
/
Bristol.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
/*
* Bristol
* Copyright 2014-2016 Tom Shawver
*/
'use strict'
const DEFAULT_SEVERITY_LEVELS = ['error', 'warn', 'info', 'debug', 'trace']
const DEFAULT_FORMATTER = require('./formatters/json')
const logUtil = require('./logUtil')
const events = require('events')
const originalPrepareStackTrace = Error.prepareStackTrace
const arrayPrepareStackTrace = (err, stack) => { return stack }
/**
* The Bristol class defines a logger capable of simultaneous logging to
* different destinations called "targets", using a specified format for each,
* and parsing application-specific object types into meaningful attributes to
* be logged.
*
* New instances of Bristol can be created with the traditional "new Bristol()"
* style, however the object returned by require()ing this library is also
* an instance of Bristol, for the convenience of being able to require it,
* configure it, and have an immediately ready-to-use logger when requiring it
* in any other file.
*/
class Bristol extends events.EventEmitter {
constructor() {
super()
this._severities = {}
this._targets = []
this._transforms = []
this._globals = {}
this.setSeverities(DEFAULT_SEVERITY_LEVELS)
}
/**
* Adds a logging target to Bristol. The options supplied apply only to the
* target module itself. A formatter can be set, and restrictions can be
* placed on the messages that this target receives by chaining this call with
* a series of calls on the returned configuration object.
*
* If no configuration options are set, the default formatter is 'json' and
* the target will receive all log messages.
* @param {string|function} target The name of a built-in target, or a
* function with the signature (options.<Object>, severity.<string>,
* date.<Date>, message.<String>). Note that the target is not responsible
* for doing anything with the severity and date, as those should be
* handled by the formatter. They are provided in the event that the
* target may change its behavior based on these values.
* @param {{}} [options] An optional set of arguments specific to the target
* being added
* @returns {{
* withFormatter,
* excluding,
* onlyIncluding,
* withLowestSeverity,
* withHighestSeverity
* }} A config chain object
*/
addTarget(target, options) {
if (typeof target === 'string') {
target = require('./targets/' + target)
}
const targetObj = {
log: target.bind(target, options || {}),
formatter: DEFAULT_FORMATTER.bind(this, [{}]),
blacklist: {},
whitelist: {},
leastVerbose: 0,
mostVerbose: Infinity
}
this._targets.push(targetObj)
return this._getTargetConfigChain(targetObj)
}
/**
* Adds a transform function to the Bristol instance. The transform functions
* are called for every individual element to be logged, so Bristol can be made
* aware of any application-specific data types and log out only appropriate
* pieces of information when they're passed as an element. This mechanism can
* also be used to customize/silence the automatically provided metadata, such
* as the origin file/line. Transform functions take an element to be logged
* as their only argument, and return the transformed version of that element.
*
* If a given element should not be transformed, simply return null. Returning
* anything other than null will halt any more transforms being run on top
* of that element. Transforms are executed in the order in which they are
* added.
* @param {function} transform A transform function, accepting an element
* that was passed to the logger and returning what should be logged.
*/
addTransform(transform) {
this._transforms.push(transform)
}
/**
* Removes a previously-set global key/value pair. Log messages pushed after
* this is called will no longer include the removed pair.
* @param {string} key The key of the global value to be deleted
*/
deleteGlobal(key) {
delete this._globals[key]
}
/**
* Pushes the provided elements to each added target, under the given severity.
* Only targets that have not been restricted against this severity or type
* of element will be triggered. The given elements can be of any type, as
* long as there is a transform to handle it or the target's formatter can
* sufficiently serialize it.
*
* This function emits two events:
* - log : fired for every call to the logger, immediately after the log
* elements are pushed to the targets. The supplied argument will have
* the following properties:
* - {string} severity: The severity under which the elements were
* logged
* - {Date} date: A javascript Date object with the date under which
* the messages were logged
* - {Array} elements: An array of elements that were logged. These
* elements could be of any type
* - log:SEVERITY : fired for every call to the logger, with 'SEVERITY'
* replaced by the actual severity used for the log message. Argument
* is the same as for the 'log' event.
* @param {string} severity A string defining the severity of the log message.
* This string should be one of the set passed to {@link #setSeverities},
* or one from the default set of ['error', 'warn', 'info', 'debug',
* 'trace'] if that function was not called.
* @param {...*} elements One or more elements of any type to be logged
*/
log(severity, elements) { // eslint-disable-line no-unused-vars
if (!this._severities.hasOwnProperty(severity)) {
throw new Error(`Severity ${severity} does not exist.`)
}
const args = Array.prototype.slice.call(arguments)
const logTime = new Date()
const objArgs = {}
const logElems = []
// Remove the severity arg and put metadata first, all in one fell swoop
args[0] = logUtil.safeMerge(this._getOrigin(), this._getGlobals())
for (let i = 0; i < args.length; i++) {
const arg = this._transform(args[i])
if (typeof arg === 'object' && !(arg instanceof Error)) {
logUtil.safeMerge(objArgs, arg)
} else {
logElems.push(arg)
}
}
logElems.push(objArgs)
this._targets.forEach((target) => {
this._logToTarget(target, severity, logTime, logElems)
})
const eventObj = {
severity: severity,
date: logTime,
elements: logElems
}
this.emit('log', eventObj)
this.emit('log:' + severity, eventObj)
}
/**
* Sets a global key/value pair that will be logged with every message sent
* by this Bristol instance. If val is provided as a function, the function
* will be executed for every log message, and the logged value for that key
* will be its result.
* @param {string} key The key number under which to log the value
* @param {*} val The value to be logged
*/
setGlobal(key, val) {
this._globals[key] = val
}
/**
* Replaces the currently set severity levels with a new set. This function
* will make a new set of severity-named functions available on the Bristol
* instances, and remove the old ones. Function calls to a previously-set
* severity level WILL fail, if that level does not also appear in the new set.
*
* Severity levels should be supplied in an array of strings, in the order of
* most-severe to least-severe.
* @param {Array.<string>} levels The severity levels to use in this Bristol
* instance
*/
setSeverities(levels) {
const oldLevels = this._severities
const self = this
// Delete the existing error level functions
logUtil.forEachObj(this._severities, (key) => {
delete this[key]
})
// Create the new ones
levels.forEach((level, idx) => {
this._severities[level] = idx
if (!this[level]) {
this[level] = function() {
const args = Array.prototype.slice.call(arguments)
args.unshift(level)
self.log.apply(self, args)
}
} else {
this.setSeverities(oldLevels)
throw new Error(`Error level ${level} is already a Bristol function and cannot be used.`)
}
})
}
/**
* Retrieves the set global key/value pairs, executing any values that were
* supplied as functions.
* @returns {{}} An object containing global key/value pairs, with any
* function values replaced with their result.
* @private
*/
_getGlobals() {
const globals = {}
logUtil.forEachObj(this._globals, (key, val) => {
if (typeof val === 'function') globals[key] = val()
else globals[key] = val
})
return globals
}
/**
* Finds the origin of the Bristol log call, and supplies the file path and
* line number.
* This function uses JavaScriptStackTraceApi to be as fast as possible:
* https://code.google.com/p/v8-wiki/wiki/JavaScriptStackTraceApi
* @returns {null|{file, line}} An object containing the file path and line
* number of the originating Bristol call, or null if this information
* cannot be found.
* @private
*/
_getOrigin() {
Error.prepareStackTrace = arrayPrepareStackTrace
const stack = (new Error()).stack
let origin = null
for (let i = 1; i < stack.length; i++) {
const file = stack[i].getFileName()
if (file !== __filename) {
origin = {
file,
line: stack[i].getLineNumber().toString()
}
break
}
}
Error.prepareStackTrace = originalPrepareStackTrace
return origin
}
/**
* Gets an object containing chainable configuration functions that allow
* options to be specified on a given target.
* @param {{
* log,
* formatter,
* blacklist,
* whitelist,
* leastVerbose,
* mostVerbose
* }} target The target to be modified
* @returns {{
* withFormatter,
* excluding,
* onlyIncluding,
* withLowestSeverity,
* withHighestSeverity
* }} A config chain object
* @private
*/
_getTargetConfigChain(target) {
const chain = {}
chain.withFormatter = (formatter, options) => {
if (typeof formatter === 'string') {
formatter = require('./formatters/' + formatter)
}
target.formatter = formatter.bind(formatter, options || {})
return chain
}
chain.excluding = (blacklist) => {
target.blacklist = blacklist
return chain
}
chain.onlyIncluding = (whitelist) => {
target.whitelist = whitelist
return chain
}
chain.withLowestSeverity = (severity) => {
if (!this._severities.hasOwnProperty(severity)) {
throw new Error(`Severity ${severity} does not exist.`)
}
target.mostVerbose = this._severities[severity]
return chain
}
chain.withHighestSeverity = (severity) => {
if (!this._severities.hasOwnProperty(severity)) {
throw new Error(`Severity ${severity} does not exist.`)
}
target.leastVerbose = this._severities[severity]
return chain
}
return chain
}
/**
* Pushes a set of elements to a given logging target, given that the
* restrictions set on the given target do not block it.
*
* Note that, for efficiency, this function assumes that the elements have
* been rearranged so that all standard JS objects have been merged into a
* single object, and that object is the last element in the array of elements.
* The side-effect of this efficiency-boost is that the blacklist/whitelist
* tests will be run on that resulting object; so if there are any duplicate
* keys that were renamed during the merge, the filters will not be applied
* to those keys. Likelihood of this being a problem is low, but this may be
* revisited in a future version of Bristol.
* @param {{
* log,
* formatter,
* blacklist,
* whitelist,
* leastVerbose,
* mostVerbose
* }} target A target object to which the log elements should be pushed
* @param {string} severity The severity level under which to log the elements
* @param {Date} date A Date object defining the time to be used for the log
* message
* @param {Array} elems An array of elements to be logged, of any type.
* @private
*/
_logToTarget(target, severity, date, elems) {
const verbosity = this._severities[severity]
const testObj = elems[elems.length - 1]
if (verbosity >= target.leastVerbose &&
verbosity <= target.mostVerbose &&
!logUtil.matchesOneKey(testObj, target.blacklist) &&
logUtil.matchesAllKeys(testObj, target.whitelist)) {
const str = target.formatter(severity, date, elems)
target.log(severity, date, str)
}
}
/**
* Applies all transforms to an element, stopping when a transform returns a
* non-null value.
* @param {*} elem Any element to be transformed
* @returns {*} The transformed element, or the original element if no
* transform modified it.
* @private
*/
_transform(elem) {
let result
for (let i = 0; i < this._transforms.length; i++) {
result = this._transforms[i](elem)
if (result !== null) break
}
return result || elem
}
}
module.exports = new Bristol()
module.exports.Bristol = Bristol
module.exports._logUtil = logUtil