@mmutils/logger 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +901 -2
- package/dist/cjs/index.cjs +72 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/levels.cjs +82 -0
- package/dist/cjs/levels.cjs.map +1 -0
- package/dist/cjs/logger.cjs +576 -0
- package/dist/cjs/logger.cjs.map +1 -0
- package/dist/cjs/outputs.cjs +24 -0
- package/dist/cjs/outputs.cjs.map +1 -0
- package/dist/cjs/plugins/columns.cjs +782 -0
- package/dist/cjs/plugins/columns.cjs.map +1 -0
- package/dist/cjs/plugins/graph.cjs +578 -0
- package/dist/cjs/plugins/graph.cjs.map +1 -0
- package/dist/cjs/plugins/json.cjs +644 -0
- package/dist/cjs/plugins/json.cjs.map +1 -0
- package/dist/cjs/plugins/message.cjs +108 -0
- package/dist/cjs/plugins/message.cjs.map +1 -0
- package/dist/cjs/plugins/shared.cjs +194 -0
- package/dist/cjs/plugins/shared.cjs.map +1 -0
- package/dist/cjs/time.cjs +234 -0
- package/dist/cjs/time.cjs.map +1 -0
- package/dist/cjs/utils/text.cjs +646 -0
- package/dist/cjs/utils/text.cjs.map +1 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/levels.js +72 -0
- package/dist/esm/levels.js.map +1 -0
- package/dist/esm/logger.js +572 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/outputs.js +21 -0
- package/dist/esm/outputs.js.map +1 -0
- package/dist/esm/plugins/columns.js +778 -0
- package/dist/esm/plugins/columns.js.map +1 -0
- package/dist/esm/plugins/graph.js +572 -0
- package/dist/esm/plugins/graph.js.map +1 -0
- package/dist/esm/plugins/json.js +641 -0
- package/dist/esm/plugins/json.js.map +1 -0
- package/dist/esm/plugins/message.js +105 -0
- package/dist/esm/plugins/message.js.map +1 -0
- package/dist/esm/plugins/shared.js +187 -0
- package/dist/esm/plugins/shared.js.map +1 -0
- package/dist/esm/time.js +231 -0
- package/dist/esm/time.js.map +1 -0
- package/dist/esm/utils/text.js +621 -0
- package/dist/esm/utils/text.js.map +1 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/levels.d.ts +50 -0
- package/dist/types/logger.d.ts +245 -0
- package/dist/types/outputs.d.ts +12 -0
- package/dist/types/plugins/columns.d.ts +151 -0
- package/dist/types/plugins/graph.d.ts +243 -0
- package/dist/types/plugins/json.d.ts +169 -0
- package/dist/types/plugins/message.d.ts +181 -0
- package/dist/types/plugins/shared.d.ts +74 -0
- package/dist/types/time.d.ts +48 -0
- package/dist/types/types.d.ts +176 -0
- package/dist/types/utils/text.d.ts +231 -0
- package/package.json +85 -4
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var levels = require('../levels.cjs');
|
|
4
|
+
var text = require('../utils/text.cjs');
|
|
5
|
+
var time = require('../time.cjs');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Most causes serialized per error; deeper causes are summarized as
|
|
9
|
+
* `"Name: message"`. A cause already in the chain is `"[Circular]"`.
|
|
10
|
+
*/
|
|
11
|
+
const MAX_CAUSES = 5;
|
|
12
|
+
const NO_CAUSES = [];
|
|
13
|
+
/**
|
|
14
|
+
* Depth of a value in the logged data. Values at `DATA` (the data itself)
|
|
15
|
+
* and `PROPERTY` (its properties or elements) get special handling: errors,
|
|
16
|
+
* binary data, bigints, functions, long strings and arrays. Objects at `DATA`
|
|
17
|
+
* are also walked key by key when native JSON would lose something. `LEAF`
|
|
18
|
+
* values (error properties) are handled like `PROPERTY` ones, except nested
|
|
19
|
+
* errors, which are summarized, so error graphs can't multiply.
|
|
20
|
+
*/
|
|
21
|
+
const DATA = 0;
|
|
22
|
+
const PROPERTY = 1;
|
|
23
|
+
const LEAF = 2;
|
|
24
|
+
const quote = (text) => JSON.stringify(text);
|
|
25
|
+
const quoteLimited = (text$1, limits) => JSON.stringify(text.truncate(text$1, limits.stringLength));
|
|
26
|
+
const placeholder = (prefix, thrown, limits) => quoteLimited(`[${prefix}: ${text.describeThrown(thrown)}]`, limits);
|
|
27
|
+
/**
|
|
28
|
+
* Appends `"key":json` to the inside of an object. Values without JSON
|
|
29
|
+
* (`undefined`) are left out, like `JSON.stringify` does.
|
|
30
|
+
*/
|
|
31
|
+
const appendProperty = (text, quotedKey, json) => json === undefined ? text : `${text}${text ? ',' : ''}${quotedKey}:${json}`;
|
|
32
|
+
/**
|
|
33
|
+
* Levels a too long value is cut into while keeping its type; deeper, the
|
|
34
|
+
* value becomes a cut string.
|
|
35
|
+
*/
|
|
36
|
+
const MAX_CUT_LEVELS = 4;
|
|
37
|
+
const NO_ANCESTORS = [];
|
|
38
|
+
/**
|
|
39
|
+
* Objects native JSON writes as objects from their own enumerable keys: not
|
|
40
|
+
* arrays, and without `toJSON` (dates are written as strings).
|
|
41
|
+
*/
|
|
42
|
+
const isKeyedObject = (value) => typeof value === 'object' &&
|
|
43
|
+
value !== null &&
|
|
44
|
+
!Array.isArray(value) &&
|
|
45
|
+
typeof value.toJSON !== 'function';
|
|
46
|
+
/**
|
|
47
|
+
* Native JSON of a value, like `JSON.stringify` (`undefined` for functions,
|
|
48
|
+
* symbols and `undefined`), or a placeholder if it can't be serialized.
|
|
49
|
+
*/
|
|
50
|
+
const elementJson = (value, limits) => {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
}
|
|
54
|
+
catch (thrown) {
|
|
55
|
+
return placeholder('Unserializable', thrown, limits);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* JSON of an element or property inside a cut value: `"[Circular]"` if it
|
|
60
|
+
* is one of the values being cut around it, native JSON when that works,
|
|
61
|
+
* otherwise (a cycle further down, a nested bigint) cut the same way, so it
|
|
62
|
+
* keeps its type too. A throwing getter gives a placeholder.
|
|
63
|
+
*
|
|
64
|
+
* @param ancestors The values being cut, the container included.
|
|
65
|
+
*/
|
|
66
|
+
const childJson = (read, room, limits, ancestors) => {
|
|
67
|
+
let child;
|
|
68
|
+
try {
|
|
69
|
+
child = read();
|
|
70
|
+
}
|
|
71
|
+
catch (thrown) {
|
|
72
|
+
return placeholder('Getter threw', thrown, limits);
|
|
73
|
+
}
|
|
74
|
+
if (typeof child === 'object' && child !== null && ancestors.includes(child))
|
|
75
|
+
return '"[Circular]"';
|
|
76
|
+
try {
|
|
77
|
+
return JSON.stringify(child);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return cutJson(child, room, limits, ancestors);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* JSON of a value that is too long, cut to about `budget` characters while
|
|
85
|
+
* keeping its type, so collectors that map fields by type (Elasticsearch)
|
|
86
|
+
* accept the line: see `cutArray` and `cutObject`. Other values (strings,
|
|
87
|
+
* dates, values deeper than `MAX_CUT_LEVELS`) become a cut string. A value
|
|
88
|
+
* that contains itself is `"[Circular]"`.
|
|
89
|
+
*
|
|
90
|
+
* @param ancestors Arrays and objects being cut around this value.
|
|
91
|
+
*/
|
|
92
|
+
const cutJson = (value, budget, limits, ancestors) => {
|
|
93
|
+
if (typeof value === 'object' && value !== null && ancestors.includes(value))
|
|
94
|
+
return '"[Circular]"';
|
|
95
|
+
if (ancestors.length < MAX_CUT_LEVELS) {
|
|
96
|
+
if (Array.isArray(value))
|
|
97
|
+
return cutArray(value, budget, limits, ancestors);
|
|
98
|
+
if (isKeyedObject(value))
|
|
99
|
+
return cutObject(value, budget, limits, ancestors);
|
|
100
|
+
}
|
|
101
|
+
// a JSON number with every digit, like bigints elsewhere in the data
|
|
102
|
+
if (typeof value === 'bigint')
|
|
103
|
+
return String(value);
|
|
104
|
+
const json = elementJson(value, limits) ?? 'null';
|
|
105
|
+
if (json.length <= budget)
|
|
106
|
+
return json;
|
|
107
|
+
// cut the text of strings (also placeholders), the JSON of other values
|
|
108
|
+
const text$1 = typeof value === 'string' ? value
|
|
109
|
+
: json.startsWith('"') ? JSON.parse(json)
|
|
110
|
+
: json;
|
|
111
|
+
return quote(text.truncate(text$1, Math.max(1, budget - 2)));
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* An array cut to about `budget` characters: elements while they fit, at
|
|
115
|
+
* most `items`, then a `"… N more items"` element. A first element that
|
|
116
|
+
* alone doesn't fit is cut the same way. The cost grows with `budget`, not
|
|
117
|
+
* with the array: elements are serialized one by one until one doesn't fit.
|
|
118
|
+
*/
|
|
119
|
+
const cutArray = (array, budget, limits, ancestors) => {
|
|
120
|
+
const max = Math.min(array.length, limits.items);
|
|
121
|
+
const inner = [...ancestors, array];
|
|
122
|
+
let text$1 = '';
|
|
123
|
+
let shown = 0;
|
|
124
|
+
for (; shown < max; shown++) {
|
|
125
|
+
const index = shown;
|
|
126
|
+
// room inside the brackets, after the separator
|
|
127
|
+
const room = budget - text$1.length - 2 - (shown ? 1 : 0);
|
|
128
|
+
const json = childJson(() => array[index], room, limits, inner) ?? 'null';
|
|
129
|
+
if (json.length <= room) {
|
|
130
|
+
text$1 += `${shown ? ',' : ''}${json}`;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!shown) {
|
|
134
|
+
text$1 = cutJson(array[0], room, limits, inner);
|
|
135
|
+
shown = 1;
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
const more = array.length - shown;
|
|
140
|
+
return `[${text$1}${more ? `${text$1 ? ',' : ''}${quote(text.describeLeftOut(more, 'item'))}` : ''}]`;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* An object cut to about `budget` characters: properties while they fit, at
|
|
144
|
+
* most `items`, then `"…": "… N more items"`. A first property whose value
|
|
145
|
+
* alone doesn't fit is cut the same way.
|
|
146
|
+
*/
|
|
147
|
+
const cutObject = (object, budget, limits, ancestors) => {
|
|
148
|
+
const keys = Object.keys(object);
|
|
149
|
+
const max = Math.min(keys.length, limits.items);
|
|
150
|
+
const inner = [...ancestors, object];
|
|
151
|
+
const read = (key) => object[key];
|
|
152
|
+
let text$1 = '';
|
|
153
|
+
let shown = 0;
|
|
154
|
+
for (; shown < max; shown++) {
|
|
155
|
+
const key = keys[shown] ?? '';
|
|
156
|
+
const property = `${quoteLimited(key, limits)}:`;
|
|
157
|
+
const room = budget - text$1.length - 2 - (text$1 ? 1 : 0) - property.length;
|
|
158
|
+
const json = childJson(() => read(key), room, limits, inner);
|
|
159
|
+
// left out by JSON (functions, symbols, undefined): counted as shown
|
|
160
|
+
if (json === undefined)
|
|
161
|
+
continue;
|
|
162
|
+
if (json.length <= room) {
|
|
163
|
+
text$1 += `${text$1 ? ',' : ''}${property}${json}`;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!text$1) {
|
|
167
|
+
text$1 = `${property}${cutJson(read(key), room, limits, inner)}`;
|
|
168
|
+
shown++;
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
const more = keys.length - shown;
|
|
173
|
+
return `{${text$1}${more ? `${text$1 ? ',' : ''}"…":${quote(text.describeLeftOut(more, 'item'))}` : ''}}`;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Counts own enumerable keys, stopping after `max + 1`: enough to know if
|
|
177
|
+
* an object has more than `max` without listing all keys.
|
|
178
|
+
*/
|
|
179
|
+
const countKeys = (object, max) => {
|
|
180
|
+
let count = 0;
|
|
181
|
+
// no hasOwn check (it doubles the cost): enumerable keys added to
|
|
182
|
+
// Object.prototype by other code can only overcount, which cuts with
|
|
183
|
+
// Object.keys and still writes the object's real keys
|
|
184
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- counting
|
|
185
|
+
for (const _ in object)
|
|
186
|
+
if (++count > max)
|
|
187
|
+
break;
|
|
188
|
+
return count;
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Native JSON of an object. A text longer than `stringLength` is cut
|
|
192
|
+
* keeping its type (`cutJson`); plain objects with more than `items` keys
|
|
193
|
+
* are cut the same way. Objects JSON shows as `{}` (maps, sets, regular
|
|
194
|
+
* expressions) are described instead.
|
|
195
|
+
*
|
|
196
|
+
* @throws Whatever `JSON.stringify` throws: callers catch it.
|
|
197
|
+
*/
|
|
198
|
+
const nativeJson = (value, limits) => {
|
|
199
|
+
if (isKeyedObject(value) && countKeys(value, limits.items) > limits.items)
|
|
200
|
+
return cutObject(value, limits.stringLength, limits, NO_ANCESTORS);
|
|
201
|
+
const text$1 = JSON.stringify(value);
|
|
202
|
+
if (text$1 === undefined)
|
|
203
|
+
return undefined;
|
|
204
|
+
if (text$1 === '{}') {
|
|
205
|
+
const described = text.describeObject(value, limits);
|
|
206
|
+
if (described !== undefined)
|
|
207
|
+
return quoteLimited(described, limits);
|
|
208
|
+
}
|
|
209
|
+
return text$1.length > limits.stringLength ?
|
|
210
|
+
cutJson(value, limits.stringLength, limits, NO_ANCESTORS)
|
|
211
|
+
: text$1;
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* Prototypes whose instances native JSON serializes completely, as long as
|
|
215
|
+
* their own values do: plain objects, null-prototype objects, arrays, dates.
|
|
216
|
+
*/
|
|
217
|
+
const isPlainPrototype = (proto) => proto === Object.prototype ||
|
|
218
|
+
proto === null ||
|
|
219
|
+
proto === Array.prototype ||
|
|
220
|
+
proto === Date.prototype;
|
|
221
|
+
/**
|
|
222
|
+
* A property value native JSON serializes the same as the walk does, within
|
|
223
|
+
* the limits. One cheap check per value of the logged data, so ordinary data
|
|
224
|
+
* keeps the speed of a single native `JSON.stringify`.
|
|
225
|
+
*/
|
|
226
|
+
const isNativeSafe = (value, limits) => {
|
|
227
|
+
switch (typeof value) {
|
|
228
|
+
case 'string':
|
|
229
|
+
return value.length <= limits.stringLength;
|
|
230
|
+
case 'number':
|
|
231
|
+
case 'boolean':
|
|
232
|
+
case 'undefined':
|
|
233
|
+
return true;
|
|
234
|
+
case 'object':
|
|
235
|
+
return (value === null ||
|
|
236
|
+
(isPlainPrototype(Object.getPrototypeOf(value)) &&
|
|
237
|
+
!(Array.isArray(value) && value.length > limits.items)));
|
|
238
|
+
default:
|
|
239
|
+
// bigint (native JSON throws), function and symbol (left out)
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* Data whose values are all native safe: one native `JSON.stringify`.
|
|
245
|
+
* `undefined` when a value is not, or reading or serializing throws
|
|
246
|
+
* (circular structures, throwing getters, revoked proxies, nested bigints),
|
|
247
|
+
* or the text exceeds `stringLength`: the data is walked instead.
|
|
248
|
+
*/
|
|
249
|
+
const tryNative = (data, isArray, limits) => {
|
|
250
|
+
try {
|
|
251
|
+
if (isArray) {
|
|
252
|
+
const array = data;
|
|
253
|
+
const { length } = array;
|
|
254
|
+
if (length > limits.items)
|
|
255
|
+
return undefined;
|
|
256
|
+
for (let index = 0; index < length; index++)
|
|
257
|
+
if (!isNativeSafe(array[index], limits))
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
let count = 0;
|
|
262
|
+
for (const key in data)
|
|
263
|
+
if (++count > limits.items ||
|
|
264
|
+
!isNativeSafe(data[key], limits))
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
const text = JSON.stringify(data);
|
|
268
|
+
if (text.length > limits.stringLength)
|
|
269
|
+
return undefined;
|
|
270
|
+
// a key takes at least 5 characters (`"":0,`): shorter text can't hold
|
|
271
|
+
// an object with more than `items` keys, so ordinary data skips the scan
|
|
272
|
+
if (text.length > MIN_KEY_LENGTH * limits.items &&
|
|
273
|
+
hasLongObject(data, isArray, limits))
|
|
274
|
+
return undefined;
|
|
275
|
+
return text;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* Shortest JSON of an object key with its value and separator: `"":0,`.
|
|
283
|
+
*/
|
|
284
|
+
const MIN_KEY_LENGTH = 5;
|
|
285
|
+
/**
|
|
286
|
+
* Whether a value of the data (one level deep, like errors) is an object
|
|
287
|
+
* with more than `items` keys, which native JSON would write whole.
|
|
288
|
+
*/
|
|
289
|
+
const hasLongObject = (data, isArray, limits) => {
|
|
290
|
+
const isLong = (value) => isKeyedObject(value) && countKeys(value, limits.items) > limits.items;
|
|
291
|
+
if (isArray) {
|
|
292
|
+
for (const value of data)
|
|
293
|
+
if (isLong(value))
|
|
294
|
+
return true;
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
// no Object.values: it allocates an array per log
|
|
298
|
+
for (const key in data)
|
|
299
|
+
if (isLong(data[key]))
|
|
300
|
+
return true;
|
|
301
|
+
return false;
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* JSON of a property, element or error property. A throwing getter gives a
|
|
305
|
+
* placeholder, so one bad property doesn't hide the others.
|
|
306
|
+
*/
|
|
307
|
+
const readProperty = (data, key, depth, limits) => {
|
|
308
|
+
let value;
|
|
309
|
+
try {
|
|
310
|
+
value = data[key];
|
|
311
|
+
}
|
|
312
|
+
catch (thrown) {
|
|
313
|
+
return placeholder('Getter threw', thrown, limits);
|
|
314
|
+
}
|
|
315
|
+
return stringifyValue(value, depth, limits);
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Logged data that is an array: each element serialized on its own (errors,
|
|
319
|
+
* binary data, ...), at most `items`, then a `"… N more items"` element.
|
|
320
|
+
*/
|
|
321
|
+
const walkArray = (array, limits) => {
|
|
322
|
+
const shown = Math.min(array.length, limits.items);
|
|
323
|
+
let text$1 = '';
|
|
324
|
+
for (let index = 0; index < shown; index++)
|
|
325
|
+
text$1 += `${index ? ',' : ''}${readProperty(array, index, PROPERTY, limits) ?? 'null'}`;
|
|
326
|
+
if (array.length > shown)
|
|
327
|
+
text$1 += `,${quote(text.describeLeftOut(array.length - shown, 'item'))}`;
|
|
328
|
+
return `[${text$1}]`;
|
|
329
|
+
};
|
|
330
|
+
/**
|
|
331
|
+
* Logged data that is an object: own enumerable keys (like JSON), each value
|
|
332
|
+
* serialized on its own, at most `items` keys, then `"…": "… N more items"`.
|
|
333
|
+
* A throwing getter gives a placeholder and the other keys still render.
|
|
334
|
+
*/
|
|
335
|
+
const walkObject = (data, limits) => {
|
|
336
|
+
if (typeof data.toJSON === 'function')
|
|
337
|
+
return nativeJson(data, limits);
|
|
338
|
+
const keys = Object.keys(data);
|
|
339
|
+
const shown = Math.min(keys.length, limits.items);
|
|
340
|
+
let text$1 = '';
|
|
341
|
+
for (let index = 0; index < shown; index++) {
|
|
342
|
+
const key = keys[index] ?? '';
|
|
343
|
+
text$1 = appendProperty(text$1, quoteLimited(key, limits), readProperty(data, key, PROPERTY, limits));
|
|
344
|
+
}
|
|
345
|
+
if (keys.length > shown)
|
|
346
|
+
text$1 = appendProperty(text$1, '"…"', quote(text.describeLeftOut(keys.length - shown, 'item')));
|
|
347
|
+
return `{${text$1}}`;
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* A property or element that is an array: longer than `items`, its first
|
|
351
|
+
* `items` elements (native JSON) and a `"… N more items"` element, without
|
|
352
|
+
* serializing the rest.
|
|
353
|
+
*/
|
|
354
|
+
const limitArray = (array, limits) => {
|
|
355
|
+
if (array.length <= limits.items)
|
|
356
|
+
return nativeJson(array, limits);
|
|
357
|
+
const head = JSON.stringify(array.slice(0, limits.items));
|
|
358
|
+
const text$1 = `${head.slice(0, -1)},${quote(text.describeLeftOut(array.length - limits.items, 'item'))}]`;
|
|
359
|
+
return text$1.length > limits.stringLength ?
|
|
360
|
+
cutArray(array, limits.stringLength, limits, NO_ANCESTORS)
|
|
361
|
+
: text$1;
|
|
362
|
+
};
|
|
363
|
+
/**
|
|
364
|
+
* Error properties written first (or last, `cause`), not repeated when an
|
|
365
|
+
* error makes them enumerable.
|
|
366
|
+
*/
|
|
367
|
+
const ERROR_KEYS = new Set(['name', 'message', 'stack', 'cause']);
|
|
368
|
+
const isErrorLike = (value) => value instanceof Error ||
|
|
369
|
+
(typeof value === 'object' && value !== null && text.isForeignError(value));
|
|
370
|
+
/**
|
|
371
|
+
* `{"name","message","stack",...own enumerable properties,"cause"}`. Name,
|
|
372
|
+
* message and stack are not enumerable, so native JSON drops them. Each part
|
|
373
|
+
* is read on its own: an unreadable one gives a placeholder, the others
|
|
374
|
+
* still render.
|
|
375
|
+
*
|
|
376
|
+
* @param chain Errors this one is the cause of, outermost first.
|
|
377
|
+
*/
|
|
378
|
+
const stringifyError = (error, chain, limits) => {
|
|
379
|
+
let text$1 = appendProperty('', '"name"', readProperty(error, 'name', LEAF, limits));
|
|
380
|
+
text$1 = appendProperty(text$1, '"message"', readProperty(error, 'message', LEAF, limits));
|
|
381
|
+
text$1 = appendProperty(text$1, '"stack"', readProperty(error, 'stack', LEAF, limits));
|
|
382
|
+
let keys = [];
|
|
383
|
+
try {
|
|
384
|
+
keys = Object.keys(error).filter((key) => !ERROR_KEYS.has(key));
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// keys of a throwing proxy are left out; name, message and stack stay
|
|
388
|
+
}
|
|
389
|
+
const shown = Math.min(keys.length, limits.items);
|
|
390
|
+
for (let index = 0; index < shown; index++) {
|
|
391
|
+
const key = keys[index] ?? '';
|
|
392
|
+
text$1 = appendProperty(text$1, quoteLimited(key, limits), readProperty(error, key, LEAF, limits));
|
|
393
|
+
}
|
|
394
|
+
if (keys.length > shown)
|
|
395
|
+
text$1 = appendProperty(text$1, '"…"', quote(text.describeLeftOut(keys.length - shown, 'item')));
|
|
396
|
+
let cause;
|
|
397
|
+
try {
|
|
398
|
+
if (!Object.hasOwn(error, 'cause'))
|
|
399
|
+
return `{${text$1}}`;
|
|
400
|
+
cause = error.cause;
|
|
401
|
+
}
|
|
402
|
+
catch (thrown) {
|
|
403
|
+
return `{${appendProperty(text$1, '"cause"', placeholder('Getter threw', thrown, limits))}}`;
|
|
404
|
+
}
|
|
405
|
+
const json = !isErrorLike(cause) ? stringifyValue(cause, LEAF, limits)
|
|
406
|
+
: cause === error || chain.includes(cause) ? '"[Circular]"'
|
|
407
|
+
: chain.length < MAX_CAUSES ?
|
|
408
|
+
stringifyError(cause, [...chain, error], limits)
|
|
409
|
+
: quoteLimited(text.describeThrown(cause), limits);
|
|
410
|
+
return `{${appendProperty(text$1, '"cause"', json)}}`;
|
|
411
|
+
};
|
|
412
|
+
/**
|
|
413
|
+
* Native JSON of a nested array or object, or, when native JSON throws (a
|
|
414
|
+
* circular element, a throwing getter, a nested bigint), the same value cut
|
|
415
|
+
* element by element: the bad element becomes a placeholder and the value
|
|
416
|
+
* keeps its type.
|
|
417
|
+
*/
|
|
418
|
+
const keepingType = (native, value, limits) => {
|
|
419
|
+
try {
|
|
420
|
+
return native();
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
return cutJson(value, limits.stringLength, limits, NO_ANCESTORS);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
const stringifyObject = (value, depth, limits) => {
|
|
427
|
+
const proto = Object.getPrototypeOf(value);
|
|
428
|
+
if (proto === Object.prototype || proto === null)
|
|
429
|
+
return depth === DATA ?
|
|
430
|
+
(tryNative(value, false, limits) ?? walkObject(value, limits))
|
|
431
|
+
: keepingType(() => nativeJson(value, limits), value, limits);
|
|
432
|
+
if (Array.isArray(value))
|
|
433
|
+
return depth === DATA ?
|
|
434
|
+
(tryNative(value, true, limits) ?? walkArray(value, limits))
|
|
435
|
+
: keepingType(() => limitArray(value, limits), value, limits);
|
|
436
|
+
if (value instanceof Error || text.isForeignError(value))
|
|
437
|
+
return depth === LEAF ?
|
|
438
|
+
quoteLimited(text.describeThrown(value), limits)
|
|
439
|
+
: stringifyError(value, NO_CAUSES, limits);
|
|
440
|
+
if (ArrayBuffer.isView(value))
|
|
441
|
+
return quoteLimited(text.describeBinary(value, limits.items), limits);
|
|
442
|
+
// dates, class instances, maps, sets, ...
|
|
443
|
+
return nativeJson(value, limits);
|
|
444
|
+
};
|
|
445
|
+
/**
|
|
446
|
+
* JSON of a value, or `undefined` for values JSON leaves out.
|
|
447
|
+
*/
|
|
448
|
+
const stringifyValue = (value, depth, limits) => {
|
|
449
|
+
try {
|
|
450
|
+
switch (typeof value) {
|
|
451
|
+
case 'string':
|
|
452
|
+
return value.length > limits.stringLength ?
|
|
453
|
+
quoteLimited(value, limits)
|
|
454
|
+
: quote(value);
|
|
455
|
+
case 'number':
|
|
456
|
+
return Number.isFinite(value) ? String(value) : 'null';
|
|
457
|
+
case 'boolean':
|
|
458
|
+
return value ? 'true' : 'false';
|
|
459
|
+
case 'bigint':
|
|
460
|
+
// a JSON number with every digit; `JSON.parse` rounds large ones
|
|
461
|
+
return String(value);
|
|
462
|
+
case 'undefined':
|
|
463
|
+
return undefined;
|
|
464
|
+
case 'function':
|
|
465
|
+
return quote(text.describeFunction(value));
|
|
466
|
+
case 'symbol':
|
|
467
|
+
return quoteLimited(String(value), limits);
|
|
468
|
+
default:
|
|
469
|
+
// objects: the remaining type
|
|
470
|
+
return value === null ? 'null' : (stringifyObject(value, depth, limits));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
catch (thrown) {
|
|
474
|
+
return placeholder('Unserializable', thrown, limits);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
/**
|
|
478
|
+
* Serializes logged data to JSON text. Never throws, and always returns
|
|
479
|
+
* valid JSON (or `undefined`, like `JSON.stringify`, for `undefined`):
|
|
480
|
+
* - ordinary data is one native `JSON.stringify`;
|
|
481
|
+
* - errors, also as property values and from other realms:
|
|
482
|
+
* `{"name","message","stack",...own properties,"cause"}`, causes up to 5
|
|
483
|
+
* deep;
|
|
484
|
+
* - buffers and typed arrays: a one-line description (`describeBinary`);
|
|
485
|
+
* - bigints: JSON numbers with every digit; functions and symbols:
|
|
486
|
+
* `"[Function: name]"`, `"Symbol(x)"`, never source code;
|
|
487
|
+
* - maps, sets and regular expressions: a description (`describeObject`);
|
|
488
|
+
* - strings longer than `stringLength`: cut, with `… N more characters`;
|
|
489
|
+
* - arrays and objects longer than `items` (the data and its direct
|
|
490
|
+
* properties): the first `items` and `"… N more items"`;
|
|
491
|
+
* - arrays and objects whose JSON is longer than `stringLength`: cut
|
|
492
|
+
* structurally, keeping their type (`cutJson`);
|
|
493
|
+
* - throwing getters and revoked proxies: placeholder strings; circular
|
|
494
|
+
* references: `"[Circular]"`; the rest of the data still renders and
|
|
495
|
+
* keeps its type.
|
|
496
|
+
*
|
|
497
|
+
* Errors, binary data, bigints, functions and long arrays are found one
|
|
498
|
+
* level deep only: in the data and in its direct properties (`{ err }`).
|
|
499
|
+
* This is for performance: ordinary data is one native `JSON.stringify`,
|
|
500
|
+
* and a walker for every object would slow down all logs. Deeper ones get
|
|
501
|
+
* native JSON, so a deeper error keeps only its own enumerable properties
|
|
502
|
+
* (`{ request: { err } }` loses message and stack).
|
|
503
|
+
*
|
|
504
|
+
* @param limits Output limits. Default: `DEFAULT_OUTPUT_LIMITS`.
|
|
505
|
+
*/
|
|
506
|
+
const renderJsonData = (data, limits = text.DEFAULT_OUTPUT_LIMITS) => stringifyValue(data, DATA, limits);
|
|
507
|
+
const epochTime = (time, timeNs) => timeNs === undefined ?
|
|
508
|
+
String(time)
|
|
509
|
+
: `${String(time)},"ns":${String(timeNs)}`;
|
|
510
|
+
/**
|
|
511
|
+
* ISO 8601 in UTC, through the timestamp compiler: the text up to the
|
|
512
|
+
* seconds is cached per second.
|
|
513
|
+
*/
|
|
514
|
+
const createIsoTime = () => {
|
|
515
|
+
const milliseconds = time.compileTimestamp('["]YYYY-MM-DD[T]HH:mm:ss.SSS[Z"]', {
|
|
516
|
+
utc: true,
|
|
517
|
+
});
|
|
518
|
+
const nanoseconds = time.compileTimestamp('["]YYYY-MM-DD[T]HH:mm:ss.SSSSSSSSS[Z"]', {
|
|
519
|
+
utc: true,
|
|
520
|
+
});
|
|
521
|
+
return (time, timeNs) => timeNs === undefined ? milliseconds(time, 0) : nanoseconds(time, timeNs);
|
|
522
|
+
};
|
|
523
|
+
const createTimeFormatter = (time) => {
|
|
524
|
+
if (time === false)
|
|
525
|
+
return undefined;
|
|
526
|
+
if (time === undefined || time === 'iso')
|
|
527
|
+
return createIsoTime();
|
|
528
|
+
if (time === 'epoch')
|
|
529
|
+
return epochTime;
|
|
530
|
+
throw new RangeError(`jsonPlugin: time must be 'iso', 'epoch' or false, got ${typeof time === 'string' ? `'${time}'` : typeof time}.`);
|
|
531
|
+
};
|
|
532
|
+
const DEL_C1 = /[\x7F-\x9F]/;
|
|
533
|
+
const DEL_C1_ALL = /[\x7F-\x9F]/g;
|
|
534
|
+
const escapeControl = (char) => `\\u00${char.charCodeAt(0).toString(16)}`;
|
|
535
|
+
/**
|
|
536
|
+
* Escapes DEL and C1 controls. They can only be inside JSON strings (the
|
|
537
|
+
* rest of a line is printable ASCII), where `\u00XX` is the same character.
|
|
538
|
+
*/
|
|
539
|
+
const escapeControls = (line) => DEL_C1.test(line) ? line.replace(DEL_C1_ALL, escapeControl) : line;
|
|
540
|
+
const keepLine = (line) => line;
|
|
541
|
+
/**
|
|
542
|
+
* JSON line of an entry: `id`, `time` (from `timestamp`), `type`, `kind`
|
|
543
|
+
* (only with `writeKind`), `data`, then properties added by transformers.
|
|
544
|
+
* The known properties are collected and joined by one template, which is
|
|
545
|
+
* faster than appending property by property.
|
|
546
|
+
*/
|
|
547
|
+
const renderJsonLine = (entry, time, ownKind, limits) => {
|
|
548
|
+
let id;
|
|
549
|
+
let type;
|
|
550
|
+
let kind;
|
|
551
|
+
let data;
|
|
552
|
+
let timed = false;
|
|
553
|
+
let extra = '';
|
|
554
|
+
for (const key in entry)
|
|
555
|
+
switch (key) {
|
|
556
|
+
case 'id':
|
|
557
|
+
id = stringifyValue(entry[key], DATA, limits);
|
|
558
|
+
break;
|
|
559
|
+
case 'timestamp':
|
|
560
|
+
timed = true;
|
|
561
|
+
break;
|
|
562
|
+
case 'type': {
|
|
563
|
+
const value = entry[key];
|
|
564
|
+
// log types need no escaping: skips a native call per log
|
|
565
|
+
type =
|
|
566
|
+
(typeof value === 'string' &&
|
|
567
|
+
typeof levels.LOG_TYPE_VALUES[value] === 'number') ?
|
|
568
|
+
`"${value}"`
|
|
569
|
+
: stringifyValue(value, DATA, limits);
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
case 'kind':
|
|
573
|
+
if (ownKind !== undefined)
|
|
574
|
+
kind =
|
|
575
|
+
entry[key] === ownKind.name ?
|
|
576
|
+
ownKind.json
|
|
577
|
+
: stringifyValue(entry[key], DATA, limits);
|
|
578
|
+
break;
|
|
579
|
+
case 'data':
|
|
580
|
+
data = stringifyValue(entry[key], DATA, limits);
|
|
581
|
+
break;
|
|
582
|
+
default:
|
|
583
|
+
extra = appendProperty(extra, quote(key), stringifyValue(entry[key], DATA, limits));
|
|
584
|
+
}
|
|
585
|
+
if (!timed)
|
|
586
|
+
time = undefined;
|
|
587
|
+
if (id !== undefined && type !== undefined && data !== undefined) {
|
|
588
|
+
const typed = kind === undefined ? type : `${type},"kind":${kind}`;
|
|
589
|
+
return time === undefined ?
|
|
590
|
+
`{"id":${id},"type":${typed},"data":${data}${extra && `,${extra}`}}`
|
|
591
|
+
: `{"id":${id},"time":${time},"type":${typed},"data":${data}${extra && `,${extra}`}}`;
|
|
592
|
+
}
|
|
593
|
+
// properties removed by transformers or `undefined` data
|
|
594
|
+
let text = appendProperty('', '"id"', id);
|
|
595
|
+
text = appendProperty(text, '"time"', time);
|
|
596
|
+
text = appendProperty(text, '"type"', type);
|
|
597
|
+
text = appendProperty(text, '"kind"', kind);
|
|
598
|
+
text = appendProperty(text, '"data"', data);
|
|
599
|
+
return `{${text}${text && extra ? ',' : ''}${extra}}`;
|
|
600
|
+
};
|
|
601
|
+
/**
|
|
602
|
+
* Writes logs of a given kind as JSON lines (NDJSON) and, by default, makes
|
|
603
|
+
* that kind the logger's default, so `logger.info({ user: 'alice' })`
|
|
604
|
+
* writes:
|
|
605
|
+
*
|
|
606
|
+
* ```
|
|
607
|
+
* {"id":1,"time":"2026-03-16T15:35:00.000Z","type":"info","data":{"user":"alice"}}
|
|
608
|
+
* ```
|
|
609
|
+
*
|
|
610
|
+
* Logging never throws because of the data; see `renderJsonData`. Errors of
|
|
611
|
+
* `write` propagate.
|
|
612
|
+
*
|
|
613
|
+
* @throws RangeError if `time` or `limits` are invalid.
|
|
614
|
+
* @throws TypeError if `write` is not a function.
|
|
615
|
+
*/
|
|
616
|
+
const jsonPlugin = (options = {}) => {
|
|
617
|
+
const kind = options.kind ?? 'json';
|
|
618
|
+
const limits = text.resolveOutputLimits(options.limits, 'jsonPlugin');
|
|
619
|
+
const formatTime = createTimeFormatter(options.time);
|
|
620
|
+
const escape = options.escapeControls === true ? escapeControls : keepLine;
|
|
621
|
+
// quoted once: the output only receives its own kind (unless a
|
|
622
|
+
// transformer changes it)
|
|
623
|
+
const ownKind = options.writeKind === true ? { name: kind, json: quote(kind) } : undefined;
|
|
624
|
+
// checked for untyped callers: it would otherwise throw on the first log
|
|
625
|
+
const write = options.write ?? console.log;
|
|
626
|
+
if (typeof write !== 'function')
|
|
627
|
+
throw new TypeError(`jsonPlugin: write is not a function, got ${write === null ? 'null' : typeof write}.`);
|
|
628
|
+
const writeLine = write;
|
|
629
|
+
return {
|
|
630
|
+
name: `json:${kind}`,
|
|
631
|
+
...((options.setAsDefault ?? true) && { defaultKind: kind }),
|
|
632
|
+
outputs: {
|
|
633
|
+
[kind]: (entry, { time, timeNs }) => {
|
|
634
|
+
writeLine(escape(renderJsonLine(entry, time === undefined || formatTime === undefined ?
|
|
635
|
+
undefined
|
|
636
|
+
: formatTime(time, timeNs), ownKind, limits)));
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
exports.jsonPlugin = jsonPlugin;
|
|
643
|
+
exports.renderJsonData = renderJsonData;
|
|
644
|
+
//# sourceMappingURL=json.cjs.map
|