@ix-xs/node-comfort 2.0.0 → 2.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/CHANGELOG.md +150 -0
- package/README.md +32 -30
- package/index.js +3 -3
- package/package.json +8 -7
- package/src/Arr.js +222 -58
- package/src/Async.js +322 -90
- package/src/Cache.js +148 -44
- package/src/Checker.js +257 -26
- package/src/Cli.js +404 -145
- package/src/Color.js +224 -83
- package/src/Crypto.js +481 -106
- package/src/Emitter.js +104 -37
- package/src/Env.js +176 -42
- package/src/FS.js +203 -60
- package/src/Func.js +226 -76
- package/src/Http.js +311 -149
- package/src/Id.js +281 -96
- package/src/Logger.js +302 -78
- package/src/Num.js +182 -60
- package/src/Obj.js +340 -125
- package/src/SQLite.js +426 -105
- package/src/Schema.js +602 -87
- package/src/Str.js +316 -86
- package/src/Sys.js +197 -54
- package/src/Time.js +572 -317
- package/src/Utils.js +62 -41
- package/src/errors.js +54 -11
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +59 -17
- package/types/src/Arr.d.ts +78 -0
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +52 -1
- package/types/src/Async.d.ts.map +1 -1
- package/types/src/Cache.d.ts +33 -7
- package/types/src/Cache.d.ts.map +1 -1
- package/types/src/Checker.d.ts +155 -2
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +97 -15
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +7 -1
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +99 -13
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +36 -17
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +31 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +36 -5
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +147 -19
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +113 -43
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts +60 -8
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +53 -2
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +48 -7
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +67 -4
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +123 -14
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +256 -7
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +49 -1
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +51 -6
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +130 -34
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +15 -11
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +27 -3
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
package/src/Logger.js
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* `#ff8800`, `bg#1e1e2e`, `rgb(255, 128, 0)` and `bgRgb(...)`.
|
|
11
11
|
*
|
|
12
12
|
* Levels, from chatty to severe: `trace`, `debug`, `info`, `warn`, `error`,
|
|
13
|
-
* `fatal`. The default is `info
|
|
14
|
-
*
|
|
13
|
+
* `fatal`. The default is `info`; `LOG_LEVEL` changes it. `DEBUG=1` shows
|
|
14
|
+
* debug messages, and `DEBUG=db,http:*` only the debug and trace messages of
|
|
15
|
+
* the loggers with these scopes. Warnings and errors go to stderr.
|
|
15
16
|
*
|
|
16
17
|
* @example
|
|
17
18
|
* nc.info("Server listening on port 3000");
|
|
@@ -24,7 +25,7 @@ const fs = require("node:fs");
|
|
|
24
25
|
const path = require("node:path");
|
|
25
26
|
const util = require("node:util");
|
|
26
27
|
const color = require("./Color.js");
|
|
27
|
-
const
|
|
28
|
+
const { timeConfig } = require("./internal/shared.js");
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Log levels, from the most verbose to the most severe. `silent` hides everything.
|
|
@@ -45,6 +46,17 @@ const time = require("./Time.js");
|
|
|
45
46
|
* @property {number} [maxFiles] How many rotated files to keep. Defaults to `5`.
|
|
46
47
|
*/
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* A message as `onLog` receives it.
|
|
51
|
+
* @typedef {object} LogEntry
|
|
52
|
+
* @property {Date} time
|
|
53
|
+
* @property {LogLevel} level
|
|
54
|
+
* @property {"log" | "trace" | "debug" | "info" | "success" | "warn" | "error" | "fatal"} kind The method that was called.
|
|
55
|
+
* @property {string | undefined} scope
|
|
56
|
+
* @property {string} message The line as written, without colors or timestamp.
|
|
57
|
+
* @property {unknown[]} args What was passed to the method, untouched.
|
|
58
|
+
*/
|
|
59
|
+
|
|
48
60
|
/**
|
|
49
61
|
* Options for `createLogger()` and `configure()`.
|
|
50
62
|
* @typedef {object} LoggerOptions
|
|
@@ -58,6 +70,8 @@ const time = require("./Time.js");
|
|
|
58
70
|
* @property {LogStream} [stderr] Where `warn`, `error` and `fatal` go. Defaults to `process.stderr`.
|
|
59
71
|
* @property {string | LogFileOptions} [file] Also write every message to a file.
|
|
60
72
|
* @property {Record<string, unknown>} [fields] Added to every JSON record, like a service name.
|
|
73
|
+
* @property {string[]} [redact] Keys whose values are replaced by `"[redacted]"` wherever they appear in logged objects, like `["password", "authorization"]`. Case doesn't matter.
|
|
74
|
+
* @property {(entry: LogEntry) => void | Promise<void>} [onLog] Called for every message that passes the level, to send logs somewhere else. It isn't awaited, and an error it throws (or a promise it rejects) is ignored: logging never breaks the program.
|
|
61
75
|
*/
|
|
62
76
|
|
|
63
77
|
/**
|
|
@@ -109,11 +123,42 @@ const STYLE_NAMES = new Set([
|
|
|
109
123
|
"bgBlackBright", "bgRedBright", "bgGreenBright", "bgYellowBright", "bgBlueBright", "bgMagentaBright", "bgCyanBright", "bgWhiteBright",
|
|
110
124
|
]);
|
|
111
125
|
|
|
126
|
+
const REDACTED = "[redacted]";
|
|
127
|
+
const TRIM_LINES = /^(\s*\r?\n)+|(\r?\n\s*)+$/g;
|
|
128
|
+
const RGB = /(bg)?rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/gi;
|
|
129
|
+
const TOKEN = /^(\S+)(\s+|$)/;
|
|
130
|
+
|
|
112
131
|
/** @returns {LogLevel} */
|
|
113
132
|
const _defaultLevel = () => {
|
|
114
133
|
const fromEnv = String(process.env.LOG_LEVEL ?? "").toLowerCase();
|
|
115
|
-
if (fromEnv
|
|
116
|
-
return process.env.DEBUG || process.env.NODE_DEBUG ? "debug" : "info";
|
|
134
|
+
if (Object.hasOwn(LEVELS, fromEnv)) return /** @type {LogLevel} */ (fromEnv);
|
|
135
|
+
return _debugAll(process.env.DEBUG) || process.env.NODE_DEBUG ? "debug" : "info";
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Does `DEBUG` ask for everything? `DEBUG=1`, `DEBUG=true` and `DEBUG=*` do.
|
|
140
|
+
* @param {string | undefined} value
|
|
141
|
+
*/
|
|
142
|
+
const _debugAll = (value) => value !== undefined && /^(?:1|true|yes|on|\*)$/i.test(value.trim());
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Does `DEBUG` name this scope? It's a list of scopes, where `*` stands for
|
|
146
|
+
* anything and a leading `-` excludes: `DEBUG=db,http:*,-http:static`.
|
|
147
|
+
* @param {string | undefined} value
|
|
148
|
+
* @param {string | undefined} scope
|
|
149
|
+
*/
|
|
150
|
+
const _debugScope = (value, scope) => {
|
|
151
|
+
if (!value || !scope || _debugAll(value)) return false;
|
|
152
|
+
let wanted = false;
|
|
153
|
+
for (const part of value.split(/[\s,]+/)) {
|
|
154
|
+
if (!part) continue;
|
|
155
|
+
const exclude = part[0] === "-";
|
|
156
|
+
const pattern = new RegExp(`^${(exclude ? part.slice(1) : part).split("*").map(_escape).join(".*")}$`);
|
|
157
|
+
if (!pattern.test(scope)) continue;
|
|
158
|
+
if (exclude) return false;
|
|
159
|
+
wanted = true;
|
|
160
|
+
}
|
|
161
|
+
return wanted;
|
|
117
162
|
};
|
|
118
163
|
|
|
119
164
|
/** @param {string} value */
|
|
@@ -152,20 +197,19 @@ const _isStyleToken = (token) =>
|
|
|
152
197
|
/**
|
|
153
198
|
* Renders `<% styles text %>` markup.
|
|
154
199
|
* @param {string} input
|
|
155
|
-
* @param {{ open: string, close: string }} delimiter
|
|
200
|
+
* @param {{ open: string, close: string, regex: RegExp }} delimiter
|
|
156
201
|
* @param {import("./Color").ColorModule} palette
|
|
157
202
|
* @returns {string}
|
|
158
203
|
*/
|
|
159
204
|
const _markup = (input, delimiter, palette) => {
|
|
160
205
|
if (!input.includes(delimiter.open)) return input;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const normalized = String(content).replace(/(bg)?rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/gi, (m0, bg, r, g, b) => `${bg ?? ""}rgb(${r},${g},${b})`);
|
|
206
|
+
return input.replace(delimiter.regex, (_, content) => {
|
|
207
|
+
const normalized = String(content).replace(RGB, (m0, bg, r, g, b) => `${bg ?? ""}rgb(${r},${g},${b})`);
|
|
164
208
|
/** @type {string[]} */
|
|
165
209
|
const tokens = [];
|
|
166
210
|
let rest = normalized.replace(/^\s+/, "");
|
|
167
211
|
for (;;) {
|
|
168
|
-
const match =
|
|
212
|
+
const match = TOKEN.exec(rest);
|
|
169
213
|
if (!match || !_isStyleToken(match[1])) break;
|
|
170
214
|
tokens.push(match[1]);
|
|
171
215
|
rest = rest.slice(match[0].length);
|
|
@@ -174,12 +218,42 @@ const _markup = (input, delimiter, palette) => {
|
|
|
174
218
|
});
|
|
175
219
|
};
|
|
176
220
|
|
|
221
|
+
/**
|
|
222
|
+
* A copy of a value with the secret keys masked, for values `JSON.stringify`
|
|
223
|
+
* can't handle on its own: class instances, circular references.
|
|
224
|
+
* @param {unknown} value
|
|
225
|
+
* @param {Set<string>} secrets Lower-cased key names.
|
|
226
|
+
* @param {Map<object, unknown>} seen
|
|
227
|
+
* @returns {unknown}
|
|
228
|
+
*/
|
|
229
|
+
const _redact = (value, secrets, seen) => {
|
|
230
|
+
if (value === null || typeof value !== "object" || value instanceof Date || value instanceof RegExp || value instanceof Error || ArrayBuffer.isView(value)) return value;
|
|
231
|
+
const known = seen.get(value);
|
|
232
|
+
if (known !== undefined) return known;
|
|
233
|
+
if (Array.isArray(value)) {
|
|
234
|
+
/** @type {unknown[]} */
|
|
235
|
+
const list = [];
|
|
236
|
+
seen.set(value, list);
|
|
237
|
+
for (const item of value) list.push(_redact(item, secrets, seen));
|
|
238
|
+
return list;
|
|
239
|
+
}
|
|
240
|
+
if (value instanceof Map || value instanceof Set) return value;
|
|
241
|
+
/** @type {Record<string, unknown>} */
|
|
242
|
+
const copy = Object.create(Object.getPrototypeOf(value));
|
|
243
|
+
seen.set(value, copy);
|
|
244
|
+
for (const key of Object.keys(value)) {
|
|
245
|
+
copy[key] = secrets.has(key.toLowerCase()) ? REDACTED : _redact(/** @type {Record<string, unknown>} */ (value)[key], secrets, seen);
|
|
246
|
+
}
|
|
247
|
+
return copy;
|
|
248
|
+
};
|
|
249
|
+
|
|
177
250
|
/**
|
|
178
251
|
* @param {unknown} value
|
|
179
252
|
* @param {boolean} colors
|
|
253
|
+
* @param {Set<string> | undefined} secrets
|
|
180
254
|
* @returns {string}
|
|
181
255
|
*/
|
|
182
|
-
const _stringify = (value, colors) => {
|
|
256
|
+
const _stringify = (value, colors, secrets) => {
|
|
183
257
|
if (typeof value === "string") return value;
|
|
184
258
|
if (value instanceof Error) {
|
|
185
259
|
let text = value.stack ?? `${value.name}: ${value.message}`;
|
|
@@ -195,13 +269,13 @@ const _stringify = (value, colors) => {
|
|
|
195
269
|
const proto = Object.getPrototypeOf(value);
|
|
196
270
|
if (Array.isArray(value) || proto === Object.prototype || proto === null) {
|
|
197
271
|
try {
|
|
198
|
-
const json = JSON.stringify(value, (
|
|
272
|
+
const json = JSON.stringify(value, (key, v) => (secrets?.has(key.toLowerCase()) ? REDACTED : typeof v === "bigint" ? `${v}n` : v), 2);
|
|
199
273
|
if (json !== undefined) return json;
|
|
200
274
|
} catch {
|
|
201
275
|
// circular or exotic: fall back to util.inspect
|
|
202
276
|
}
|
|
203
277
|
}
|
|
204
|
-
return util.inspect(value, { depth: 6, colors, breakLength: 100 });
|
|
278
|
+
return util.inspect(secrets ? _redact(value, secrets, new Map()) : value, { depth: 6, colors, breakLength: 100 });
|
|
205
279
|
}
|
|
206
280
|
return typeof value === "symbol" || typeof value === "function" ? util.inspect(value, { colors }) : String(value);
|
|
207
281
|
};
|
|
@@ -211,11 +285,13 @@ const _stringify = (value, colors) => {
|
|
|
211
285
|
* @param {unknown[]} args
|
|
212
286
|
* @param {string | undefined} scope
|
|
213
287
|
* @param {Record<string, unknown>} fields
|
|
288
|
+
* @param {Set<string> | undefined} secrets
|
|
289
|
+
* @param {Date} now
|
|
214
290
|
* @returns {string}
|
|
215
291
|
*/
|
|
216
|
-
const _json = (level, args, scope, fields) => {
|
|
292
|
+
const _json = (level, args, scope, fields, secrets, now) => {
|
|
217
293
|
/** @type {Record<string, unknown>} */
|
|
218
|
-
const record = { time:
|
|
294
|
+
const record = { time: now.toISOString(), level, ...fields };
|
|
219
295
|
if (scope) record.scope = scope;
|
|
220
296
|
const messages = [];
|
|
221
297
|
for (const arg of args) {
|
|
@@ -224,49 +300,93 @@ const _json = (level, args, scope, fields) => {
|
|
|
224
300
|
} else if (arg !== null && typeof arg === "object" && !Array.isArray(arg)) {
|
|
225
301
|
Object.assign(record, arg);
|
|
226
302
|
} else {
|
|
227
|
-
messages.push(typeof arg === "string" ? color.strip(arg) : _stringify(arg, false));
|
|
303
|
+
messages.push(typeof arg === "string" ? color.strip(arg) : _stringify(arg, false, secrets));
|
|
228
304
|
}
|
|
229
305
|
}
|
|
230
306
|
record.msg = messages.join(" ");
|
|
231
307
|
try {
|
|
232
|
-
return JSON.stringify(record, (
|
|
308
|
+
return JSON.stringify(record, (key, v) => (secrets?.has(key.toLowerCase()) ? REDACTED : typeof v === "bigint" ? v.toString() : v));
|
|
233
309
|
} catch {
|
|
234
310
|
return JSON.stringify({ time: record.time, level, msg: `${record.msg} [unserializable fields]` });
|
|
235
311
|
}
|
|
236
312
|
};
|
|
237
313
|
|
|
238
314
|
/**
|
|
239
|
-
*
|
|
315
|
+
* A log file: where it is, how big it may get, how big it is now.
|
|
316
|
+
* @typedef {{ target: string, limit: number | undefined, keep: number, size: number, ready: boolean }} _LogFile
|
|
317
|
+
*/
|
|
318
|
+
|
|
319
|
+
/**
|
|
240
320
|
* @param {LogFileOptions} file
|
|
321
|
+
* @returns {_LogFile}
|
|
322
|
+
*/
|
|
323
|
+
const _openFile = (file) => ({
|
|
324
|
+
target: path.resolve(file.path),
|
|
325
|
+
limit: file.maxSize === undefined ? undefined : typeof file.maxSize === "number" ? file.maxSize : require("./Num.js").parseBytes(file.maxSize),
|
|
326
|
+
keep: Math.max(1, file.maxFiles ?? 5),
|
|
327
|
+
size: 0,
|
|
328
|
+
ready: false,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
/** @param {string} target */
|
|
332
|
+
const _sizeOf = (target) => {
|
|
333
|
+
try {
|
|
334
|
+
return fs.statSync(target).size;
|
|
335
|
+
} catch {
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Appends a line, rotating the file when it gets too big. The folder is
|
|
342
|
+
* made once and the size is counted here, so a line costs one write.
|
|
343
|
+
* @param {_LogFile} file
|
|
241
344
|
* @param {string} line
|
|
242
345
|
*/
|
|
243
346
|
const _writeFile = (file, line) => {
|
|
347
|
+
const text = `${line}\n`;
|
|
244
348
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
for (let i = keep - 1; i >= 1; i--) {
|
|
258
|
-
if (fs.existsSync(`${target}.${i}`)) fs.renameSync(`${target}.${i}`, `${target}.${i + 1}`);
|
|
349
|
+
if (!file.ready) {
|
|
350
|
+
fs.mkdirSync(path.dirname(file.target), { recursive: true });
|
|
351
|
+
file.size = _sizeOf(file.target);
|
|
352
|
+
file.ready = true;
|
|
353
|
+
}
|
|
354
|
+
const bytes = Buffer.byteLength(text);
|
|
355
|
+
if (file.limit !== undefined && file.size > 0 && file.size + bytes - 1 > file.limit) {
|
|
356
|
+
// Another process may have rotated or removed the file: look before moving anything.
|
|
357
|
+
file.size = _sizeOf(file.target);
|
|
358
|
+
if (file.size > 0 && file.size + bytes - 1 > file.limit) {
|
|
359
|
+
for (let i = file.keep - 1; i >= 1; i--) {
|
|
360
|
+
if (fs.existsSync(`${file.target}.${i}`)) fs.renameSync(`${file.target}.${i}`, `${file.target}.${i + 1}`);
|
|
259
361
|
}
|
|
260
|
-
if (fs.existsSync(`${target}.${keep + 1}`)) fs.rmSync(`${target}.${keep + 1}`);
|
|
261
|
-
fs.renameSync(target, `${target}.1`);
|
|
362
|
+
if (fs.existsSync(`${file.target}.${file.keep + 1}`)) fs.rmSync(`${file.target}.${file.keep + 1}`);
|
|
363
|
+
fs.renameSync(file.target, `${file.target}.1`);
|
|
364
|
+
file.size = 0;
|
|
262
365
|
}
|
|
263
366
|
}
|
|
264
|
-
|
|
367
|
+
try {
|
|
368
|
+
fs.appendFileSync(file.target, text);
|
|
369
|
+
} catch {
|
|
370
|
+
// The folder was removed since: make it again, once.
|
|
371
|
+
fs.mkdirSync(path.dirname(file.target), { recursive: true });
|
|
372
|
+
fs.appendFileSync(file.target, text);
|
|
373
|
+
file.size = 0;
|
|
374
|
+
}
|
|
375
|
+
file.size += bytes;
|
|
265
376
|
} catch {
|
|
266
377
|
// Logging must never crash the application.
|
|
267
378
|
}
|
|
268
379
|
};
|
|
269
380
|
|
|
381
|
+
/** @param {number} n */
|
|
382
|
+
const _two = (n) => (n < 10 ? `0${n}` : String(n));
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* What a message looks like on one output: its colors, and the parts that
|
|
386
|
+
* are the same on every line, styled once.
|
|
387
|
+
* @typedef {{ palette: import("./Color").ColorModule, colors: boolean, labels: Record<string, string>, scope: string, stamp: string, stampAt: number, stampWidth: number }} _Face
|
|
388
|
+
*/
|
|
389
|
+
|
|
270
390
|
/**
|
|
271
391
|
* @param {LoggerOptions} options
|
|
272
392
|
* @returns {Logger}
|
|
@@ -283,70 +403,170 @@ const _create = (options) => {
|
|
|
283
403
|
stderr: options.stderr,
|
|
284
404
|
file: typeof options.file === "string" ? { path: options.file } : options.file,
|
|
285
405
|
fields: options.fields ?? {},
|
|
406
|
+
redact: options.redact,
|
|
407
|
+
onLog: options.onLog,
|
|
286
408
|
};
|
|
287
409
|
/** @type {string[]} */
|
|
288
410
|
const groups = [];
|
|
289
411
|
/** @type {Map<string, bigint>} */
|
|
290
412
|
const timers = new Map();
|
|
291
413
|
|
|
414
|
+
// Everything below is worked out from the settings, and again when they change.
|
|
415
|
+
let threshold = 0;
|
|
416
|
+
let debugScope = false;
|
|
417
|
+
// What each method does with a message, decided once: a call that prints
|
|
418
|
+
// nothing must cost next to nothing.
|
|
419
|
+
/** @type {Record<keyof typeof BADGES, boolean>} */
|
|
420
|
+
const on = { log: true, trace: false, debug: false, info: true, success: true, warn: true, error: true, fatal: true };
|
|
421
|
+
/** @type {{ open: string, close: string, regex: RegExp }} */
|
|
422
|
+
let delimiter;
|
|
423
|
+
/** @type {Set<string> | undefined} */
|
|
424
|
+
let secrets;
|
|
425
|
+
/** @type {_LogFile | undefined} */
|
|
426
|
+
let file;
|
|
427
|
+
/** @type {{ stdout?: _Face, stderr?: _Face, file?: _Face }} */
|
|
428
|
+
let faces = {};
|
|
429
|
+
|
|
430
|
+
const gate = () => {
|
|
431
|
+
threshold = LEVELS[settings.level];
|
|
432
|
+
for (const kind of /** @type {Array<keyof typeof BADGES>} */ (Object.keys(BADGES))) {
|
|
433
|
+
on[kind] = LEVELS[BADGES[kind].level] >= threshold || (debugScope && threshold !== Infinity && (kind === "debug" || kind === "trace"));
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const refresh = () => {
|
|
438
|
+
// DEBUG=db,http:* turns debug on for those scopes only, whatever the level.
|
|
439
|
+
debugScope = _debugScope(process.env.DEBUG, settings.scope);
|
|
440
|
+
gate();
|
|
441
|
+
const { open, close } = settings.delimiter;
|
|
442
|
+
delimiter = { open, close, regex: new RegExp(`${_escape(open)}([\\s\\S]*?)${_escape(close)}`, "g") };
|
|
443
|
+
secrets = settings.redact?.length ? new Set(settings.redact.map((key) => String(key).toLowerCase())) : undefined;
|
|
444
|
+
file = settings.file ? _openFile(settings.file) : undefined;
|
|
445
|
+
faces = {};
|
|
446
|
+
};
|
|
447
|
+
|
|
292
448
|
/** @param {"stdout" | "stderr"} which */
|
|
293
449
|
const streamOf = (which) => (which === "stderr" ? settings.stderr ?? process.stderr : settings.stdout ?? process.stdout);
|
|
294
450
|
|
|
295
|
-
/** @param {"stdout" | "stderr"} which */
|
|
451
|
+
/** @param {"stdout" | "stderr" | "file"} which */
|
|
296
452
|
const paletteOf = (which) => {
|
|
453
|
+
if (which === "file" || settings.colors === false) return color.create(0);
|
|
297
454
|
if (settings.colors === true) return color.create(color.level > 0 ? color.level : 3);
|
|
298
|
-
if (settings.colors === false) return color.create(0);
|
|
299
455
|
const stream = streamOf(which);
|
|
300
456
|
return stream === process.stdout || stream === process.stderr ? color.create(color.detect(/** @type {any} */ (stream))) : color.create(0);
|
|
301
457
|
};
|
|
302
458
|
|
|
303
|
-
|
|
459
|
+
/**
|
|
460
|
+
* @param {"stdout" | "stderr" | "file"} which
|
|
461
|
+
* @returns {_Face}
|
|
462
|
+
*/
|
|
463
|
+
const faceOf = (which) => {
|
|
464
|
+
let face = faces[which];
|
|
465
|
+
if (face) return face;
|
|
466
|
+
const palette = paletteOf(which);
|
|
467
|
+
face = { palette, colors: palette.level > 0, labels: {}, scope: settings.scope ? palette.magenta(`[${settings.scope}]`) : "", stamp: "", stampAt: -1, stampWidth: 0 };
|
|
468
|
+
for (const kind of Object.keys(BADGES)) face.labels[kind] = BADGES[kind].label ? _style(palette, BADGES[kind].style.split(" "), BADGES[kind].label) : "";
|
|
469
|
+
faces[which] = face;
|
|
470
|
+
return face;
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* The timestamp, written again only when the second changes.
|
|
475
|
+
* @param {_Face} face
|
|
476
|
+
* @param {Date} now
|
|
477
|
+
*/
|
|
478
|
+
const stampOf = (face, now) => {
|
|
479
|
+
const second = Math.floor(now.getTime() / 1000);
|
|
480
|
+
if (second === face.stampAt) return;
|
|
481
|
+
face.stampAt = second;
|
|
482
|
+
// A pattern, or a time zone set with time.setTimezone(), needs the real formatter.
|
|
483
|
+
const text = typeof settings.timestamp === "string" || timeConfig.timeZone !== undefined
|
|
484
|
+
? require("./Time.js").format(now, typeof settings.timestamp === "string" ? settings.timestamp : "HH:mm:ss")
|
|
485
|
+
: `${_two(now.getHours())}:${_two(now.getMinutes())}:${_two(now.getSeconds())}`;
|
|
486
|
+
face.stamp = `${face.palette.gray(`[${text}]`)} `;
|
|
487
|
+
face.stampWidth = color.width(text) + 3;
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* @param {_Face} face
|
|
492
|
+
* @param {keyof typeof BADGES} kind
|
|
493
|
+
* @param {unknown[]} args
|
|
494
|
+
* @param {Date} now
|
|
495
|
+
* @param {boolean} stamped
|
|
496
|
+
* @returns {string}
|
|
497
|
+
*/
|
|
498
|
+
const render = (face, kind, args, now, stamped) => {
|
|
499
|
+
const { palette } = face;
|
|
500
|
+
let body = "";
|
|
501
|
+
for (let i = 0; i < args.length; i++) {
|
|
502
|
+
const text = _markup(_stringify(args[i], face.colors, secrets), delimiter, palette);
|
|
503
|
+
body += (i ? " " : "") + (face.colors && (kind === "debug" || kind === "trace") ? palette.gray(text) : text);
|
|
504
|
+
}
|
|
505
|
+
let prefix = "";
|
|
506
|
+
let padWidth = 0;
|
|
507
|
+
if (stamped) {
|
|
508
|
+
stampOf(face, now);
|
|
509
|
+
prefix = face.stamp;
|
|
510
|
+
padWidth = face.stampWidth;
|
|
511
|
+
}
|
|
512
|
+
if (groups.length) {
|
|
513
|
+
prefix += " ".repeat(groups.length);
|
|
514
|
+
padWidth += groups.length * 2;
|
|
515
|
+
}
|
|
516
|
+
if (face.scope) prefix += `${face.scope} `;
|
|
517
|
+
if (face.labels[kind]) prefix += `${face.labels[kind]} `;
|
|
518
|
+
if (body.indexOf("\n") === -1) return (prefix + body).trimEnd();
|
|
519
|
+
const lines = body.replace(TRIM_LINES, "").split(/\r?\n/);
|
|
520
|
+
const pad = " ".repeat(padWidth);
|
|
521
|
+
let out = (prefix + lines[0]).trimEnd();
|
|
522
|
+
for (let i = 1; i < lines.length; i++) out += `\n${pad}${lines[i]}`;
|
|
523
|
+
return out;
|
|
524
|
+
};
|
|
304
525
|
|
|
305
526
|
/**
|
|
306
527
|
* @param {keyof typeof BADGES} kind
|
|
307
528
|
* @param {unknown[]} args
|
|
308
529
|
*/
|
|
309
530
|
const emit = (kind, args) => {
|
|
531
|
+
if (!on[kind]) return;
|
|
310
532
|
const badge = BADGES[kind];
|
|
311
|
-
|
|
533
|
+
const now = new Date();
|
|
534
|
+
const stream = streamOf(badge.stream);
|
|
312
535
|
if (settings.format === "json") {
|
|
313
|
-
const line = _json(kind === "log" ? "info" : kind, args, settings.scope, settings.fields);
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
|
|
536
|
+
const line = _json(kind === "log" ? "info" : kind, args, settings.scope, settings.fields, secrets, now);
|
|
537
|
+
stream.write(`${line}\n`);
|
|
538
|
+
if (file) _writeFile(file, line);
|
|
539
|
+
} else {
|
|
540
|
+
const face = faceOf(badge.stream);
|
|
541
|
+
const line = render(face, kind, args, now, Boolean(settings.timestamp));
|
|
542
|
+
stream.write(`${line}\n`);
|
|
543
|
+
// The file gets the same line without colors: the one just made, when it has none.
|
|
544
|
+
if (file) _writeFile(file, face.colors ? render(faceOf("file"), kind, args, now, Boolean(settings.timestamp)) : line);
|
|
545
|
+
}
|
|
546
|
+
if (settings.onLog) {
|
|
547
|
+
try {
|
|
548
|
+
/** @type {unknown} */
|
|
549
|
+
const pending = settings.onLog({ time: now, level: badge.level, kind: /** @type {LogEntry["kind"]} */ (kind), scope: settings.scope, message: render(faceOf("file"), kind, args, now, false), args });
|
|
550
|
+
// An async hook that fails would otherwise be an unhandled rejection.
|
|
551
|
+
if (pending && typeof (/** @type {any} */ (pending).then) === "function") /** @type {PromiseLike<unknown>} */ (pending).then(undefined, () => {});
|
|
552
|
+
} catch {
|
|
553
|
+
// A broken hook must not take the program down with it.
|
|
554
|
+
}
|
|
317
555
|
}
|
|
318
|
-
const render = (/** @type {import("./Color").ColorModule} */ palette) => {
|
|
319
|
-
const colors = palette.level > 0;
|
|
320
|
-
const stamp = settings.timestamp
|
|
321
|
-
? _style(palette, ["gray"], `[${time.format(new Date(), typeof settings.timestamp === "string" ? settings.timestamp : "HH:mm:ss")}]`)
|
|
322
|
-
: "";
|
|
323
|
-
const scope = settings.scope ? _style(palette, ["magenta"], `[${settings.scope}]`) : "";
|
|
324
|
-
const label = badge.label ? _style(palette, badge.style.split(" "), badge.label) : "";
|
|
325
|
-
const body = args
|
|
326
|
-
.map((arg) => _markup(_stringify(arg, colors), settings.delimiter, palette))
|
|
327
|
-
.map((text) => (kind === "debug" || kind === "trace" ? _style(palette, ["gray"], text) : text))
|
|
328
|
-
.join(" ");
|
|
329
|
-
const indent = " ".repeat(groups.length);
|
|
330
|
-
const tags = [scope, label].filter(Boolean).join(" ");
|
|
331
|
-
const prefix = `${stamp ? `${stamp} ` : ""}${indent}${tags ? `${tags} ` : ""}`;
|
|
332
|
-
const lines = body.replace(/^(\s*\r?\n)+|(\r?\n\s*)+$/g, "").split(/\r?\n/);
|
|
333
|
-
const pad = " ".repeat(color.width(stamp ? `${stamp} ` : "") + indent.length);
|
|
334
|
-
return lines.map((line, i) => (i === 0 ? `${prefix}${line}`.trimEnd() : pad + line)).join("\n");
|
|
335
|
-
};
|
|
336
|
-
streamOf(badge.stream).write(`${render(paletteOf(badge.stream))}\n`);
|
|
337
|
-
if (settings.file) _writeFile(settings.file, render(plain));
|
|
338
556
|
};
|
|
339
557
|
|
|
558
|
+
refresh();
|
|
559
|
+
|
|
340
560
|
/** @type {Logger} */
|
|
341
561
|
const logger = {
|
|
342
|
-
log: (...args) => (emit("log", args), logger),
|
|
343
|
-
trace: (...args) => (emit("trace", args), logger),
|
|
344
|
-
debug: (...args) => (emit("debug", args), logger),
|
|
345
|
-
info: (...args) => (emit("info", args), logger),
|
|
346
|
-
success: (...args) => (emit("success", args), logger),
|
|
347
|
-
warn: (...args) => (emit("warn", args), logger),
|
|
348
|
-
error: (...args) => (emit("error", args), logger),
|
|
349
|
-
fatal: (...args) => (emit("fatal", args), logger),
|
|
562
|
+
log: (...args) => (on.log && emit("log", args), logger),
|
|
563
|
+
trace: (...args) => (on.trace && emit("trace", args), logger),
|
|
564
|
+
debug: (...args) => (on.debug && emit("debug", args), logger),
|
|
565
|
+
info: (...args) => (on.info && emit("info", args), logger),
|
|
566
|
+
success: (...args) => (on.success && emit("success", args), logger),
|
|
567
|
+
warn: (...args) => (on.warn && emit("warn", args), logger),
|
|
568
|
+
error: (...args) => (on.error && emit("error", args), logger),
|
|
569
|
+
fatal: (...args) => (on.fatal && emit("fatal", args), logger),
|
|
350
570
|
group: (label = "") => {
|
|
351
571
|
emit("log", [`▼ ${label}`.trimEnd()]);
|
|
352
572
|
groups.push(label);
|
|
@@ -365,10 +585,10 @@ const _create = (options) => {
|
|
|
365
585
|
if (start === undefined) return logger.warn(`Timer "${label}" does not exist`);
|
|
366
586
|
timers.delete(label);
|
|
367
587
|
const ms = Number(process.hrtime.bigint() - start) / 1e6;
|
|
368
|
-
return logger.log(`${label}: ${ms < 1000 ? `${Math.round(ms * 100) / 100}ms` :
|
|
588
|
+
return logger.log(`${label}: ${ms < 1000 ? `${Math.round(ms * 100) / 100}ms` : require("./Time.js").formatDuration(ms, { units: 2 })}`);
|
|
369
589
|
},
|
|
370
|
-
table: (rows, tableOptions) => logger.log(require("./Cli.js").table(rows, { ...tableOptions, colors:
|
|
371
|
-
box: (text, boxOptions) => logger.log(require("./Cli.js").box(text, { ...boxOptions, colors:
|
|
590
|
+
table: (rows, tableOptions) => logger.log(require("./Cli.js").table(rows, { ...tableOptions, colors: faceOf("stdout").colors })),
|
|
591
|
+
box: (text, boxOptions) => logger.log(require("./Cli.js").box(text, { ...boxOptions, colors: faceOf("stdout").colors })),
|
|
372
592
|
divider: (title) => {
|
|
373
593
|
const columns = /** @type {any} */ (streamOf("stdout")).columns ?? 80;
|
|
374
594
|
const width = Math.max(10, Math.min(columns, 100) - (settings.timestamp ? 11 : 0) - groups.length * 2);
|
|
@@ -383,12 +603,13 @@ const _create = (options) => {
|
|
|
383
603
|
scope: settings.scope ? `${settings.scope}:${scope}` : scope,
|
|
384
604
|
}),
|
|
385
605
|
setLevel: (level) => {
|
|
386
|
-
if (!(level
|
|
606
|
+
if (!Object.hasOwn(LEVELS, level)) throw new RangeError(`Unknown log level "${level}"`);
|
|
387
607
|
settings.level = level;
|
|
608
|
+
gate();
|
|
388
609
|
return logger;
|
|
389
610
|
},
|
|
390
611
|
getLevel: () => settings.level,
|
|
391
|
-
isLevelEnabled: (level) =>
|
|
612
|
+
isLevelEnabled: (level) => threshold !== Infinity && (LEVELS[level] >= threshold || (debugScope && (level === "debug" || level === "trace"))),
|
|
392
613
|
configure: (next) => {
|
|
393
614
|
if (next.level !== undefined) logger.setLevel(next.level);
|
|
394
615
|
if (next.scope !== undefined) settings.scope = next.scope;
|
|
@@ -400,6 +621,9 @@ const _create = (options) => {
|
|
|
400
621
|
if (next.stderr !== undefined) settings.stderr = next.stderr;
|
|
401
622
|
if (next.file !== undefined) settings.file = typeof next.file === "string" ? { path: next.file } : next.file;
|
|
402
623
|
if (next.fields !== undefined) settings.fields = next.fields;
|
|
624
|
+
if (next.redact !== undefined) settings.redact = next.redact;
|
|
625
|
+
if (next.onLog !== undefined) settings.onLog = next.onLog;
|
|
626
|
+
refresh();
|
|
403
627
|
return logger;
|
|
404
628
|
},
|
|
405
629
|
};
|
|
@@ -431,7 +655,7 @@ function createLogger(options = {}) {
|
|
|
431
655
|
* pretty-printed, and several arguments are joined with spaces.
|
|
432
656
|
*
|
|
433
657
|
* @example
|
|
434
|
-
* nc.log("<% green ✔ Saved
|
|
658
|
+
* nc.log("<% green ✔ Saved%> in <% bold 12ms%>");
|
|
435
659
|
* nc.log("user:", { id: 1, roles: ["admin"] });
|
|
436
660
|
*
|
|
437
661
|
* @param {...unknown} args
|
|
@@ -566,7 +790,7 @@ function groupEnd() {
|
|
|
566
790
|
* @example
|
|
567
791
|
* nc.timeStart("import");
|
|
568
792
|
* await importProducts();
|
|
569
|
-
* nc.timeEnd("import"); // "import:
|
|
793
|
+
* nc.timeEnd("import"); // "import: 1s 240ms"
|
|
570
794
|
*
|
|
571
795
|
* @param {string} [label="default"]
|
|
572
796
|
* @returns {typeof import("./Logger")}
|