@ayepi/log 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 +216 -0
- package/dist/file.cjs +175 -0
- package/dist/file.d.cts +50 -0
- package/dist/file.d.ts +50 -0
- package/dist/file.js +174 -0
- package/dist/index.cjs +239 -0
- package/dist/index.d.cts +125 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.js +218 -0
- package/dist/internal.cjs +546 -0
- package/dist/internal.d.cts +153 -0
- package/dist/internal.d.ts +153 -0
- package/dist/internal.js +415 -0
- package/dist/middleware.cjs +38 -0
- package/dist/middleware.d.cts +27 -0
- package/dist/middleware.d.ts +27 -0
- package/dist/middleware.js +37 -0
- package/dist/server.cjs +40 -0
- package/dist/server.d.cts +36 -0
- package/dist/server.d.ts +36 -0
- package/dist/server.js +39 -0
- package/package.json +100 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
let node_async_hooks = require("node:async_hooks");
|
|
2
|
+
//#region src/internal.ts
|
|
3
|
+
/**
|
|
4
|
+
* # @ayepi/log internals
|
|
5
|
+
*
|
|
6
|
+
* Shared, dependency-free building blocks used by every entry: the level table,
|
|
7
|
+
* the collision-renaming context {@link merge}, {@link deepEqual}, error
|
|
8
|
+
* serialization, the {@link AsyncLocalStorage}-backed trace context + {@link runWith}
|
|
9
|
+
* (the `logWith` core), record building, and formatting.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
/** Severity ordering — lower = more verbose. The threshold emits levels `>=` it. */
|
|
14
|
+
const LEVELS = {
|
|
15
|
+
debug: 10,
|
|
16
|
+
info: 20,
|
|
17
|
+
warn: 30,
|
|
18
|
+
error: 40
|
|
19
|
+
};
|
|
20
|
+
/** Default threshold. */
|
|
21
|
+
const DEFAULT_LEVEL = "info";
|
|
22
|
+
/** Give up suffixing after this many collisions (guards pathological loops). The first suffix is `key2`. */
|
|
23
|
+
const SUFFIX_MAX = 100;
|
|
24
|
+
/** Default `console` method → level mapping for interception (the logging-output methods). */
|
|
25
|
+
const CONSOLE_LEVEL_MAP = {
|
|
26
|
+
log: "info",
|
|
27
|
+
info: "info",
|
|
28
|
+
debug: "debug",
|
|
29
|
+
warn: "warn",
|
|
30
|
+
error: "error",
|
|
31
|
+
trace: "debug",
|
|
32
|
+
dir: "info"
|
|
33
|
+
};
|
|
34
|
+
/** Default `cause` recursion depth for error serialization. */
|
|
35
|
+
const DEFAULT_MAX_CAUSE_DEPTH = 5;
|
|
36
|
+
/** Symbol under which `logWith` stashes the full merged context onto a rejected error. `Symbol.for` → stable across bundled entries. */
|
|
37
|
+
const LOG_CONTEXT = Symbol.for("@ayepi/log:ctx");
|
|
38
|
+
/** Placeholder substituted for a value whose resolution (a `toLOG`/`toJSON` hook, a `logMaybe`, a promise) threw or rejected. */
|
|
39
|
+
const UNRESOLVED = "(unresolved value)";
|
|
40
|
+
/** Recursive structural equality. Handles primitives, `Date`, arrays, `Error`, plain objects, and cycles. */
|
|
41
|
+
function deepEqual(a, b, seen = /* @__PURE__ */ new WeakMap()) {
|
|
42
|
+
if (Object.is(a, b)) return true;
|
|
43
|
+
if (typeof a !== typeof b || a === null || b === null || typeof a !== "object") return false;
|
|
44
|
+
const ao = a;
|
|
45
|
+
const bo = b;
|
|
46
|
+
if (seen.get(ao) === bo) return true;
|
|
47
|
+
seen.set(ao, bo);
|
|
48
|
+
if (a instanceof Date || b instanceof Date) return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
|
|
49
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
50
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
51
|
+
return a.every((x, i) => deepEqual(x, b[i], seen));
|
|
52
|
+
}
|
|
53
|
+
if (a instanceof Error || b instanceof Error) return a instanceof Error && b instanceof Error && a.name === b.name && a.message === b.message;
|
|
54
|
+
const ar = a;
|
|
55
|
+
const br = b;
|
|
56
|
+
const ak = Object.keys(ar);
|
|
57
|
+
if (ak.length !== Object.keys(br).length) return false;
|
|
58
|
+
return ak.every((k) => k in br && deepEqual(ar[k], br[k], seen));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Merge `b` into `a` immutably. `a` keeps all of its own keys; each key of `b` is
|
|
62
|
+
* placed in the first slot among `key, key2, key3, …` that is free **or** already
|
|
63
|
+
* deep-equals `b`'s value (dedup). Returns a **new** object — neither input is
|
|
64
|
+
* mutated.
|
|
65
|
+
*/
|
|
66
|
+
function merge(a, b) {
|
|
67
|
+
const result = { ...a };
|
|
68
|
+
for (const key of Object.keys(b)) {
|
|
69
|
+
const v = b[key];
|
|
70
|
+
let placed = false;
|
|
71
|
+
for (let i = 1; i <= SUFFIX_MAX; i++) {
|
|
72
|
+
const slot = i === 1 ? key : `${key}${i}`;
|
|
73
|
+
if (!(slot in result)) {
|
|
74
|
+
result[slot] = v;
|
|
75
|
+
placed = true;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
if (deepEqual(result[slot], v)) {
|
|
79
|
+
placed = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!placed) result[`${key}${SUFFIX_MAX}_overflow`] = v;
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
/** Serialize `err` per `cfg` (stack/cause/fields, depth-bounded). Non-`Error` values become `{ name:'NonError', message }`. */
|
|
88
|
+
function serializeError(err, cfg = {}, depth = 0) {
|
|
89
|
+
if (!(err instanceof Error)) return {
|
|
90
|
+
name: "NonError",
|
|
91
|
+
message: typeof err === "string" ? err : safeString(err)
|
|
92
|
+
};
|
|
93
|
+
const out = {
|
|
94
|
+
name: err.name,
|
|
95
|
+
message: err.message
|
|
96
|
+
};
|
|
97
|
+
if (cfg.stack !== false && err.stack) out.stack = err.stack;
|
|
98
|
+
const cause = err.cause;
|
|
99
|
+
if (cfg.cause !== false && cause !== void 0 && depth < (cfg.maxCauseDepth ?? DEFAULT_MAX_CAUSE_DEPTH)) out.cause = cause instanceof Error ? serializeError(cause, cfg, depth + 1) : cause;
|
|
100
|
+
if (cfg.fields !== false) {
|
|
101
|
+
const rec = err;
|
|
102
|
+
for (const k of Object.keys(err)) {
|
|
103
|
+
if (k === "name" || k === "message" || k === "stack" || k === "cause") continue;
|
|
104
|
+
out[k] = rec[k];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
function safeString(v) {
|
|
110
|
+
try {
|
|
111
|
+
return String(v);
|
|
112
|
+
} catch {
|
|
113
|
+
return "[unstringifiable]";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const store = new node_async_hooks.AsyncLocalStorage();
|
|
117
|
+
/** The current merged `logWith` context (empty object outside any `logWith`). */
|
|
118
|
+
const getContext = () => store.getStore() ?? {};
|
|
119
|
+
/** Attach the full context to a rejected error under {@link LOG_CONTEXT} — only if not already present (innermost wins). */
|
|
120
|
+
function attachContext(err, ctx) {
|
|
121
|
+
if (err !== null && typeof err === "object" && !(LOG_CONTEXT in err)) Object.defineProperty(err, LOG_CONTEXT, {
|
|
122
|
+
value: ctx,
|
|
123
|
+
enumerable: false,
|
|
124
|
+
configurable: true,
|
|
125
|
+
writable: true
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** The core of `logWith`: merge `add` into the current context, run `inner` within it, tag promise rejections. */
|
|
129
|
+
function runWith(add, inner) {
|
|
130
|
+
const merged = merge(getContext(), add);
|
|
131
|
+
return store.run(merged, () => {
|
|
132
|
+
const out = inner();
|
|
133
|
+
if (out !== null && typeof out === "object" && typeof out.then === "function") return out.then((v) => v, (err) => {
|
|
134
|
+
attachContext(err, merged);
|
|
135
|
+
throw err;
|
|
136
|
+
});
|
|
137
|
+
return out;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const effectiveErrorCfg = (level, cfg) => {
|
|
141
|
+
const { perLevel, ...base } = cfg;
|
|
142
|
+
return {
|
|
143
|
+
...base,
|
|
144
|
+
...perLevel?.[level]
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
/** Try each serializer in turn; the first that returns non-`undefined` wins (a throw declines + reports). */
|
|
148
|
+
function runSerializers(value, serializers, opts) {
|
|
149
|
+
for (const s of serializers) {
|
|
150
|
+
let out;
|
|
151
|
+
try {
|
|
152
|
+
out = s(value);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
opts.onError?.(err);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (out !== void 0) return out;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Call a hook, substituting {@link UNRESOLVED} (and reporting) if it throws — logging is best-effort. */
|
|
161
|
+
function safeCall(fn, ctx, key, opts) {
|
|
162
|
+
try {
|
|
163
|
+
return fn.call(ctx, key);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
opts.onError?.(err);
|
|
166
|
+
return UNRESOLVED;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The resolver core (see {@link resolveLogValue}). `opts.onError` observes a throwing hook or a
|
|
171
|
+
* rejecting promise; without it, such failures still degrade to {@link UNRESOLVED} (rejections
|
|
172
|
+
* propagate to the awaiter). A `toLOG`/`toJSON`/promise may resolve asynchronously: the returned
|
|
173
|
+
* structure then carries embedded promises, which {@link settleDeep} awaits before the record is built.
|
|
174
|
+
*/
|
|
175
|
+
function resolveLog(value, key, seen, opts) {
|
|
176
|
+
if (value === null || typeof value !== "object") return value;
|
|
177
|
+
if (opts.serializers) {
|
|
178
|
+
const s = runSerializers(value, opts.serializers, opts);
|
|
179
|
+
if (s !== void 0) return resolveLog(s, key, seen, opts);
|
|
180
|
+
}
|
|
181
|
+
if (value instanceof Error) return value;
|
|
182
|
+
if (isThenable(value)) return value.then((r) => resolveLog(r, key, seen, opts), (err) => {
|
|
183
|
+
opts.onError?.(err);
|
|
184
|
+
return UNRESOLVED;
|
|
185
|
+
});
|
|
186
|
+
const toLog = value.toLOG;
|
|
187
|
+
if (typeof toLog === "function") return resolveLog(safeCall(toLog, value, key, opts), key, seen, opts);
|
|
188
|
+
const toJson = value.toJSON;
|
|
189
|
+
if (typeof toJson === "function") return resolveLog(safeCall(toJson, value, key, opts), key, seen, opts);
|
|
190
|
+
if (seen.has(value)) return value;
|
|
191
|
+
seen.add(value);
|
|
192
|
+
if (Array.isArray(value)) return value.map((v, i) => resolveLog(v, String(i), seen, opts));
|
|
193
|
+
const src = value;
|
|
194
|
+
const out = {};
|
|
195
|
+
for (const k of Object.keys(src)) out[k] = resolveLog(src[k], k, seen, opts);
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Resolve a value to its **loggable plain shape**, deeply and eagerly — so a logged value carries
|
|
200
|
+
* its intended shape everywhere consistently: in the record object the transports receive, through
|
|
201
|
+
* the `sanitize` pass, and in both the JSON and text output (not just incidentally when a formatter
|
|
202
|
+
* happens to stringify it). Two serialization hooks are honored before a structural copy:
|
|
203
|
+
*
|
|
204
|
+
* 1. **`toLOG()`** — a logging-specific hook that **takes precedence**: when present, the value
|
|
205
|
+
* becomes its result. Use it to shape a value for logs alone, without affecting `JSON.stringify`
|
|
206
|
+
* / API responses (which still use `toJSON`). It may return a **promise** (resolved before the
|
|
207
|
+
* record is built).
|
|
208
|
+
* 2. **`toJSON(key)`** — the standard hook `JSON.stringify` uses (e.g. `Date`).
|
|
209
|
+
*
|
|
210
|
+
* Objects/arrays without either hook are rebuilt from their own enumerable entries (mirroring
|
|
211
|
+
* `JSON.stringify`); `Error`s and primitives pass through; cycles are left as the original
|
|
212
|
+
* reference. A promise anywhere (an async hook, or a raw promise value) becomes its awaited result.
|
|
213
|
+
*/
|
|
214
|
+
function resolveLogValue(value, key = "", seen = /* @__PURE__ */ new WeakSet()) {
|
|
215
|
+
return resolveLog(value, key, seen, {});
|
|
216
|
+
}
|
|
217
|
+
/** True if `value` is, or (deeply) contains, a thenable — i.e. resolving it needs an async pass. */
|
|
218
|
+
function containsThenable(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
219
|
+
if (isThenable(value)) return true;
|
|
220
|
+
if (value === null || typeof value !== "object" || seen.has(value)) return false;
|
|
221
|
+
seen.add(value);
|
|
222
|
+
if (Array.isArray(value)) return value.some((v) => containsThenable(v, seen));
|
|
223
|
+
return Object.values(value).some((v) => containsThenable(v, seen));
|
|
224
|
+
}
|
|
225
|
+
/** Await every embedded promise in an already-resolved structure, yielding plain data. */
|
|
226
|
+
async function settleDeep(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
227
|
+
if (isThenable(value)) return settleDeep(await value, seen);
|
|
228
|
+
if (value === null || typeof value !== "object") return value;
|
|
229
|
+
if (seen.has(value)) return value;
|
|
230
|
+
seen.add(value);
|
|
231
|
+
if (Array.isArray(value)) return Promise.all(value.map((v) => settleDeep(v, seen)));
|
|
232
|
+
const src = value;
|
|
233
|
+
const out = {};
|
|
234
|
+
for (const k of Object.keys(src)) out[k] = await settleDeep(src[k], seen);
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Build a {@link LogRecord} from `log()` args. `msg` is the space-joined non-object
|
|
239
|
+
* args; objects and the ambient context are merged (ambient keeps bare keys);
|
|
240
|
+
* `Error` args become `error`/`additionalErrors` and contribute any attached
|
|
241
|
+
* trace context.
|
|
242
|
+
*/
|
|
243
|
+
function buildRecord(level, args, opts) {
|
|
244
|
+
const tms = opts.timestamp === "epoch" ? opts.now() : new Date(opts.now()).toISOString();
|
|
245
|
+
const msgParts = [];
|
|
246
|
+
const objects = [];
|
|
247
|
+
const errors = [];
|
|
248
|
+
for (const arg of args) if (arg instanceof Error) errors.push(arg);
|
|
249
|
+
else if (arg !== null && typeof arg === "object") objects.push(arg);
|
|
250
|
+
else msgParts.push(safeString(arg));
|
|
251
|
+
const errCfg = effectiveErrorCfg(level, opts.error);
|
|
252
|
+
let error;
|
|
253
|
+
const additionalErrors = [];
|
|
254
|
+
const errorContexts = [];
|
|
255
|
+
errors.forEach((e, i) => {
|
|
256
|
+
const ser = serializeError(e, errCfg);
|
|
257
|
+
if (i === 0) error = ser;
|
|
258
|
+
else additionalErrors.push(ser);
|
|
259
|
+
const attached = e[LOG_CONTEXT];
|
|
260
|
+
if (attached !== null && typeof attached === "object") errorContexts.push(attached);
|
|
261
|
+
});
|
|
262
|
+
let record = {
|
|
263
|
+
tms,
|
|
264
|
+
level,
|
|
265
|
+
msg: msgParts.join(" ")
|
|
266
|
+
};
|
|
267
|
+
if (error) record.error = error;
|
|
268
|
+
if (additionalErrors.length) record.additionalErrors = additionalErrors;
|
|
269
|
+
record = merge(record, getContext());
|
|
270
|
+
for (const o of objects) record = merge(record, o);
|
|
271
|
+
for (const c of errorContexts) record = merge(record, c);
|
|
272
|
+
return record;
|
|
273
|
+
}
|
|
274
|
+
const renderValue = (v) => {
|
|
275
|
+
if (typeof v === "string") return v;
|
|
276
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
|
|
277
|
+
if (v === null || v === void 0) return String(v);
|
|
278
|
+
try {
|
|
279
|
+
return JSON.stringify(v) ?? safeString(v);
|
|
280
|
+
} catch {
|
|
281
|
+
return safeString(v);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
/** `[tms] level msg key=value, key=value` plus a trailing `error=Name: message`. */
|
|
285
|
+
function formatText(record) {
|
|
286
|
+
const { tms, level, msg, error, additionalErrors, ...rest } = record;
|
|
287
|
+
let line = `[${String(tms)}] ${level} ${msg}`.trimEnd();
|
|
288
|
+
const pairs = Object.entries(rest).map(([k, v]) => `${k}=${renderValue(v)}`);
|
|
289
|
+
if (pairs.length) line += ` ${pairs.join(", ")}`;
|
|
290
|
+
if (error) line += ` error=${error.name}: ${error.message}`;
|
|
291
|
+
if (additionalErrors && additionalErrors.length) line += ` (+${additionalErrors.length} more)`;
|
|
292
|
+
return line;
|
|
293
|
+
}
|
|
294
|
+
/** Stable JSON, dropping `undefined` and guarding residual cycles. */
|
|
295
|
+
function formatJson(record) {
|
|
296
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
297
|
+
return JSON.stringify(record, (_k, v) => {
|
|
298
|
+
if (v === void 0) return;
|
|
299
|
+
if (v !== null && typeof v === "object") {
|
|
300
|
+
if (seen.has(v)) return "[Circular]";
|
|
301
|
+
seen.add(v);
|
|
302
|
+
}
|
|
303
|
+
return v;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/** Symbol marking a {@link logMaybe} value; `Symbol.for` keeps it stable across bundled entries. */
|
|
307
|
+
const LOG_MAYBE = Symbol.for("@ayepi/log:maybe");
|
|
308
|
+
/** Node's custom-inspect symbol, so a lazy value renders nicely under a non-intercepted `console.log`. */
|
|
309
|
+
const INSPECT = Symbol.for("nodejs.util.inspect.custom");
|
|
310
|
+
/** True for a thenable (a `Promise` or any `{ then(): … }`). */
|
|
311
|
+
const isThenable = (v) => v !== null && typeof v === "object" && typeof v.then === "function";
|
|
312
|
+
/** True for a {@link logMaybe} marker. */
|
|
313
|
+
const isLazy = (v) => v !== null && typeof v === "object" && LOG_MAYBE in v;
|
|
314
|
+
/**
|
|
315
|
+
* Defer an expensive log argument. The function runs **only if** the line will actually be
|
|
316
|
+
* logged (its level passes the threshold): under console interception the structured pipeline
|
|
317
|
+
* calls it (with the record's level) and awaits the result, treating it as a normal argument.
|
|
318
|
+
* On the non-intercepted path `toJSON` (and Node's inspect) render the synchronous value, or
|
|
319
|
+
* `'(unresolved value)'` when the function returns a promise that can't be awaited there.
|
|
320
|
+
*
|
|
321
|
+
* ```ts
|
|
322
|
+
* log.debug('state', logMaybe(() => expensiveSnapshot())) // snapshot built only at debug level
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
function logMaybe(fn) {
|
|
326
|
+
const render = () => {
|
|
327
|
+
let v;
|
|
328
|
+
try {
|
|
329
|
+
v = fn("info");
|
|
330
|
+
} catch {
|
|
331
|
+
return UNRESOLVED;
|
|
332
|
+
}
|
|
333
|
+
return isThenable(v) ? UNRESOLVED : v;
|
|
334
|
+
};
|
|
335
|
+
return {
|
|
336
|
+
[LOG_MAYBE]: fn,
|
|
337
|
+
toJSON: render,
|
|
338
|
+
[INSPECT]: render
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const DEFAULT_MASK = () => "[redacted]";
|
|
342
|
+
/**
|
|
343
|
+
* A {@link SanitizeOptions.mask} that keeps the first `keep` characters and replaces the rest
|
|
344
|
+
* with `fill` (e.g. `partialMask(3)('secret-token') === 'sec***'`). Values no longer than
|
|
345
|
+
* `keep` are fully masked, so nothing short leaks. With the default `keep` of 0 it masks fully.
|
|
346
|
+
*/
|
|
347
|
+
function partialMask(keep = 0, fill = "***") {
|
|
348
|
+
return (value) => {
|
|
349
|
+
const s = typeof value === "string" ? value : safeString(value);
|
|
350
|
+
return s.length <= keep ? fill : s.slice(0, keep) + fill;
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const matchKey = (key, patterns) => patterns.some((p) => typeof p === "string" ? p.toLowerCase() === key.toLowerCase() : p.test(key));
|
|
354
|
+
const matchValue = (val, patterns) => patterns.some((p) => typeof p === "string" ? val.toLowerCase().includes(p.toLowerCase()) : p.test(val));
|
|
355
|
+
const kindOf = (v) => v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
|
|
356
|
+
const isHomogeneous = (arr) => {
|
|
357
|
+
const k = kindOf(arr[0]);
|
|
358
|
+
return arr.every((e) => kindOf(e) === k);
|
|
359
|
+
};
|
|
360
|
+
const isPlainObject = (v) => {
|
|
361
|
+
const proto = Object.getPrototypeOf(v);
|
|
362
|
+
return proto === Object.prototype || proto === null;
|
|
363
|
+
};
|
|
364
|
+
const truncateString = (s, max) => `${s.slice(0, max)}... (+${s.length - max} more)`;
|
|
365
|
+
/**
|
|
366
|
+
* Build a record transformer that redacts sensitive keys/values and truncates long
|
|
367
|
+
* strings/arrays per {@link SanitizeOptions}. Returns the (new) record, or `null` to drop it
|
|
368
|
+
* (the `filter` returned `false`) — the same shape as `LoggerConfig.filter`, so it composes
|
|
369
|
+
* there too. Only plain objects and arrays are walked; `Date`/class instances/{@link logMaybe}
|
|
370
|
+
* markers pass through untouched, and the reserved `tms`/`level` fields are left pristine.
|
|
371
|
+
*/
|
|
372
|
+
function createSanitizer(opts) {
|
|
373
|
+
const mask = opts.mask ?? DEFAULT_MASK;
|
|
374
|
+
const keys = opts.sensitiveKeys ?? [];
|
|
375
|
+
const values = opts.sensitiveValues ?? [];
|
|
376
|
+
const maxStr = opts.maxStringLength;
|
|
377
|
+
const maxArr = opts.maxArrayLength;
|
|
378
|
+
const visit = (value, key, seen) => {
|
|
379
|
+
if (key !== void 0 && keys.length > 0 && matchKey(key, keys)) return mask(value, key);
|
|
380
|
+
if (typeof value === "string") {
|
|
381
|
+
if (values.length > 0 && matchValue(value, values)) return mask(value, key);
|
|
382
|
+
if (maxStr !== void 0 && value.length > maxStr) return truncateString(value, maxStr);
|
|
383
|
+
return value;
|
|
384
|
+
}
|
|
385
|
+
if (value === null || typeof value !== "object" || isLazy(value)) return value;
|
|
386
|
+
if (seen.has(value)) return value;
|
|
387
|
+
if (Array.isArray(value)) {
|
|
388
|
+
seen.add(value);
|
|
389
|
+
let kept = value;
|
|
390
|
+
let extra = 0;
|
|
391
|
+
if (maxArr !== void 0 && value.length > maxArr && isHomogeneous(value)) {
|
|
392
|
+
extra = value.length - maxArr;
|
|
393
|
+
kept = value.slice(0, maxArr);
|
|
394
|
+
}
|
|
395
|
+
const out = kept.map((e) => visit(e, void 0, seen));
|
|
396
|
+
if (extra > 0) out.push(`(+${extra} more)`);
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
if (!isPlainObject(value)) return value;
|
|
400
|
+
seen.add(value);
|
|
401
|
+
const src = value;
|
|
402
|
+
const masked = {};
|
|
403
|
+
for (const k of Object.keys(src)) masked[k] = visit(src[k], k, seen);
|
|
404
|
+
return masked;
|
|
405
|
+
};
|
|
406
|
+
return (record) => {
|
|
407
|
+
if (opts.filter && !opts.filter(record)) return null;
|
|
408
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
409
|
+
const out = {};
|
|
410
|
+
for (const k of Object.keys(record)) out[k] = k === "tms" || k === "level" ? record[k] : visit(record[k], k, seen);
|
|
411
|
+
return out;
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
//#endregion
|
|
415
|
+
Object.defineProperty(exports, "CONSOLE_LEVEL_MAP", {
|
|
416
|
+
enumerable: true,
|
|
417
|
+
get: function() {
|
|
418
|
+
return CONSOLE_LEVEL_MAP;
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
Object.defineProperty(exports, "DEFAULT_LEVEL", {
|
|
422
|
+
enumerable: true,
|
|
423
|
+
get: function() {
|
|
424
|
+
return DEFAULT_LEVEL;
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
Object.defineProperty(exports, "LEVELS", {
|
|
428
|
+
enumerable: true,
|
|
429
|
+
get: function() {
|
|
430
|
+
return LEVELS;
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
Object.defineProperty(exports, "LOG_CONTEXT", {
|
|
434
|
+
enumerable: true,
|
|
435
|
+
get: function() {
|
|
436
|
+
return LOG_CONTEXT;
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
Object.defineProperty(exports, "LOG_MAYBE", {
|
|
440
|
+
enumerable: true,
|
|
441
|
+
get: function() {
|
|
442
|
+
return LOG_MAYBE;
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
Object.defineProperty(exports, "UNRESOLVED", {
|
|
446
|
+
enumerable: true,
|
|
447
|
+
get: function() {
|
|
448
|
+
return UNRESOLVED;
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
Object.defineProperty(exports, "buildRecord", {
|
|
452
|
+
enumerable: true,
|
|
453
|
+
get: function() {
|
|
454
|
+
return buildRecord;
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
Object.defineProperty(exports, "containsThenable", {
|
|
458
|
+
enumerable: true,
|
|
459
|
+
get: function() {
|
|
460
|
+
return containsThenable;
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
Object.defineProperty(exports, "createSanitizer", {
|
|
464
|
+
enumerable: true,
|
|
465
|
+
get: function() {
|
|
466
|
+
return createSanitizer;
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
Object.defineProperty(exports, "deepEqual", {
|
|
470
|
+
enumerable: true,
|
|
471
|
+
get: function() {
|
|
472
|
+
return deepEqual;
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
Object.defineProperty(exports, "formatJson", {
|
|
476
|
+
enumerable: true,
|
|
477
|
+
get: function() {
|
|
478
|
+
return formatJson;
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
Object.defineProperty(exports, "formatText", {
|
|
482
|
+
enumerable: true,
|
|
483
|
+
get: function() {
|
|
484
|
+
return formatText;
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
Object.defineProperty(exports, "getContext", {
|
|
488
|
+
enumerable: true,
|
|
489
|
+
get: function() {
|
|
490
|
+
return getContext;
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
Object.defineProperty(exports, "isLazy", {
|
|
494
|
+
enumerable: true,
|
|
495
|
+
get: function() {
|
|
496
|
+
return isLazy;
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
Object.defineProperty(exports, "logMaybe", {
|
|
500
|
+
enumerable: true,
|
|
501
|
+
get: function() {
|
|
502
|
+
return logMaybe;
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
Object.defineProperty(exports, "merge", {
|
|
506
|
+
enumerable: true,
|
|
507
|
+
get: function() {
|
|
508
|
+
return merge;
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
Object.defineProperty(exports, "partialMask", {
|
|
512
|
+
enumerable: true,
|
|
513
|
+
get: function() {
|
|
514
|
+
return partialMask;
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
Object.defineProperty(exports, "resolveLog", {
|
|
518
|
+
enumerable: true,
|
|
519
|
+
get: function() {
|
|
520
|
+
return resolveLog;
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
Object.defineProperty(exports, "resolveLogValue", {
|
|
524
|
+
enumerable: true,
|
|
525
|
+
get: function() {
|
|
526
|
+
return resolveLogValue;
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
Object.defineProperty(exports, "runWith", {
|
|
530
|
+
enumerable: true,
|
|
531
|
+
get: function() {
|
|
532
|
+
return runWith;
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
Object.defineProperty(exports, "serializeError", {
|
|
536
|
+
enumerable: true,
|
|
537
|
+
get: function() {
|
|
538
|
+
return serializeError;
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
Object.defineProperty(exports, "settleDeep", {
|
|
542
|
+
enumerable: true,
|
|
543
|
+
get: function() {
|
|
544
|
+
return settleDeep;
|
|
545
|
+
}
|
|
546
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
//#region src/internal.d.ts
|
|
2
|
+
|
|
3
|
+
/** Symbol under which `logWith` stashes the full merged context onto a rejected error. `Symbol.for` → stable across bundled entries. */
|
|
4
|
+
declare const LOG_CONTEXT: unique symbol;
|
|
5
|
+
/** Placeholder substituted for a value whose resolution (a `toLOG`/`toJSON` hook, a `logMaybe`, a promise) threw or rejected. */
|
|
6
|
+
|
|
7
|
+
/** The log levels, in console-method parity. */
|
|
8
|
+
type Level = 'debug' | 'info' | 'warn' | 'error';
|
|
9
|
+
/** A finished, fully-merged log record. Always carries the reserved fields. */
|
|
10
|
+
interface LogRecord {
|
|
11
|
+
/** Timestamp — ISO string by default, numeric epoch ms when `timestamp:'epoch'`. */
|
|
12
|
+
readonly tms: string | number;
|
|
13
|
+
readonly level: Level;
|
|
14
|
+
readonly msg: string;
|
|
15
|
+
/** Serialized primary error (the first `Error` arg). */
|
|
16
|
+
readonly error?: SerializedError;
|
|
17
|
+
/** Serialized 2nd+ `Error` args. */
|
|
18
|
+
readonly additionalErrors?: readonly SerializedError[];
|
|
19
|
+
/** Merged fields (ambient context + object args + error-attached context). */
|
|
20
|
+
readonly [key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
/** A serialized `Error`: standard fields, depth-bounded `cause`, and own enumerable props. */
|
|
23
|
+
interface SerializedError {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly message: string;
|
|
26
|
+
readonly stack?: string;
|
|
27
|
+
readonly cause?: unknown;
|
|
28
|
+
readonly [key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
/** What to capture when serializing an `Error`. */
|
|
31
|
+
interface ErrorCaptureConfig {
|
|
32
|
+
/** Include `error.stack` (default `true`). */
|
|
33
|
+
readonly stack?: boolean;
|
|
34
|
+
/** Recurse into `error.cause` (default `true`). */
|
|
35
|
+
readonly cause?: boolean;
|
|
36
|
+
/** Include own enumerable non-standard props like `code`/`statusCode` (default `true`). */
|
|
37
|
+
readonly fields?: boolean;
|
|
38
|
+
/** Max `cause` recursion depth (default 5). */
|
|
39
|
+
readonly maxCauseDepth?: number;
|
|
40
|
+
}
|
|
41
|
+
/** Error capture config plus per-level overrides ("what is captured at each level"). */
|
|
42
|
+
interface ErrorConfig extends ErrorCaptureConfig {
|
|
43
|
+
/** Per-level overrides, shallow-merged over the base (e.g. drop stacks below `error`). */
|
|
44
|
+
readonly perLevel?: Partial<Record<Level, ErrorCaptureConfig>>;
|
|
45
|
+
}
|
|
46
|
+
/** A sink for finished records. */
|
|
47
|
+
interface Transport {
|
|
48
|
+
/** A name, for debugging. */
|
|
49
|
+
readonly name: string;
|
|
50
|
+
/** Write one record; `text` is the pre-formatted line. May be async; the logger never awaits it. */
|
|
51
|
+
write(record: LogRecord, text: string): void | Promise<void>;
|
|
52
|
+
/** Optional: drain any buffered writes to their destination (without tearing the transport down). */
|
|
53
|
+
flush?(): void | Promise<void>;
|
|
54
|
+
/** Optional: flush and release resources (timers, file handles). The transport isn't used after. */
|
|
55
|
+
close?(): void | Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Shape a value the logger doesn't own (and so can't carry a {@link logMaybe}/`toLOG` hook) —
|
|
59
|
+
* e.g. a `Request`, `URL`, `Buffer`, or a third-party class. Return the replacement shape, or
|
|
60
|
+
* `undefined` to decline (the next serializer, then `toLOG`/`toJSON`/a structural copy, is tried).
|
|
61
|
+
* Applied at **every depth**. Configured via `LoggerConfig.serializers`.
|
|
62
|
+
*/
|
|
63
|
+
type Serializer = (value: object) => unknown;
|
|
64
|
+
/** Recursive structural equality. Handles primitives, `Date`, arrays, `Error`, plain objects, and cycles. */
|
|
65
|
+
declare function deepEqual(a: unknown, b: unknown, seen?: WeakMap<object, object>): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Merge `b` into `a` immutably. `a` keeps all of its own keys; each key of `b` is
|
|
68
|
+
* placed in the first slot among `key, key2, key3, …` that is free **or** already
|
|
69
|
+
* deep-equals `b`'s value (dedup). Returns a **new** object — neither input is
|
|
70
|
+
* mutated.
|
|
71
|
+
*/
|
|
72
|
+
declare function merge(a: Record<string, unknown>, b: Record<string, unknown>): Record<string, unknown>;
|
|
73
|
+
/** Serialize `err` per `cfg` (stack/cause/fields, depth-bounded). Non-`Error` values become `{ name:'NonError', message }`. */
|
|
74
|
+
declare function serializeError(err: unknown, cfg?: ErrorCaptureConfig, depth?: number): SerializedError;
|
|
75
|
+
/** The current merged `logWith` context (empty object outside any `logWith`). */
|
|
76
|
+
declare const getContext: () => Record<string, unknown>;
|
|
77
|
+
/** The core of `logWith`: merge `add` into the current context, run `inner` within it, tag promise rejections. */
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a value to its **loggable plain shape**, deeply and eagerly — so a logged value carries
|
|
81
|
+
* its intended shape everywhere consistently: in the record object the transports receive, through
|
|
82
|
+
* the `sanitize` pass, and in both the JSON and text output (not just incidentally when a formatter
|
|
83
|
+
* happens to stringify it). Two serialization hooks are honored before a structural copy:
|
|
84
|
+
*
|
|
85
|
+
* 1. **`toLOG()`** — a logging-specific hook that **takes precedence**: when present, the value
|
|
86
|
+
* becomes its result. Use it to shape a value for logs alone, without affecting `JSON.stringify`
|
|
87
|
+
* / API responses (which still use `toJSON`). It may return a **promise** (resolved before the
|
|
88
|
+
* record is built).
|
|
89
|
+
* 2. **`toJSON(key)`** — the standard hook `JSON.stringify` uses (e.g. `Date`).
|
|
90
|
+
*
|
|
91
|
+
* Objects/arrays without either hook are rebuilt from their own enumerable entries (mirroring
|
|
92
|
+
* `JSON.stringify`); `Error`s and primitives pass through; cycles are left as the original
|
|
93
|
+
* reference. A promise anywhere (an async hook, or a raw promise value) becomes its awaited result.
|
|
94
|
+
*/
|
|
95
|
+
declare function resolveLogValue(value: unknown, key?: string, seen?: WeakSet<object>): unknown;
|
|
96
|
+
/** True if `value` is, or (deeply) contains, a thenable — i.e. resolving it needs an async pass. */
|
|
97
|
+
|
|
98
|
+
/** A value produced either synchronously or asynchronously. */
|
|
99
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
100
|
+
/** Symbol marking a {@link logMaybe} value; `Symbol.for` keeps it stable across bundled entries. */
|
|
101
|
+
declare const LOG_MAYBE: unique symbol;
|
|
102
|
+
/** A deferred log argument produced by {@link logMaybe}. */
|
|
103
|
+
interface LazyLogValue {
|
|
104
|
+
/** Produce the value for `level` — invoked only when the line will actually be logged. */
|
|
105
|
+
readonly [LOG_MAYBE]: (level: Level) => MaybePromise<unknown>;
|
|
106
|
+
/** Best-effort **synchronous** rendering for the non-intercepted path (JSON / `console.log`). */
|
|
107
|
+
toJSON(): unknown;
|
|
108
|
+
}
|
|
109
|
+
/** True for a thenable (a `Promise` or any `{ then(): … }`). */
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Defer an expensive log argument. The function runs **only if** the line will actually be
|
|
113
|
+
* logged (its level passes the threshold): under console interception the structured pipeline
|
|
114
|
+
* calls it (with the record's level) and awaits the result, treating it as a normal argument.
|
|
115
|
+
* On the non-intercepted path `toJSON` (and Node's inspect) render the synchronous value, or
|
|
116
|
+
* `'(unresolved value)'` when the function returns a promise that can't be awaited there.
|
|
117
|
+
*
|
|
118
|
+
* ```ts
|
|
119
|
+
* log.debug('state', logMaybe(() => expensiveSnapshot())) // snapshot built only at debug level
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
declare function logMaybe(fn: (level: Level) => MaybePromise<unknown>): LazyLogValue;
|
|
123
|
+
/** Options for {@link createSanitizer} (and `LoggerConfig.sanitize`). */
|
|
124
|
+
interface SanitizeOptions {
|
|
125
|
+
/** Decide whether a built record is logged at all — return `false` to drop it. Runs first. */
|
|
126
|
+
readonly filter?: (record: LogRecord) => boolean;
|
|
127
|
+
/** Property names whose values are masked — a `string` (case-insensitive exact match) or a `RegExp`. Matched at any depth. */
|
|
128
|
+
readonly sensitiveKeys?: readonly (string | RegExp)[];
|
|
129
|
+
/** String **values** are masked when they match — a `string` (case-insensitive substring) or a `RegExp`. */
|
|
130
|
+
readonly sensitiveValues?: readonly (string | RegExp)[];
|
|
131
|
+
/** Turn a sensitive value into its masked form (default `() => '[redacted]'`; see {@link partialMask}). */
|
|
132
|
+
readonly mask?: (value: unknown, key?: string) => unknown;
|
|
133
|
+
/** Truncate any string longer than this many characters, appending `'... (+N more)'`. */
|
|
134
|
+
readonly maxStringLength?: number;
|
|
135
|
+
/** Truncate a **homogeneous** array (all elements the same kind) beyond this many, appending a `'(+N more)'` element. */
|
|
136
|
+
readonly maxArrayLength?: number;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A {@link SanitizeOptions.mask} that keeps the first `keep` characters and replaces the rest
|
|
140
|
+
* with `fill` (e.g. `partialMask(3)('secret-token') === 'sec***'`). Values no longer than
|
|
141
|
+
* `keep` are fully masked, so nothing short leaks. With the default `keep` of 0 it masks fully.
|
|
142
|
+
*/
|
|
143
|
+
declare function partialMask(keep?: number, fill?: string): (value: unknown) => string;
|
|
144
|
+
/**
|
|
145
|
+
* Build a record transformer that redacts sensitive keys/values and truncates long
|
|
146
|
+
* strings/arrays per {@link SanitizeOptions}. Returns the (new) record, or `null` to drop it
|
|
147
|
+
* (the `filter` returned `false`) — the same shape as `LoggerConfig.filter`, so it composes
|
|
148
|
+
* there too. Only plain objects and arrays are walked; `Date`/class instances/{@link logMaybe}
|
|
149
|
+
* markers pass through untouched, and the reserved `tms`/`level` fields are left pristine.
|
|
150
|
+
*/
|
|
151
|
+
declare function createSanitizer(opts: SanitizeOptions): (record: LogRecord) => LogRecord | null;
|
|
152
|
+
//#endregion
|
|
153
|
+
export { partialMask as _, Level as a, SanitizeOptions as c, Transport as d, createSanitizer as f, merge as g, logMaybe as h, LazyLogValue as i, SerializedError as l, getContext as m, ErrorConfig as n, LogRecord as o, deepEqual as p, LOG_CONTEXT as r, MaybePromise as s, ErrorCaptureConfig as t, Serializer as u, resolveLogValue as v, serializeError as y };
|