-
-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathLogMessage.js
259 lines (222 loc) · 7.04 KB
/
LogMessage.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
const stringify = require('fast-safe-stringify');
const symbols = {
LOG: Symbol('log'),
META: Symbol('meta'),
ERROR: Symbol('error'),
OPTS: Symbol('opts')
};
/**
* The LogMessage class is a private/internal class that is used for generating log messages. All log methods return an instance of LogMessage allowing for a chainable api.
* Having a seperate class and instance for each log allows chaining and the ability to further customize this module in the future without major breaking changes. The documentation
* provided here is what is available to you for each log message.
*/
class LogMessage {
/**
* Constructor for LogMessage
* @class
* @param {object} log Object containing all the information for a log.
* @param {string} log.level The log level.
* @param {*} log.msg The message for the log.
* @param {object} [log.meta] Metadata attached to the log.
* @param {string[]|Function[]} [log.tags] Additional tags to attach to the log.
* @param {object} opts Configuration options from LambdaLog.
*/
constructor(log, opts) {
this[symbols.LOG] = log;
this[symbols.META] = {};
this[symbols.ERROR] = null;
this[symbols.OPTS] = opts;
const { meta, tags } = this[symbols.LOG];
if(meta && (typeof meta !== 'object' || Array.isArray(meta))) {
this[symbols.LOG].meta = { meta };
}
if(!meta) this[symbols.LOG].meta = {};
if(!tags) this[symbols.LOG].tags = [];
// If `msg` is an Error-like object, use the message and add the `stack` to `meta`
if(LogMessage.isError(log.msg)) {
const err = log.msg;
this[symbols.ERROR] = err;
this[symbols.META].stack = err.stack;
this[symbols.LOG].msg = err.message;
}
}
/**
* String log level of the message.
* @type {string}
*/
get level() {
return this[symbols.LOG].level;
}
/**
* The message for the log. If an Error was provided, it will be the message of the error.
* @type {string}
*/
get msg() {
return this[symbols.LOG].msg;
}
/**
* Update the message for this log to something else.
* @param {string} msg A string to update the message with.
*/
set msg(msg) {
this[symbols.LOG].msg = msg;
}
/**
* Alias for `this.msg`.
* @type {string}
*/
get message() {
return this.msg;
}
/**
* Alias for `this.msg = 'New message';`
* @param {string} msg A string to update the message with.
*/
set message(msg) {
this.msg = msg;
}
/**
* The fully compiled metadata object for the log. Includes global and dynamic metadata.
* @type {object}
*/
get meta() {
const opts = this[symbols.OPTS];
let meta = {
...this[symbols.META],
...this[symbols.OPTS].meta,
...this[symbols.LOG].meta
};
if(opts.dynamicMeta && typeof opts.dynamicMeta === 'function') {
const dynMeta = opts.dynamicMeta.call(this, this, opts);
if(typeof dynMeta === 'object') {
meta = Object.assign(meta, dynMeta);
}
}
for(const [key, val] of Object.entries(meta)) {
if(typeof val !== 'object') continue;
if(LogMessage.isError(val)) {
meta[key] = LogMessage.stubError(val);
}
}
return meta;
}
/**
* Set additional metadata on the log message.
* @param {object} obj An object with properties to append or overwrite in the metadata.
*/
set meta(obj) {
this[symbols.LOG].meta = {
...this[symbols.LOG].meta,
...obj
};
}
/**
* Array of tags attached to this log. Includes global tags.
* @type {string[]}
*/
get tags() {
const opts = this[symbols.OPTS];
const tags = [].concat(opts.tags, this[symbols.LOG].tags);
return tags.map(tag => {
if(typeof tag === 'function') {
return tag.call(this, {
level: this.level,
meta: this.meta,
options: opts
});
}
const hasVar = tag.match(/(<<(.*)>>)/);
if(!hasVar) return tag;
const varName = hasVar[2];
if(varName === 'level') return tag.replace(hasVar[1], this.level);
return tag;
}).filter(tag => tag !== null && tag !== undefined && tag !== '');
}
/**
* Appends additional tags to this log message.
* @param {string[]|Function[]} tags Array of string tags or enhanced tag functions to append to the tags array.
*/
set tags(tags) {
this[symbols.LOG].tags = this[symbols.LOG].tags.concat(tags);
}
/**
* The full log object. This is the object used in logMessage.toJSON() and when the log is written to the console.
* @returns {object} The full log object.
*/
get value() {
const opts = this[symbols.OPTS];
return {
[opts.levelKey]: opts.levelKey ? this.level : undefined,
[opts.messageKey]: this.msg,
...this.meta,
[opts.tagsKey]: opts.tagsKey ? this.tags : undefined
};
}
/**
* Alias of `logMessage.value`.
* @returns {object} The full log object.
*/
get log() {
return this.value;
}
/**
* Throws the log. If an error was not provided, one will be generated for you and thrown. This is useful in cases where you need to log an
* error, but also throw it.
* @throws {Error} The provided error, or a newly generated error.
*/
get throw() {
const err = this[symbols.ERROR] || new Error(this.msg);
err.log = this;
throw err;
}
/**
* Returns the compiled log object converted into JSON. This method utilizes `options.replacer` for the replacer function. It also uses
* [fast-safe-stringify](https://www.npmjs.com/package/fast-safe-stringify) to prevent circular reference issues.
* @param {boolean} [format=false] Enable pretty-printing of the JSON object (4 space indentation).
* @returns {string} Log object stringified as JSON.
*/
toJSON(format) {
return stringify(this.value, this[symbols.OPTS].replacer || null, format ? 4 : 0);
}
/**
* Checks if value is an Error or Error-like object
* @static
* @param {*} val Value to test
* @returns {boolean} Whether the value is an Error or Error-like object
*/
static isError(val) {
return Boolean(val) && typeof val === 'object' && (
val instanceof Error || (
Object.prototype.hasOwnProperty.call(val, 'message') &&
Object.prototype.hasOwnProperty.call(val, 'stack')
)
);
}
/**
* Stubs an Error or Error-like object to include a toJSON method.
* @static
* @param {Error} err An Error or Error-like object.
* @returns {Error} The original error stubbed with a toJSON() method.
*/
static stubError(err) {
if(typeof err.toJSON === 'function') return err;
err.toJSON = function () {
const keys = [
'name',
'message',
'stack'
].concat(Object.keys(err));
return keys.reduce((obj, key) => {
if(key in err) {
const val = err[key];
if(typeof val === 'function') return obj;
obj[key] = val;
}
return obj;
}, {});
};
return err;
}
}
LogMessage.symbols = symbols;
module.exports = LogMessage;