@lakutata/logger 0.0.0-stage → 3.0.0-beta.1
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 +23 -0
- package/README.md +137 -2
- package/THIRD_PARTY_NOTICES.md +158 -0
- package/dist/cjs/Logger.d.ts +291 -0
- package/dist/cjs/Logger.js +185 -0
- package/dist/cjs/exports/Logger.d.ts +2 -0
- package/dist/cjs/exports/Logger.js +20 -0
- package/dist/cjs/lib/Colors.d.ts +8 -0
- package/dist/cjs/lib/Colors.js +48 -0
- package/dist/cjs/lib/Destination.d.ts +38 -0
- package/dist/cjs/lib/Destination.js +103 -0
- package/dist/cjs/lib/Format.d.ts +7 -0
- package/dist/cjs/lib/Format.js +106 -0
- package/dist/cjs/lib/Pretty.d.ts +9 -0
- package/dist/cjs/lib/Pretty.js +265 -0
- package/dist/cjs/lib/Record.d.ts +25 -0
- package/dist/cjs/lib/Record.js +139 -0
- package/dist/cjs/lib/Serializers.d.ts +26 -0
- package/dist/cjs/lib/Serializers.js +150 -0
- package/dist/cjs/lib/Stringify.d.ts +5 -0
- package/dist/cjs/lib/Stringify.js +110 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/Logger.js +182 -0
- package/dist/esm/exports/Logger.js +1 -0
- package/dist/esm/lib/Colors.js +45 -0
- package/dist/esm/lib/Destination.js +99 -0
- package/dist/esm/lib/Format.js +103 -0
- package/dist/esm/lib/Pretty.js +262 -0
- package/dist/esm/lib/Record.js +134 -0
- package/dist/esm/lib/Serializers.js +143 -0
- package/dist/esm/lib/Stringify.js +107 -0
- package/dist/types/Logger.d.ts +291 -0
- package/dist/types/exports/Logger.d.ts +2 -0
- package/dist/types/lib/Colors.d.ts +8 -0
- package/dist/types/lib/Destination.d.ts +38 -0
- package/dist/types/lib/Format.d.ts +7 -0
- package/dist/types/lib/Pretty.d.ts +9 -0
- package/dist/types/lib/Record.d.ts +25 -0
- package/dist/types/lib/Serializers.d.ts +26 -0
- package/dist/types/lib/Stringify.d.ts +5 -0
- package/package.json +43 -4
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
const EOL = '\n';
|
|
2
|
+
const IDENT = ' ';
|
|
3
|
+
const LEVEL_LABELS = { default: 'USERLVL', 60: 'FATAL', 50: 'ERROR', 40: 'WARN', 30: 'INFO', 20: 'DEBUG', 10: 'TRACE' };
|
|
4
|
+
const LEVEL_NUMBERS = { fatal: 60, error: 50, warn: 40, info: 30, debug: 20, trace: 10 };
|
|
5
|
+
//The keys of the logger, not printed as properties
|
|
6
|
+
const LOGGER_KEYS = ['pid', 'hostname', 'name', 'level', 'time', 'timestamp', 'caller'];
|
|
7
|
+
const ERROR_KEYS = new Set(['err', 'error']);
|
|
8
|
+
/**
|
|
9
|
+
* The JSON of a property value, indented by 2 spaces
|
|
10
|
+
* @param value
|
|
11
|
+
*/
|
|
12
|
+
function propertyJSON(value) {
|
|
13
|
+
switch (typeof value) {
|
|
14
|
+
case 'number':
|
|
15
|
+
case 'boolean':
|
|
16
|
+
return String(value);
|
|
17
|
+
case 'string':
|
|
18
|
+
return JSON.stringify(value);
|
|
19
|
+
default:
|
|
20
|
+
return JSON.stringify(value, null, 2);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const hasOwn = Object.hasOwn;
|
|
24
|
+
//The prototype pollution suspects of a JSON text
|
|
25
|
+
const SUSPECT_PROTO = /"(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])"\s*:/;
|
|
26
|
+
const SUSPECT_CONSTRUCTOR = /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/;
|
|
27
|
+
/**
|
|
28
|
+
* Remove the "__proto__" properties of a parsed log, false if it has a constructor with a prototype (not printed as a log)
|
|
29
|
+
* @param log
|
|
30
|
+
*/
|
|
31
|
+
function sanitize(log) {
|
|
32
|
+
let next = [log];
|
|
33
|
+
try {
|
|
34
|
+
while (next.length) {
|
|
35
|
+
const nodes = next;
|
|
36
|
+
next = [];
|
|
37
|
+
for (const node of nodes) {
|
|
38
|
+
if (hasOwn(node, '__proto__'))
|
|
39
|
+
delete node.__proto__;
|
|
40
|
+
if (hasOwn(node, 'constructor') && Object.prototype.hasOwnProperty.call(node.constructor, 'prototype'))
|
|
41
|
+
return false;
|
|
42
|
+
for (const key in node) {
|
|
43
|
+
const value = node[key];
|
|
44
|
+
if (value && typeof value === 'object')
|
|
45
|
+
next.push(value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Indent the lines after the first one
|
|
57
|
+
* @param input
|
|
58
|
+
* @param ident
|
|
59
|
+
*/
|
|
60
|
+
function joinLines(input, ident = IDENT) {
|
|
61
|
+
const lines = input.split(/\r?\n/);
|
|
62
|
+
for (let index = 1; index < lines.length; index++)
|
|
63
|
+
lines[index] = ident + lines[index];
|
|
64
|
+
return lines.join(EOL);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether a value is a plain object
|
|
68
|
+
* @param value
|
|
69
|
+
*/
|
|
70
|
+
function isObjectValue(value) {
|
|
71
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Pad a number with zeros
|
|
75
|
+
* @param value
|
|
76
|
+
* @param length
|
|
77
|
+
*/
|
|
78
|
+
function pad(value, length = 2) {
|
|
79
|
+
return String(value).padStart(length, '0');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The time of a log, HH:MM:ss.SSS in the local time zone
|
|
83
|
+
* @param log
|
|
84
|
+
*/
|
|
85
|
+
function prettyTime(log) {
|
|
86
|
+
let time = null;
|
|
87
|
+
if ('time' in log) {
|
|
88
|
+
time = log.time;
|
|
89
|
+
}
|
|
90
|
+
else if ('timestamp' in log) {
|
|
91
|
+
time = log.timestamp;
|
|
92
|
+
}
|
|
93
|
+
if (time === null)
|
|
94
|
+
return undefined;
|
|
95
|
+
let date = new Date(time);
|
|
96
|
+
if (isNaN(date.getTime()))
|
|
97
|
+
date = new Date(+time);
|
|
98
|
+
if (isNaN(date.getTime()))
|
|
99
|
+
return `[${time}]`;
|
|
100
|
+
return `[${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}]`;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The label of the level of a log
|
|
104
|
+
* @param log
|
|
105
|
+
* @param palette
|
|
106
|
+
*/
|
|
107
|
+
function prettyLevel(log, palette) {
|
|
108
|
+
if (!hasOwn(log, 'level'))
|
|
109
|
+
return undefined;
|
|
110
|
+
const level = log.level;
|
|
111
|
+
if (level === undefined)
|
|
112
|
+
return undefined;
|
|
113
|
+
let levelNumber = 'default';
|
|
114
|
+
if (Number.isInteger(+level)) {
|
|
115
|
+
levelNumber = hasOwn(LEVEL_LABELS, level) ? level : levelNumber;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const name = String(level).toLowerCase();
|
|
119
|
+
levelNumber = hasOwn(LEVEL_NUMBERS, name) ? LEVEL_NUMBERS[name] : levelNumber;
|
|
120
|
+
}
|
|
121
|
+
const label = LEVEL_LABELS[levelNumber];
|
|
122
|
+
return hasOwn(palette.level, levelNumber) ? palette.level[levelNumber](label) : palette.level.default(label);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The metadata of a log: (name/pid on hostname) <caller>
|
|
126
|
+
* @param log
|
|
127
|
+
*/
|
|
128
|
+
function prettyMetadata(log) {
|
|
129
|
+
let line = '';
|
|
130
|
+
if (log.name || log.pid || log.hostname) {
|
|
131
|
+
line += '(';
|
|
132
|
+
if (log.name)
|
|
133
|
+
line += log.name;
|
|
134
|
+
if (log.pid)
|
|
135
|
+
line += log.name ? `/${log.pid}` : log.pid;
|
|
136
|
+
if (log.hostname)
|
|
137
|
+
line += `${line === '(' ? 'on' : ' on'} ${log.hostname}`;
|
|
138
|
+
line += ')';
|
|
139
|
+
}
|
|
140
|
+
if (log.caller)
|
|
141
|
+
line += `${line === '' ? '' : ' '}<${log.caller}>`;
|
|
142
|
+
return line === '' ? undefined : line;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* An error property, its stack is written on its own lines
|
|
146
|
+
* @param key
|
|
147
|
+
* @param lines
|
|
148
|
+
*/
|
|
149
|
+
function prettyError(key, lines) {
|
|
150
|
+
let result = '';
|
|
151
|
+
const splitLines = `${IDENT}${key}: ${joinLines(lines)}${EOL}`.split(EOL);
|
|
152
|
+
for (let index = 0; index < splitLines.length; index++) {
|
|
153
|
+
if (index)
|
|
154
|
+
result += EOL;
|
|
155
|
+
const line = splitLines[index];
|
|
156
|
+
const matches = /^\s*"stack"/.test(line) ? /^(\s*"stack":)\s*(".*"),?$/.exec(line) : null;
|
|
157
|
+
if (matches) {
|
|
158
|
+
const indentation = ' '.repeat(/^\s*/.exec(line)[0].length + 4);
|
|
159
|
+
result += matches[1] + EOL + indentation + JSON.parse(matches[2]).replace(/\n/g, EOL + indentation);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
result += line;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The properties of a log, one per line
|
|
169
|
+
* @param log
|
|
170
|
+
* @param palette
|
|
171
|
+
* @param skipKeys
|
|
172
|
+
* @param excludeLoggerKeys
|
|
173
|
+
* @param ident
|
|
174
|
+
*/
|
|
175
|
+
function prettyObject(log, palette, skipKeys, excludeLoggerKeys = true, ident = IDENT) {
|
|
176
|
+
let result = '';
|
|
177
|
+
let errors;
|
|
178
|
+
for (const key in log) {
|
|
179
|
+
if (!hasOwn(log, key) || skipKeys.includes(key) || (excludeLoggerKeys && LOGGER_KEYS.includes(key)))
|
|
180
|
+
continue;
|
|
181
|
+
const value = log[key];
|
|
182
|
+
if (ERROR_KEYS.has(key)) {
|
|
183
|
+
(errors ??= []).push([key, value]);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
let lines = propertyJSON(value);
|
|
187
|
+
if (lines === undefined)
|
|
188
|
+
continue;
|
|
189
|
+
if (lines.includes('\\\\'))
|
|
190
|
+
lines = lines.replace(/\\\\/gi, '\\');
|
|
191
|
+
const joinedLines = lines.includes('\n') ? joinLines(lines, ident) : lines;
|
|
192
|
+
result += `${ident}${palette.property(key)}:${joinedLines.startsWith(EOL) ? '' : ' '}${joinedLines}${EOL}`;
|
|
193
|
+
}
|
|
194
|
+
if (!errors)
|
|
195
|
+
return result;
|
|
196
|
+
for (const [key, value] of errors) {
|
|
197
|
+
const lines = JSON.stringify(value, null, 2);
|
|
198
|
+
if (lines !== undefined)
|
|
199
|
+
result += prettyError(key, lines);
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* A log which is an error: its stack
|
|
205
|
+
* @param log
|
|
206
|
+
* @param palette
|
|
207
|
+
*/
|
|
208
|
+
function prettyErrorLog(log, palette) {
|
|
209
|
+
let result = `${IDENT}${joinLines(log.stack)}${EOL}`;
|
|
210
|
+
//The error properties to print: none
|
|
211
|
+
if ('' in log) {
|
|
212
|
+
result += isObjectValue(log['']) ? `${IDENT}: {${EOL}${prettyObject(log[''], palette, [], false, IDENT + IDENT)}${IDENT}}${EOL}` : `${IDENT}: ${log['']}${EOL}`;
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Format a JSON log line for humans:
|
|
218
|
+
* [time] LEVEL (name/pid): message, then the properties on their own lines
|
|
219
|
+
* @param line the JSON log line, without its line terminator
|
|
220
|
+
* @param palette
|
|
221
|
+
* @param parsed the parsed line, if it is known (the log is changed)
|
|
222
|
+
*/
|
|
223
|
+
export function pretty(line, palette, parsed) {
|
|
224
|
+
let log;
|
|
225
|
+
if (parsed) {
|
|
226
|
+
log = parsed;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
try {
|
|
230
|
+
log = JSON.parse(line);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return line + EOL;
|
|
234
|
+
}
|
|
235
|
+
if (!isObjectValue(log))
|
|
236
|
+
return line + EOL;
|
|
237
|
+
}
|
|
238
|
+
if ((SUSPECT_PROTO.test(line) || SUSPECT_CONSTRUCTOR.test(line)) && !sanitize(log))
|
|
239
|
+
return line + EOL;
|
|
240
|
+
const message = 'msg' in log && (typeof log.msg === 'string' || typeof log.msg === 'number' || typeof log.msg === 'boolean') ? palette.message(log.msg) : undefined;
|
|
241
|
+
//The host name is not printed
|
|
242
|
+
if (hasOwn(log, 'hostname'))
|
|
243
|
+
delete log.hostname;
|
|
244
|
+
const level = prettyLevel(log, palette);
|
|
245
|
+
const metadata = prettyMetadata(log);
|
|
246
|
+
const time = prettyTime(log);
|
|
247
|
+
let result = time ?? '';
|
|
248
|
+
if (level)
|
|
249
|
+
result = result ? `${result} ${level}` : level;
|
|
250
|
+
if (metadata)
|
|
251
|
+
result = result ? `${result} ${metadata}:` : metadata;
|
|
252
|
+
if (!result.endsWith(':') && result !== '')
|
|
253
|
+
result += ':';
|
|
254
|
+
if (message !== undefined)
|
|
255
|
+
result = result ? `${result} ${message}` : message;
|
|
256
|
+
if (result.length)
|
|
257
|
+
result += EOL;
|
|
258
|
+
if (log.type === 'Error' && typeof log.stack === 'string')
|
|
259
|
+
return result + prettyErrorLog(log, palette);
|
|
260
|
+
const skipKeys = ['msg', 'level', 'time'].filter((key) => typeof log[key] === 'string' || typeof log[key] === 'number' || typeof log[key] === 'boolean');
|
|
261
|
+
return result + prettyObject(log, palette, skipKeys);
|
|
262
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
//Derived from the upstream projects listed in THIRD_PARTY_NOTICES.md (rewritten in TypeScript)
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { format } from './Format.js';
|
|
4
|
+
import { serializeError, serializeRequest, serializeResponse } from './Serializers.js';
|
|
5
|
+
import { stringify } from './Stringify.js';
|
|
6
|
+
export const LEVELS = { trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60, silent: Infinity };
|
|
7
|
+
const HOSTNAME = hostname();
|
|
8
|
+
/**
|
|
9
|
+
* The JSON of a log value, undefined if it is not logged
|
|
10
|
+
* @param value
|
|
11
|
+
*/
|
|
12
|
+
function valueJSON(value) {
|
|
13
|
+
switch (typeof value) {
|
|
14
|
+
case 'undefined':
|
|
15
|
+
case 'function':
|
|
16
|
+
return undefined;
|
|
17
|
+
case 'number':
|
|
18
|
+
return Number.isFinite(value) ? String(value) : 'null';
|
|
19
|
+
case 'boolean':
|
|
20
|
+
return String(value);
|
|
21
|
+
case 'string':
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
default:
|
|
24
|
+
return stringify(value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The bindings of the log lines of an application: pid, hostname and name
|
|
29
|
+
* @param name
|
|
30
|
+
*/
|
|
31
|
+
export function logBindings(name) {
|
|
32
|
+
return { json: `,"pid":${process.pid},"hostname":${JSON.stringify(HOSTNAME)},"name":${JSON.stringify(name)}`, name: name };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The value of a JSON text, as parsed
|
|
36
|
+
* @param value
|
|
37
|
+
* @param json
|
|
38
|
+
*/
|
|
39
|
+
function parsedValue(value, json) {
|
|
40
|
+
switch (typeof value) {
|
|
41
|
+
case 'string':
|
|
42
|
+
case 'boolean':
|
|
43
|
+
return value;
|
|
44
|
+
case 'number':
|
|
45
|
+
return Number.isFinite(value) ? value + 0 : null;
|
|
46
|
+
default:
|
|
47
|
+
return JSON.parse(json);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Set a property of a parsed log, as JSON.parse does (the first position and the last value of a key)
|
|
52
|
+
* @param log
|
|
53
|
+
* @param key
|
|
54
|
+
* @param value
|
|
55
|
+
*/
|
|
56
|
+
function setParsed(log, key, value) {
|
|
57
|
+
key === '__proto__' ? Object.defineProperty(log, key, { value: value, enumerable: true, writable: true, configurable: true }) : log[key] = value;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build a JSON log line from the arguments of a log method:
|
|
61
|
+
* an object (or error, HTTP request or response) with an optional message and its format arguments,
|
|
62
|
+
* or a message with its format arguments
|
|
63
|
+
* @param level
|
|
64
|
+
* @param bindings
|
|
65
|
+
* @param args
|
|
66
|
+
* @param parse whether to also build the parsed line
|
|
67
|
+
* @param redacted the lower-case names of the headers of an HTTP request or response written as [Redacted]
|
|
68
|
+
*/
|
|
69
|
+
export function buildRecord(level, bindings, args, parse = false, redacted = new Set()) {
|
|
70
|
+
let object = null;
|
|
71
|
+
let message;
|
|
72
|
+
let [first, ...rest] = args;
|
|
73
|
+
if (typeof first === 'object') {
|
|
74
|
+
message = first;
|
|
75
|
+
if (first !== null) {
|
|
76
|
+
if (first.method && first.headers && first.socket) {
|
|
77
|
+
first = { req: serializeRequest(first, redacted) };
|
|
78
|
+
}
|
|
79
|
+
else if (typeof first.setHeader === 'function') {
|
|
80
|
+
first = { res: serializeResponse(first, redacted) };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
let formatArgs;
|
|
84
|
+
if (message === null && !rest.length) {
|
|
85
|
+
formatArgs = [null];
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
message = rest.shift();
|
|
89
|
+
formatArgs = rest;
|
|
90
|
+
}
|
|
91
|
+
object = first;
|
|
92
|
+
message = format(message, formatArgs);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
message = format(first === undefined ? rest.shift() : first, rest);
|
|
96
|
+
}
|
|
97
|
+
if (object === undefined || object === null) {
|
|
98
|
+
object = {};
|
|
99
|
+
}
|
|
100
|
+
else if (object instanceof Error) {
|
|
101
|
+
if (message === undefined)
|
|
102
|
+
message = object.message;
|
|
103
|
+
object = { err: object };
|
|
104
|
+
}
|
|
105
|
+
else if (message === undefined && object.msg === undefined && object.err) {
|
|
106
|
+
message = object.err.message;
|
|
107
|
+
}
|
|
108
|
+
const time = Date.now();
|
|
109
|
+
let line = `{"level":${level},"time":${time}${bindings.json}`;
|
|
110
|
+
const log = parse ? { level: level, time: time, pid: process.pid, hostname: HOSTNAME, name: bindings.name } : undefined;
|
|
111
|
+
for (const key in object) {
|
|
112
|
+
let value = object[key];
|
|
113
|
+
if (!Object.prototype.hasOwnProperty.call(object, key) || value === undefined)
|
|
114
|
+
continue;
|
|
115
|
+
if (key === 'err')
|
|
116
|
+
value = serializeError(value);
|
|
117
|
+
const json = valueJSON(value);
|
|
118
|
+
if (json === undefined)
|
|
119
|
+
continue;
|
|
120
|
+
line += `,${JSON.stringify(key)}:${json}`;
|
|
121
|
+
if (log)
|
|
122
|
+
setParsed(log, key, parsedValue(value, json));
|
|
123
|
+
}
|
|
124
|
+
if (message !== undefined && typeof message !== 'function') {
|
|
125
|
+
const json = typeof message === 'string' ? JSON.stringify(message) : valueJSON(message);
|
|
126
|
+
//A message which cannot be serialized (a symbol) is omitted
|
|
127
|
+
if (json !== undefined) {
|
|
128
|
+
line += `,"msg":${json}`;
|
|
129
|
+
if (log)
|
|
130
|
+
setParsed(log, 'msg', parsedValue(message, json));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { line: `${line}}\n`, log: log };
|
|
134
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
//Derived from the upstream projects listed in THIRD_PARTY_NOTICES.md (rewritten in TypeScript)
|
|
2
|
+
const SEEN = Symbol('seen');
|
|
3
|
+
/**
|
|
4
|
+
* Whether a value looks like an error
|
|
5
|
+
* @param value
|
|
6
|
+
*/
|
|
7
|
+
export function isErrorLike(value) {
|
|
8
|
+
return !!value && typeof value.message === 'string';
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The cause of an error
|
|
12
|
+
* @param error
|
|
13
|
+
*/
|
|
14
|
+
function causeOf(error) {
|
|
15
|
+
if (!error)
|
|
16
|
+
return undefined;
|
|
17
|
+
const cause = typeof error.cause === 'function' ? error.cause() : error.cause;
|
|
18
|
+
return isErrorLike(cause) ? cause : undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The stack of an error followed by the stacks of its causes
|
|
22
|
+
* @param error
|
|
23
|
+
* @param seen
|
|
24
|
+
*/
|
|
25
|
+
function stackWithCauses(error, seen = new Set()) {
|
|
26
|
+
if (!isErrorLike(error))
|
|
27
|
+
return '';
|
|
28
|
+
const stack = error.stack || '';
|
|
29
|
+
if (seen.has(error))
|
|
30
|
+
return `${stack}\ncauses have become circular...`;
|
|
31
|
+
const cause = causeOf(error);
|
|
32
|
+
if (!cause)
|
|
33
|
+
return stack;
|
|
34
|
+
seen.add(error);
|
|
35
|
+
return `${stack}\ncaused by: ${stackWithCauses(cause, seen)}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The message of an error followed by the messages of its causes
|
|
39
|
+
* @param error
|
|
40
|
+
* @param seen
|
|
41
|
+
* @param skip
|
|
42
|
+
*/
|
|
43
|
+
function messageWithCauses(error, seen = new Set(), skip = false) {
|
|
44
|
+
if (!isErrorLike(error))
|
|
45
|
+
return '';
|
|
46
|
+
const message = skip ? '' : (error.message || '');
|
|
47
|
+
if (seen.has(error))
|
|
48
|
+
return `${message}: ...`;
|
|
49
|
+
const cause = causeOf(error);
|
|
50
|
+
if (!cause)
|
|
51
|
+
return message;
|
|
52
|
+
seen.add(error);
|
|
53
|
+
//A cause function (VError) already includes the message of its cause, as an error wrapping a cause of the same
|
|
54
|
+
//message (an exception keeping the original error)
|
|
55
|
+
const included = typeof error.cause === 'function' || (!skip && !seen.has(cause) && message === cause.message);
|
|
56
|
+
return message + (included ? '' : ': ') + messageWithCauses(cause, seen, included);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Serialize an error: type, message and stack (with the causes), aggregated errors and the other enumerable properties
|
|
60
|
+
* @param error
|
|
61
|
+
*/
|
|
62
|
+
export function serializeError(error) {
|
|
63
|
+
if (!isErrorLike(error))
|
|
64
|
+
return error;
|
|
65
|
+
error[SEEN] = undefined;
|
|
66
|
+
const serialized = {};
|
|
67
|
+
serialized.type = Object.prototype.toString.call(error.constructor) === '[object Function]' ? error.constructor.name : error.name;
|
|
68
|
+
serialized.message = messageWithCauses(error);
|
|
69
|
+
serialized.stack = stackWithCauses(error);
|
|
70
|
+
if (Array.isArray(error.errors))
|
|
71
|
+
serialized.aggregateErrors = error.errors.map((item) => serializeError(item));
|
|
72
|
+
for (const key in error) {
|
|
73
|
+
//"raw" is reserved for the original error (not serialized)
|
|
74
|
+
if (serialized[key] !== undefined || key === 'raw')
|
|
75
|
+
continue;
|
|
76
|
+
const value = error[key];
|
|
77
|
+
if (isErrorLike(value)) {
|
|
78
|
+
if (key !== 'cause' && !Object.prototype.hasOwnProperty.call(value, SEEN))
|
|
79
|
+
serialized[key] = serializeError(value);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
serialized[key] = value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
delete error[SEEN];
|
|
86
|
+
return serialized;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The headers written as [Redacted] by default: the credentials of the requests and of the responses
|
|
90
|
+
*/
|
|
91
|
+
export const REDACTED_HEADERS = Object.freeze(['authorization', 'cookie', 'set-cookie', 'proxy-authorization']);
|
|
92
|
+
/**
|
|
93
|
+
* A copy of headers, the values of the redacted ones (lower-case names) replaced by [Redacted]
|
|
94
|
+
* @param headers
|
|
95
|
+
* @param redacted
|
|
96
|
+
*/
|
|
97
|
+
function redactHeaders(headers, redacted) {
|
|
98
|
+
if (!redacted.size || !headers || typeof headers !== 'object')
|
|
99
|
+
return headers;
|
|
100
|
+
//A copy: the headers of the request or the response are not changed
|
|
101
|
+
const copy = { ...headers };
|
|
102
|
+
for (const name of Object.keys(copy))
|
|
103
|
+
if (redacted.has(name.toLowerCase()))
|
|
104
|
+
copy[name] = '[Redacted]';
|
|
105
|
+
return copy;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Serialize an HTTP request
|
|
109
|
+
* @param request
|
|
110
|
+
* @param redacted the lower-case names of the headers written as [Redacted]
|
|
111
|
+
*/
|
|
112
|
+
export function serializeRequest(request, redacted = new Set()) {
|
|
113
|
+
const connection = request.info || request.socket;
|
|
114
|
+
const serialized = {};
|
|
115
|
+
serialized.id = typeof request.id === 'function' ? request.id() : (request.id || (request.info ? request.info.id : undefined));
|
|
116
|
+
serialized.method = request.method;
|
|
117
|
+
if (request.originalUrl) {
|
|
118
|
+
serialized.url = request.originalUrl;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const path = request.path;
|
|
122
|
+
serialized.url = typeof path === 'string' ? path : (request.url ? request.url.path || request.url : undefined);
|
|
123
|
+
}
|
|
124
|
+
if (request.query)
|
|
125
|
+
serialized.query = request.query;
|
|
126
|
+
if (request.params)
|
|
127
|
+
serialized.params = request.params;
|
|
128
|
+
serialized.headers = redactHeaders(request.headers, redacted);
|
|
129
|
+
serialized.remoteAddress = connection && connection.remoteAddress;
|
|
130
|
+
serialized.remotePort = connection && connection.remotePort;
|
|
131
|
+
return serialized;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Serialize an HTTP response
|
|
135
|
+
* @param response
|
|
136
|
+
* @param redacted the lower-case names of the headers written as [Redacted]
|
|
137
|
+
*/
|
|
138
|
+
export function serializeResponse(response, redacted = new Set()) {
|
|
139
|
+
return {
|
|
140
|
+
statusCode: response.headersSent ? response.statusCode : null,
|
|
141
|
+
headers: redactHeaders(response.getHeaders ? response.getHeaders() : response._headers, redacted)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
//Derived from the upstream projects listed in THIRD_PARTY_NOTICES.md (rewritten in TypeScript)
|
|
2
|
+
const MAXIMUM_DEPTH = 5;
|
|
3
|
+
const MAXIMUM_BREADTH = 100;
|
|
4
|
+
const TypedArrayToStringTag = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Int8Array.prototype), Symbol.toStringTag).get;
|
|
5
|
+
/**
|
|
6
|
+
* The count of the omitted items
|
|
7
|
+
* @param count
|
|
8
|
+
*/
|
|
9
|
+
function itemCount(count) {
|
|
10
|
+
return count === 1 ? '1 item' : `${count} items`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Serialize a value which JSON cannot (circular references, bigints):
|
|
14
|
+
* sorted keys, circular references as "[Circular]", limited depth and breadth
|
|
15
|
+
* @param key
|
|
16
|
+
* @param value
|
|
17
|
+
* @param stack
|
|
18
|
+
*/
|
|
19
|
+
function safeStringify(key, value, stack) {
|
|
20
|
+
switch (typeof value) {
|
|
21
|
+
case 'string':
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
case 'number':
|
|
24
|
+
return isFinite(value) ? String(value) : 'null';
|
|
25
|
+
case 'boolean':
|
|
26
|
+
return value ? 'true' : 'false';
|
|
27
|
+
case 'bigint':
|
|
28
|
+
return String(value);
|
|
29
|
+
case 'object': {
|
|
30
|
+
if (value === null)
|
|
31
|
+
return 'null';
|
|
32
|
+
if (typeof value.toJSON === 'function') {
|
|
33
|
+
value = value.toJSON(key);
|
|
34
|
+
if (typeof value !== 'object')
|
|
35
|
+
return safeStringify(key, value, stack);
|
|
36
|
+
if (value === null)
|
|
37
|
+
return 'null';
|
|
38
|
+
}
|
|
39
|
+
if (stack.includes(value))
|
|
40
|
+
return '"[Circular]"';
|
|
41
|
+
let result = '';
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
if (!value.length)
|
|
44
|
+
return '[]';
|
|
45
|
+
if (MAXIMUM_DEPTH < stack.length + 1)
|
|
46
|
+
return '"[Array]"';
|
|
47
|
+
stack.push(value);
|
|
48
|
+
const count = Math.min(value.length, MAXIMUM_BREADTH);
|
|
49
|
+
for (let index = 0; index < count; index++) {
|
|
50
|
+
result += `${index ? ',' : ''}${safeStringify(String(index), value[index], stack) ?? 'null'}`;
|
|
51
|
+
}
|
|
52
|
+
if (value.length - 1 > MAXIMUM_BREADTH)
|
|
53
|
+
result += `,"... ${itemCount(value.length - MAXIMUM_BREADTH - 1)} not stringified"`;
|
|
54
|
+
stack.pop();
|
|
55
|
+
return `[${result}]`;
|
|
56
|
+
}
|
|
57
|
+
let keys = Object.keys(value);
|
|
58
|
+
const keyCount = keys.length;
|
|
59
|
+
if (!keyCount)
|
|
60
|
+
return '{}';
|
|
61
|
+
if (MAXIMUM_DEPTH < stack.length + 1)
|
|
62
|
+
return '"[Object]"';
|
|
63
|
+
let separator = '';
|
|
64
|
+
let count = Math.min(keyCount, MAXIMUM_BREADTH);
|
|
65
|
+
if (value.length !== undefined && TypedArrayToStringTag.call(value) !== undefined && value.length !== 0) {
|
|
66
|
+
const items = Math.min(value.length, MAXIMUM_BREADTH);
|
|
67
|
+
for (let index = 0; index < items; index++)
|
|
68
|
+
result += `${index ? ',' : ''}"${index}":${value[index]}`;
|
|
69
|
+
keys = keys.slice(value.length);
|
|
70
|
+
count -= value.length;
|
|
71
|
+
separator = ',';
|
|
72
|
+
}
|
|
73
|
+
keys.sort();
|
|
74
|
+
stack.push(value);
|
|
75
|
+
for (let index = 0; index < count; index++) {
|
|
76
|
+
const item = safeStringify(keys[index], value[keys[index]], stack);
|
|
77
|
+
if (item === undefined)
|
|
78
|
+
continue;
|
|
79
|
+
result += `${separator}${JSON.stringify(keys[index])}:${item}`;
|
|
80
|
+
separator = ',';
|
|
81
|
+
}
|
|
82
|
+
if (keyCount > MAXIMUM_BREADTH)
|
|
83
|
+
result += `${separator}"...":"${itemCount(keyCount - MAXIMUM_BREADTH)} not stringified"`;
|
|
84
|
+
stack.pop();
|
|
85
|
+
return `{${result}}`;
|
|
86
|
+
}
|
|
87
|
+
default:
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Serialize a value to JSON, the values JSON cannot serialize are serialized safely
|
|
93
|
+
* @param value
|
|
94
|
+
*/
|
|
95
|
+
export function stringify(value) {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.stringify(value);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
try {
|
|
101
|
+
return safeStringify('', value, []);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return '"[unable to serialize, circular reference is too complex to analyze]"';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|