@amkisko/json-logging 0.1.1 → 0.2.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/README.md +11 -4
- package/dist/context.d.ts +10 -2
- package/dist/context.js +15 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/logger.d.ts +2 -1
- package/dist/logger.js +21 -7
- package/dist/redact.d.ts +16 -2
- package/dist/redact.js +131 -20
- package/dist/serialize.d.ts +3 -2
- package/dist/serialize.js +13 -4
- package/dist/types.d.ts +24 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Structured JSON logging for Bun, Node, Deno, Next.js, and Remix. The portable API follows the Ruby `activesupport-json_logging` gem style: create a logger, call severity methods, use `tagged` and `withContext`, and emit one JSON object per line.
|
|
4
4
|
|
|
5
|
-
Zero runtime dependencies. Ambient `withContext` / block `tagged` use AsyncLocalStorage when the runtime exposes it through `process.getBuiltinModule("async_hooks")` (Node, Bun, Deno with Node builtins, Cloudflare Workers with `nodejs_compat`). The package never statically imports `node:async_hooks`, so browsers and Workers without Node builtins can still import and emit basic JSON lines; ambient context is a no-op there. Call `isAsyncLocalStorageAvailable()` to detect ALS. Helpers under `@amkisko/json-logging/next` and `@amkisko/json-logging/remix` use Web APIs and do not add framework packages as runtime dependencies.
|
|
5
|
+
Zero runtime dependencies. Ambient `withContext` / block `tagged` use AsyncLocalStorage when the runtime exposes it through `process.getBuiltinModule("async_hooks")` (Node, Bun, Deno with Node builtins, Cloudflare Workers with `nodejs_compat`). The package never statically imports `node:async_hooks`, so browsers and Workers without Node builtins can still import and emit basic JSON lines; ambient context is a no-op there. On Workers that import `AsyncLocalStorage` from `node:async_hooks` themselves, call `configureContextStorage(new AsyncLocalStorage())` once at boot. Call `isAsyncLocalStorageAvailable()` to detect ALS. Helpers under `@amkisko/json-logging/next` and `@amkisko/json-logging/remix` use Web APIs and do not add framework packages as runtime dependencies.
|
|
6
6
|
|
|
7
7
|
## Runtime compatibility
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@ Zero runtime dependencies. Ambient `withContext` / block `tagged` use AsyncLocal
|
|
|
11
11
|
| Node.js | Compatible | Primary target; ALS context works. |
|
|
12
12
|
| Bun | Compatible | Tested in CI; `@amkisko/json-logging/bun` for `Bun.serve`. |
|
|
13
13
|
| Deno | Compatible | ALS via Deno Node builtins (`getBuiltinModule`). |
|
|
14
|
-
| Cloudflare Workers | Compatible | Full ALS with `nodejs_compat
|
|
14
|
+
| Cloudflare Workers | Compatible | Full ALS with `nodejs_compat` (soft-load or `configureContextStorage`). Without it: import and basic logging still work; `withContext` does not retain fields unless you inject ALS. |
|
|
15
15
|
| Browsers | Compatible | Basic logging and sinks work. Ambient context requires a host that provides ALS (unusual in browsers). |
|
|
16
16
|
|
|
17
17
|
On the JSR package settings page, mark Node.js, Bun, Deno, Cloudflare Workers, and Browsers as compatible. Keep the package description aligned with `package.json` (`JSR_TOKEN=... ./usr/bin/sync-jsr-settings`, or set it in the Settings tab).
|
|
@@ -64,12 +64,17 @@ Each line looks like:
|
|
|
64
64
|
|
|
65
65
|
Object fields merge at the root. Tags appear as `tags`. Request fields appear under `context`. System keys (`tags`, `severity`, `timestamp`, `message`, `context`) are reserved.
|
|
66
66
|
|
|
67
|
-
Optional hot-path helpers (RFC-0002):
|
|
67
|
+
Optional hot-path helpers (RFC-0002, RFC-0005):
|
|
68
68
|
|
|
69
69
|
```ts
|
|
70
70
|
const logger = createLogger({
|
|
71
71
|
level: "info",
|
|
72
|
-
redact:
|
|
72
|
+
redact: {
|
|
73
|
+
paths: ["user.password", "headers.authorization"],
|
|
74
|
+
keys: ["ssn", "apiKey"],
|
|
75
|
+
caseInsensitive: true,
|
|
76
|
+
censor: "hash", // or "filtered" | "remove" | (value) => unknown
|
|
77
|
+
},
|
|
73
78
|
serializers: {
|
|
74
79
|
err: (value) => value, // root-field transform
|
|
75
80
|
},
|
|
@@ -80,6 +85,8 @@ if (logger.isLevelEnabled("debug")) {
|
|
|
80
85
|
}
|
|
81
86
|
```
|
|
82
87
|
|
|
88
|
+
Path-only form `redact: ["user.password"]` still applies `[FILTERED]`.
|
|
89
|
+
|
|
83
90
|
## Sinks
|
|
84
91
|
|
|
85
92
|
Console is the default. Pass a custom sink or a `WritableStream`:
|
package/dist/context.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/** Key/value fields stored in the ambient request context. */
|
|
2
2
|
export type ContextFields = Record<string, unknown>;
|
|
3
|
-
|
|
3
|
+
/** ALS store shape for {@link configureContextStorage} and ambient helpers. */
|
|
4
|
+
export type ContextStore = {
|
|
4
5
|
context: ContextFields;
|
|
5
6
|
tags: string[];
|
|
6
7
|
};
|
|
8
|
+
type Store = ContextStore;
|
|
7
9
|
/** Minimal ALS surface used for request/block context (run + getStore only). */
|
|
8
10
|
export type AlsInstance<T> = {
|
|
9
11
|
run<R>(store: T, callback: () => R): R;
|
|
@@ -11,8 +13,14 @@ export type AlsInstance<T> = {
|
|
|
11
13
|
};
|
|
12
14
|
/** Constructor for {@link AlsInstance}, matching `node:async_hooks`.AsyncLocalStorage. */
|
|
13
15
|
export type AlsConstructor = new <T>() => AlsInstance<T>;
|
|
14
|
-
/** True when
|
|
16
|
+
/** True when ambient context can use AsyncLocalStorage (injected or soft-loaded). */
|
|
15
17
|
export declare function isAsyncLocalStorageAvailable(): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Inject an AsyncLocalStorage instance for ambient `withContext` / block `tagged`
|
|
20
|
+
* when soft-load via `process.getBuiltinModule` is unavailable (for example Workers
|
|
21
|
+
* that import `node:async_hooks` directly). Pass `null` to clear and restore soft-load.
|
|
22
|
+
*/
|
|
23
|
+
export declare function configureContextStorage(storage: AlsInstance<ContextStore> | null): void;
|
|
16
24
|
/** Test helper: force ALS constructor resolution (`null` = unavailable). */
|
|
17
25
|
export declare function setAlsConstructorForTests(value: AlsConstructor | null | undefined): void;
|
|
18
26
|
/** Test helper: pass null to simulate a runtime without AsyncLocalStorage. */
|
package/dist/context.js
CHANGED
|
@@ -50,10 +50,24 @@ function loadAlsConstructor() {
|
|
|
50
50
|
cachedConstructor = undefined;
|
|
51
51
|
return undefined;
|
|
52
52
|
}
|
|
53
|
-
/** True when
|
|
53
|
+
/** True when ambient context can use AsyncLocalStorage (injected or soft-loaded). */
|
|
54
54
|
export function isAsyncLocalStorageAvailable() {
|
|
55
|
+
if (alsOverride !== undefined && alsOverride !== null) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (alsOverride === null) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
55
61
|
return loadAlsConstructor() !== undefined;
|
|
56
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Inject an AsyncLocalStorage instance for ambient `withContext` / block `tagged`
|
|
65
|
+
* when soft-load via `process.getBuiltinModule` is unavailable (for example Workers
|
|
66
|
+
* that import `node:async_hooks` directly). Pass `null` to clear and restore soft-load.
|
|
67
|
+
*/
|
|
68
|
+
export function configureContextStorage(storage) {
|
|
69
|
+
alsOverride = storage === null ? undefined : storage;
|
|
70
|
+
}
|
|
57
71
|
/** Test helper: force ALS constructor resolution (`null` = unavailable). */
|
|
58
72
|
export function setAlsConstructorForTests(value) {
|
|
59
73
|
alsConstructorOverride = value;
|
package/dist/index.d.ts
CHANGED
|
@@ -15,17 +15,17 @@
|
|
|
15
15
|
*/
|
|
16
16
|
export type { ConsoleMethodName, ConsoleMethods } from "./console-state.js";
|
|
17
17
|
export { captureConsoleMethods, getBridgeOriginals, isInBridge, } from "./console-state.js";
|
|
18
|
-
export type { AlsConstructor, AlsInstance, ContextFields } from "./context.js";
|
|
19
|
-
export { getBlockTags, getContext, isAsyncLocalStorageAvailable, peekLogStore, withContext, } from "./context.js";
|
|
18
|
+
export type { AlsConstructor, AlsInstance, ContextFields, ContextStore } from "./context.js";
|
|
19
|
+
export { configureContextStorage, getBlockTags, getContext, isAsyncLocalStorageAvailable, peekLogStore, withContext, } from "./context.js";
|
|
20
20
|
export { createLogger, JsonLogging } from "./json-logging.js";
|
|
21
21
|
export { Logger } from "./logger.js";
|
|
22
|
-
export type { CompiledRedactPath } from "./redact.js";
|
|
23
|
-
export { applyCompiledRedactPaths, applyRedactPaths, compileRedactPaths, } from "./redact.js";
|
|
22
|
+
export type { CompiledRedact, CompiledRedactPath } from "./redact.js";
|
|
23
|
+
export { applyCompiledRedact, applyCompiledRedactPaths, applyRedactPaths, compileRedact, compileRedactPaths, hasRedactWork, } from "./redact.js";
|
|
24
24
|
export { isSensitiveKey, MAX_CONTEXT_SIZE, MAX_DEPTH, MAX_STRING_LENGTH, prepareTags, sanitizeError, sanitizeHash, sanitizeString, } from "./sanitize.js";
|
|
25
25
|
export { buildFallbackRecord, buildRecord, currentTimestamp, formatTimestamp, parseMessage, SYSTEM_KEYS, toJsonLine, } from "./serialize.js";
|
|
26
26
|
export { isEnabled, LEVEL_ORDER, SEVERITY_NAMES, severityName } from "./severity.js";
|
|
27
27
|
export { consoleSink, consoleSinkFrom, streamSink } from "./sinks.js";
|
|
28
28
|
export type { PerformanceLike } from "./timestamp.js";
|
|
29
29
|
export { defaultPerformance } from "./timestamp.js";
|
|
30
|
-
export type { ContextProvider, FieldSerializer, JsonPrimitive, JsonValue, Level, LoggerOptions, LogMessage, LogRecord, SeverityName, Sink, } from "./types.js";
|
|
30
|
+
export type { ContextProvider, FieldSerializer, JsonPrimitive, JsonValue, Level, LoggerOptions, LogMessage, LogRecord, RedactCensor, RedactOptions, SeverityName, Sink, } from "./types.js";
|
|
31
31
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* @module
|
|
15
15
|
*/
|
|
16
16
|
export { captureConsoleMethods, getBridgeOriginals, isInBridge, } from "./console-state.js";
|
|
17
|
-
export { getBlockTags, getContext, isAsyncLocalStorageAvailable, peekLogStore, withContext, } from "./context.js";
|
|
17
|
+
export { configureContextStorage, getBlockTags, getContext, isAsyncLocalStorageAvailable, peekLogStore, withContext, } from "./context.js";
|
|
18
18
|
export { createLogger, JsonLogging } from "./json-logging.js";
|
|
19
19
|
export { Logger } from "./logger.js";
|
|
20
|
-
export { applyCompiledRedactPaths, applyRedactPaths, compileRedactPaths, } from "./redact.js";
|
|
20
|
+
export { applyCompiledRedact, applyCompiledRedactPaths, applyRedactPaths, compileRedact, compileRedactPaths, hasRedactWork, } from "./redact.js";
|
|
21
21
|
export { isSensitiveKey, MAX_CONTEXT_SIZE, MAX_DEPTH, MAX_STRING_LENGTH, prepareTags, sanitizeError, sanitizeHash, sanitizeString, } from "./sanitize.js";
|
|
22
22
|
export { buildFallbackRecord, buildRecord, currentTimestamp, formatTimestamp, parseMessage, SYSTEM_KEYS, toJsonLine, } from "./serialize.js";
|
|
23
23
|
export { isEnabled, LEVEL_ORDER, SEVERITY_NAMES, severityName } from "./severity.js";
|
package/dist/logger.d.ts
CHANGED
|
@@ -11,12 +11,13 @@ export declare class Logger {
|
|
|
11
11
|
readonly clock?: () => Date;
|
|
12
12
|
/** Sticky tags from construction or {@link Logger.tagged}. */
|
|
13
13
|
readonly tags: readonly string[];
|
|
14
|
-
/** Configured redact paths (dot notation). */
|
|
14
|
+
/** Configured redact paths (dot notation); empty when only keys are set. */
|
|
15
15
|
readonly redact: readonly string[];
|
|
16
16
|
/** Root-field serializers. */
|
|
17
17
|
readonly serializers: Readonly<Record<string, FieldSerializer>>;
|
|
18
18
|
/** Optional ambient context provider. */
|
|
19
19
|
readonly contextProvider?: ContextProvider;
|
|
20
|
+
private readonly redactSource;
|
|
20
21
|
private readonly compiledRedact;
|
|
21
22
|
constructor(options?: LoggerOptions);
|
|
22
23
|
/** True when a message at `level` would be emitted. */
|
package/dist/logger.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { withBlockTags } from "./context.js";
|
|
2
|
-
import {
|
|
2
|
+
import { compileRedact, hasRedactWork } from "./redact.js";
|
|
3
3
|
import { prepareTags, sanitizeHash } from "./sanitize.js";
|
|
4
4
|
import { buildFallbackRecord, buildRecord, toJsonLine } from "./serialize.js";
|
|
5
5
|
import { isEnabled } from "./severity.js";
|
|
@@ -7,6 +7,18 @@ import { consoleSink } from "./sinks.js";
|
|
|
7
7
|
function isCallback(value) {
|
|
8
8
|
return typeof value === "function";
|
|
9
9
|
}
|
|
10
|
+
function isRedactOptions(value) {
|
|
11
|
+
return !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function redactPathsFrom(input) {
|
|
14
|
+
if (input === undefined) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
if (isRedactOptions(input)) {
|
|
18
|
+
return input.paths ? [...input.paths] : [];
|
|
19
|
+
}
|
|
20
|
+
return [...input];
|
|
21
|
+
}
|
|
10
22
|
/** Structured logger: severity methods, sticky tags, child loggers, never-raise emit path. */
|
|
11
23
|
export class Logger {
|
|
12
24
|
/** Minimum level that will be written. */
|
|
@@ -19,12 +31,13 @@ export class Logger {
|
|
|
19
31
|
clock;
|
|
20
32
|
/** Sticky tags from construction or {@link Logger.tagged}. */
|
|
21
33
|
tags;
|
|
22
|
-
/** Configured redact paths (dot notation). */
|
|
34
|
+
/** Configured redact paths (dot notation); empty when only keys are set. */
|
|
23
35
|
redact;
|
|
24
36
|
/** Root-field serializers. */
|
|
25
37
|
serializers;
|
|
26
38
|
/** Optional ambient context provider. */
|
|
27
39
|
contextProvider;
|
|
40
|
+
redactSource;
|
|
28
41
|
compiledRedact;
|
|
29
42
|
constructor(options = {}) {
|
|
30
43
|
this.level = options.level ?? "info";
|
|
@@ -32,10 +45,11 @@ export class Logger {
|
|
|
32
45
|
this.base = options.base ? sanitizeHash(options.base) : {};
|
|
33
46
|
this.clock = options.clock;
|
|
34
47
|
this.tags = options.tags ? prepareTags(options.tags) : [];
|
|
35
|
-
this.
|
|
48
|
+
this.redactSource = options.redact;
|
|
49
|
+
this.redact = redactPathsFrom(options.redact);
|
|
36
50
|
this.serializers = options.serializers ? { ...options.serializers } : {};
|
|
37
51
|
this.contextProvider = options.contextProvider;
|
|
38
|
-
this.compiledRedact =
|
|
52
|
+
this.compiledRedact = compileRedact(options.redact);
|
|
39
53
|
}
|
|
40
54
|
/** True when a message at `level` would be emitted. */
|
|
41
55
|
isLevelEnabled(level) {
|
|
@@ -68,7 +82,7 @@ export class Logger {
|
|
|
68
82
|
sink: this.sink,
|
|
69
83
|
clock: this.clock,
|
|
70
84
|
tags: this.tags,
|
|
71
|
-
redact: this.
|
|
85
|
+
redact: this.redactSource,
|
|
72
86
|
serializers: this.serializers,
|
|
73
87
|
contextProvider: this.contextProvider,
|
|
74
88
|
base: { ...this.base, ...sanitizeHash(bindings) },
|
|
@@ -89,7 +103,7 @@ export class Logger {
|
|
|
89
103
|
sink: this.sink,
|
|
90
104
|
clock: this.clock,
|
|
91
105
|
base: this.base,
|
|
92
|
-
redact: this.
|
|
106
|
+
redact: this.redactSource,
|
|
93
107
|
serializers: this.serializers,
|
|
94
108
|
contextProvider: this.contextProvider,
|
|
95
109
|
tags: [...this.tags, ...tags],
|
|
@@ -108,7 +122,7 @@ export class Logger {
|
|
|
108
122
|
preparedBase: true,
|
|
109
123
|
preparedTags: true,
|
|
110
124
|
clock: this.clock,
|
|
111
|
-
compiledRedact: this.compiledRedact,
|
|
125
|
+
compiledRedact: hasRedactWork(this.compiledRedact) ? this.compiledRedact : undefined,
|
|
112
126
|
serializers: this.serializers,
|
|
113
127
|
contextProvider: this.contextProvider,
|
|
114
128
|
});
|
package/dist/redact.d.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
import type { RedactOptions } from "./types.js";
|
|
1
2
|
/** Dot-path segments after {@link compileRedactPaths} (config-time). */
|
|
2
3
|
export type CompiledRedactPath = readonly string[];
|
|
4
|
+
/** Resolved redact plan used on the log hot path. */
|
|
5
|
+
export type CompiledRedact = {
|
|
6
|
+
paths: readonly CompiledRedactPath[];
|
|
7
|
+
keys: ReadonlySet<string>;
|
|
8
|
+
caseInsensitive: boolean;
|
|
9
|
+
replace: (value: unknown) => unknown;
|
|
10
|
+
};
|
|
3
11
|
/** Compile redact paths once at logger construction. Config-time only. */
|
|
4
12
|
export declare function compileRedactPaths(paths: readonly string[]): CompiledRedactPath[];
|
|
5
|
-
/**
|
|
13
|
+
/** Compile path and key redact options once at logger construction. */
|
|
14
|
+
export declare function compileRedact(input: readonly string[] | RedactOptions | undefined): CompiledRedact;
|
|
15
|
+
/** Apply a compiled redact plan (paths then keys). */
|
|
16
|
+
export declare function applyCompiledRedact(value: unknown, compiled: CompiledRedact): unknown;
|
|
17
|
+
/** Apply precompiled path list with default `[FILTERED]` censor. */
|
|
6
18
|
export declare function applyCompiledRedactPaths(value: unknown, paths: readonly CompiledRedactPath[]): unknown;
|
|
7
|
-
/** Apply string paths (compiles each call). Prefer
|
|
19
|
+
/** Apply string paths (compiles each call). Prefer compileRedact on hot paths. */
|
|
8
20
|
export declare function applyRedactPaths(value: unknown, paths: readonly string[]): unknown;
|
|
21
|
+
/** True when the compiled plan has any paths or keys. */
|
|
22
|
+
export declare function hasRedactWork(compiled: CompiledRedact): boolean;
|
|
9
23
|
//# sourceMappingURL=redact.d.ts.map
|
package/dist/redact.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { plainObjectFrom } from "./objects.js";
|
|
2
2
|
const FILTERED = "[FILTERED]";
|
|
3
|
+
const REMOVED = Symbol("json-logging-removed");
|
|
3
4
|
function splitPath(path) {
|
|
4
5
|
if (path.length === 0) {
|
|
5
6
|
return undefined;
|
|
@@ -21,57 +22,167 @@ export function compileRedactPaths(paths) {
|
|
|
21
22
|
}
|
|
22
23
|
return compiled;
|
|
23
24
|
}
|
|
24
|
-
function
|
|
25
|
+
function fnv1a32Hex(text) {
|
|
26
|
+
let hash = 0x811c9dc5;
|
|
27
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
28
|
+
hash ^= text.charCodeAt(i);
|
|
29
|
+
hash = Math.imul(hash, 0x01000193);
|
|
30
|
+
}
|
|
31
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
32
|
+
}
|
|
33
|
+
function valueFingerprint(value) {
|
|
34
|
+
if (typeof value === "string") {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
return JSON.stringify(value) ?? String(value);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return String(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function resolveReplace(censor, hashLabel) {
|
|
45
|
+
if (censor === "remove") {
|
|
46
|
+
return () => REMOVED;
|
|
47
|
+
}
|
|
48
|
+
if (censor === "hash") {
|
|
49
|
+
const label = hashLabel.length > 0 ? hashLabel : "hash";
|
|
50
|
+
return (value) => `[${label}:${fnv1a32Hex(valueFingerprint(value))}]`;
|
|
51
|
+
}
|
|
52
|
+
if (typeof censor === "function") {
|
|
53
|
+
return censor;
|
|
54
|
+
}
|
|
55
|
+
return () => FILTERED;
|
|
56
|
+
}
|
|
57
|
+
function isRedactOptions(value) {
|
|
58
|
+
return !Array.isArray(value);
|
|
59
|
+
}
|
|
60
|
+
function normalizeOptions(input) {
|
|
61
|
+
if (input === undefined) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
if (isRedactOptions(input)) {
|
|
65
|
+
return input;
|
|
66
|
+
}
|
|
67
|
+
return { paths: input };
|
|
68
|
+
}
|
|
69
|
+
/** Compile path and key redact options once at logger construction. */
|
|
70
|
+
export function compileRedact(input) {
|
|
71
|
+
const options = normalizeOptions(input);
|
|
72
|
+
const caseInsensitive = options.caseInsensitive === true;
|
|
73
|
+
const keys = new Set();
|
|
74
|
+
for (const key of options.keys ?? []) {
|
|
75
|
+
if (key.length === 0) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
keys.add(caseInsensitive ? key.toLowerCase() : key);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
paths: compileRedactPaths(options.paths ?? []),
|
|
82
|
+
keys,
|
|
83
|
+
caseInsensitive,
|
|
84
|
+
replace: resolveReplace(options.censor, options.hashLabel ?? "hash"),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function keyMatches(name, compiled) {
|
|
88
|
+
return compiled.keys.has(compiled.caseInsensitive ? name.toLowerCase() : name);
|
|
89
|
+
}
|
|
90
|
+
function redactArray(value, parts, index, key, compiled) {
|
|
25
91
|
if (key === "*") {
|
|
26
|
-
return value.map((item) => redactAt(item, parts, index + 1));
|
|
92
|
+
return value.map((item) => redactAt(item, parts, index + 1, compiled));
|
|
27
93
|
}
|
|
28
94
|
const idx = Number(key);
|
|
29
95
|
if (!Number.isInteger(idx) || idx < 0 || idx >= value.length) {
|
|
30
96
|
return value;
|
|
31
97
|
}
|
|
32
98
|
const copy = value.slice();
|
|
33
|
-
|
|
99
|
+
const next = redactAt(copy[idx], parts, index + 1, compiled);
|
|
100
|
+
copy[idx] = next === REMOVED ? null : next;
|
|
34
101
|
return copy;
|
|
35
102
|
}
|
|
36
|
-
function redactObject(value, parts, index, key) {
|
|
103
|
+
function redactObject(value, parts, index, key, compiled) {
|
|
37
104
|
if (key === "*") {
|
|
38
105
|
const out = {};
|
|
39
106
|
for (const childKey of Object.keys(value)) {
|
|
40
|
-
|
|
107
|
+
const next = redactAt(value[childKey], parts, index + 1, compiled);
|
|
108
|
+
if (next !== REMOVED) {
|
|
109
|
+
out[childKey] = next;
|
|
110
|
+
}
|
|
41
111
|
}
|
|
42
112
|
return out;
|
|
43
113
|
}
|
|
44
114
|
if (!(key in value)) {
|
|
45
115
|
return value;
|
|
46
116
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
117
|
+
const next = redactAt(value[key], parts, index + 1, compiled);
|
|
118
|
+
if (next === REMOVED) {
|
|
119
|
+
const out = { ...value };
|
|
120
|
+
delete out[key];
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
return { ...value, [key]: next };
|
|
51
124
|
}
|
|
52
|
-
function redactAt(value, parts, index) {
|
|
125
|
+
function redactAt(value, parts, index, compiled) {
|
|
53
126
|
if (index >= parts.length) {
|
|
54
|
-
return
|
|
127
|
+
return compiled.replace(value);
|
|
55
128
|
}
|
|
56
129
|
if (typeof value !== "object" || value === null) {
|
|
57
130
|
return value;
|
|
58
131
|
}
|
|
59
132
|
const key = String(parts[index]);
|
|
60
133
|
if (Array.isArray(value)) {
|
|
61
|
-
return redactArray(value, parts, index, key);
|
|
134
|
+
return redactArray(value, parts, index, key, compiled);
|
|
62
135
|
}
|
|
63
|
-
return redactObject(plainObjectFrom(value), parts, index, key);
|
|
136
|
+
return redactObject(plainObjectFrom(value), parts, index, key, compiled);
|
|
64
137
|
}
|
|
65
|
-
|
|
66
|
-
|
|
138
|
+
function applyKeys(value, compiled) {
|
|
139
|
+
if (compiled.keys.size === 0) {
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
if (typeof value !== "object" || value === null) {
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(value)) {
|
|
146
|
+
const items = value;
|
|
147
|
+
return items.map((item) => applyKeys(item, compiled));
|
|
148
|
+
}
|
|
149
|
+
const source = plainObjectFrom(value);
|
|
150
|
+
const out = {};
|
|
151
|
+
for (const key of Object.keys(source)) {
|
|
152
|
+
if (keyMatches(key, compiled)) {
|
|
153
|
+
const replaced = compiled.replace(source[key]);
|
|
154
|
+
if (replaced !== REMOVED) {
|
|
155
|
+
out[key] = replaced;
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
out[key] = applyKeys(source[key], compiled);
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/** Apply a compiled redact plan (paths then keys). */
|
|
164
|
+
export function applyCompiledRedact(value, compiled) {
|
|
67
165
|
let current = value;
|
|
68
|
-
for (const parts of paths) {
|
|
69
|
-
current = redactAt(current, parts, 0);
|
|
166
|
+
for (const parts of compiled.paths) {
|
|
167
|
+
current = redactAt(current, parts, 0, compiled);
|
|
70
168
|
}
|
|
71
|
-
return current;
|
|
169
|
+
return applyKeys(current, compiled);
|
|
170
|
+
}
|
|
171
|
+
/** Apply precompiled path list with default `[FILTERED]` censor. */
|
|
172
|
+
export function applyCompiledRedactPaths(value, paths) {
|
|
173
|
+
return applyCompiledRedact(value, {
|
|
174
|
+
paths,
|
|
175
|
+
keys: new Set(),
|
|
176
|
+
caseInsensitive: false,
|
|
177
|
+
replace: () => FILTERED,
|
|
178
|
+
});
|
|
72
179
|
}
|
|
73
|
-
/** Apply string paths (compiles each call). Prefer
|
|
180
|
+
/** Apply string paths (compiles each call). Prefer compileRedact on hot paths. */
|
|
74
181
|
export function applyRedactPaths(value, paths) {
|
|
75
|
-
return
|
|
182
|
+
return applyCompiledRedact(value, compileRedact(paths));
|
|
183
|
+
}
|
|
184
|
+
/** True when the compiled plan has any paths or keys. */
|
|
185
|
+
export function hasRedactWork(compiled) {
|
|
186
|
+
return compiled.paths.length > 0 || compiled.keys.size > 0;
|
|
76
187
|
}
|
|
77
188
|
//# sourceMappingURL=redact.js.map
|
package/dist/serialize.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CompiledRedactPath } from "./redact.js";
|
|
1
|
+
import { type CompiledRedact, type CompiledRedactPath } from "./redact.js";
|
|
2
2
|
import type { ContextProvider, FieldSerializer, Level, LogMessage, LogRecord } from "./types.js";
|
|
3
3
|
export type { PerformanceLike } from "./timestamp.js";
|
|
4
4
|
export { currentTimestamp, defaultPerformance, formatTimestamp, } from "./timestamp.js";
|
|
@@ -18,7 +18,8 @@ export type BuildLineInput = {
|
|
|
18
18
|
preparedTags?: boolean;
|
|
19
19
|
clock?: () => Date;
|
|
20
20
|
redact?: readonly string[];
|
|
21
|
-
|
|
21
|
+
/** Precompiled path list (RFC-0002) or full plan (RFC-0005). */
|
|
22
|
+
compiledRedact?: readonly CompiledRedactPath[] | CompiledRedact;
|
|
22
23
|
serializers?: Readonly<Record<string, FieldSerializer>>;
|
|
23
24
|
contextProvider?: ContextProvider;
|
|
24
25
|
};
|
package/dist/serialize.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { peekLogStore } from "./context.js";
|
|
2
2
|
import { isPlainObject, readObjectField } from "./objects.js";
|
|
3
|
-
import { applyCompiledRedactPaths, applyRedactPaths } from "./redact.js";
|
|
3
|
+
import { applyCompiledRedact, applyCompiledRedactPaths, applyRedactPaths, hasRedactWork, } from "./redact.js";
|
|
4
4
|
import { prepareTags, sanitizeError, sanitizeHash, sanitizeString, sanitizeValue, } from "./sanitize.js";
|
|
5
5
|
import { severityName } from "./severity.js";
|
|
6
6
|
import { currentTimestamp } from "./timestamp.js";
|
|
@@ -48,6 +48,9 @@ function mergeUserFields(payload, fields, alreadySanitized = false) {
|
|
|
48
48
|
}
|
|
49
49
|
return payload;
|
|
50
50
|
}
|
|
51
|
+
function isCompiledRedactPlan(value) {
|
|
52
|
+
return !Array.isArray(value) && typeof value === "object" && value !== null && "replace" in value;
|
|
53
|
+
}
|
|
51
54
|
function applySerializers(payload, serializers) {
|
|
52
55
|
for (const key of Object.keys(serializers)) {
|
|
53
56
|
if (!Object.hasOwn(payload, key)) {
|
|
@@ -146,9 +149,15 @@ export function buildRecord(input) {
|
|
|
146
149
|
applySerializers(payload, input.serializers);
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
|
-
if (input.compiledRedact
|
|
150
|
-
const redacted =
|
|
151
|
-
|
|
152
|
+
if (input.compiledRedact) {
|
|
153
|
+
const redacted = isCompiledRedactPlan(input.compiledRedact)
|
|
154
|
+
? hasRedactWork(input.compiledRedact)
|
|
155
|
+
? applyCompiledRedact(payload, input.compiledRedact)
|
|
156
|
+
: payload
|
|
157
|
+
: input.compiledRedact.length > 0
|
|
158
|
+
? applyCompiledRedactPaths(payload, input.compiledRedact)
|
|
159
|
+
: payload;
|
|
160
|
+
if (redacted !== payload && isPlainObject(redacted)) {
|
|
152
161
|
for (const key of Object.keys(payload)) {
|
|
153
162
|
delete payload[key];
|
|
154
163
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -14,6 +14,25 @@ export type Sink = {
|
|
|
14
14
|
export type FieldSerializer = (value: unknown) => unknown;
|
|
15
15
|
/** Optional ambient enrichment at log time (e.g. trace ids). Must not throw; throws take never-raise fallback. */
|
|
16
16
|
export type ContextProvider = () => Record<string, unknown> | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* How matched redact leaves are replaced.
|
|
19
|
+
* `filtered` writes `[FILTERED]`; `remove` drops object keys (array slots become `null`);
|
|
20
|
+
* `hash` writes a non-cryptographic `[label:hex8]` correlation token.
|
|
21
|
+
*/
|
|
22
|
+
export type RedactCensor = "filtered" | "remove" | "hash" | ((value: unknown) => unknown);
|
|
23
|
+
/** Path and key redaction options (RFC-0005). Array form remains path-only with `filtered`. */
|
|
24
|
+
export type RedactOptions = {
|
|
25
|
+
/** Dot paths; segment `*` matches one key or index. */
|
|
26
|
+
paths?: readonly string[];
|
|
27
|
+
/** Object key names matched anywhere in the record tree. */
|
|
28
|
+
keys?: readonly string[];
|
|
29
|
+
/** When true, `keys` matching ignores case. Default false. */
|
|
30
|
+
caseInsensitive?: boolean;
|
|
31
|
+
/** Replacement mode. Default `filtered`. */
|
|
32
|
+
censor?: RedactCensor;
|
|
33
|
+
/** Label inside hash tokens. Default `hash`. */
|
|
34
|
+
hashLabel?: string;
|
|
35
|
+
};
|
|
17
36
|
/** Construction options for {@link Logger}. */
|
|
18
37
|
export type LoggerOptions = {
|
|
19
38
|
/** Minimum level to emit. Default `info`. */
|
|
@@ -26,8 +45,11 @@ export type LoggerOptions = {
|
|
|
26
45
|
clock?: () => Date;
|
|
27
46
|
/** Sticky tags applied to every line from this logger. */
|
|
28
47
|
tags?: readonly string[];
|
|
29
|
-
/**
|
|
30
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Dot paths (RFC-0002) or {@link RedactOptions} with keys and censor modes (RFC-0005).
|
|
50
|
+
* Config-time only. Segment `*` matches one key or index.
|
|
51
|
+
*/
|
|
52
|
+
redact?: readonly string[] | RedactOptions;
|
|
31
53
|
/** Root field name to serializer. */
|
|
32
54
|
serializers?: Readonly<Record<string, FieldSerializer>>;
|
|
33
55
|
/** Merged under `context` after ALS ambient context; call-site context wins. */
|