3.0.2
[squeep-logger-json-console] / lib / logger.js
1 'use strict';
2
3 const jsonReplacers = require('./json-replacers');
4 const dataSanitizers = require('./data-sanitizers');
5
6 /**
7 * Log as JSON to stdout/stderr.
8 */
9
10 const nop = () => { /**/ };
11
12
13 class Logger {
14 /**
15 * Wrap backend calls with payload normalization.
16 * @param {Object} options
17 * @param {String} options.nodeId - unique identifier for running instance, usually a uuid
18 * @param {Object} options.logger
19 * @param {String} options.logger.ignoreBelowLevel - minimum level to log
20 */
21 constructor(options) {
22 const logLevels = Object.keys(Logger.nullLogger);
23 const ignoreBelowLevel = options && options.logger && options.logger.ignoreBelowLevel || 'debug';
24 this.backend = console;
25 this.nodeId = options.nodeId;
26 this.jsonReplacers = Object.values(jsonReplacers);
27 this.dataSanitizers = Object.values(dataSanitizers);
28
29 if (!logLevels.includes(ignoreBelowLevel)) {
30 throw new RangeError(`unrecognized minimum log level '${ignoreBelowLevel}'`);
31 }
32 const ignoreLevelIdx = logLevels.indexOf(ignoreBelowLevel);
33 logLevels.forEach((level) => {
34 // eslint-disable-next-line security/detect-object-injection
35 this[level] = (logLevels.indexOf(level) > ignoreLevelIdx) ?
36 nop :
37 (...args) => this.backend[level](this.payload(level, ...args)); // eslint-disable-line security/detect-object-injection
38 });
39 }
40
41
42 /**
43 * All the expected Console levels, but do nothing.
44 * Ordered from highest priority to lowest.
45 */
46 static get nullLogger() {
47 return {
48 error: nop,
49 warn: nop,
50 info: nop,
51 log: nop,
52 debug: nop,
53 };
54 }
55
56
57 /**
58 * Structure all expected log data into JSON string.
59 * @param {String} level
60 * @param {String} scope
61 * @param {String} message
62 * @param {Object} data
63 * @param {...any} other
64 * @returns {String} JSON string
65 */
66 payload(level, scope, message, data, ...other) {
67 if (this.sanitizationNeeded(data)) {
68 // Create copy of data so we are not changing anything important.
69 data = JSON.parse(JSON.stringify(data, this.jsonReplacer.bind(this)));
70 this.sanitize(data);
71 }
72
73 const now = new Date();
74 return JSON.stringify({
75 nodeId: this.nodeId,
76 timestamp: now.toISOString(),
77 timestampMs: now.getTime(),
78 level: level,
79 scope: scope || '[unknown]',
80 message: message || '',
81 data: data || {},
82 ...(other.length && { other }),
83 }, this.jsonReplacer.bind(this));
84 }
85
86
87 /**
88 * Determine if data needs sanitizing.
89 * @param {Object} data
90 * @returns {Boolean}
91 */
92 sanitizationNeeded(data) {
93 return this.dataSanitizers.some((sanitizer) => sanitizer(data, false));
94 }
95
96
97 /**
98 * Mogrify data.
99 * @param {Object} data
100 */
101 sanitize(data) {
102 this.dataSanitizers.forEach((sanitizer) => sanitizer(data));
103 }
104
105
106 /**
107 * Convert data into JSON.
108 * @param {String} _key
109 * @param {*} value
110 * @returns {String} serialized value
111 */
112 jsonReplacer(key, value) {
113 let replaced;
114
115 // Try applying all our replacers, until one does something.
116 this.jsonReplacers.every((replacer) => {
117 ({ replaced, value } = replacer(key, value));
118 return !replaced;
119 });
120 return value;
121 }
122
123 }
124
125 module.exports = Logger;