@amkisko/json-logging 0.1.0 → 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 +12 -5
- package/dist/console-bridge.d.ts +6 -0
- package/dist/console-bridge.js +3 -0
- package/dist/console-state.d.ts +8 -0
- package/dist/console-state.js +6 -0
- package/dist/context.d.ts +10 -2
- package/dist/context.js +15 -1
- package/dist/http-ambient.d.ts +2 -0
- package/dist/http-ambient.js +2 -0
- package/dist/http-context.d.ts +16 -0
- package/dist/http-context.js +9 -0
- 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/next.d.ts +1 -0
- package/dist/next.js +1 -0
- package/dist/redact.d.ts +16 -2
- package/dist/redact.js +131 -20
- package/dist/sanitize.d.ts +5 -0
- package/dist/sanitize.js +5 -0
- package/dist/serialize.d.ts +7 -2
- package/dist/serialize.js +16 -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,10 +11,10 @@ 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
|
-
On the JSR package settings page, mark Node.js, Bun, Deno, Cloudflare Workers, and Browsers as compatible.
|
|
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).
|
|
18
18
|
|
|
19
19
|
## Install
|
|
20
20
|
|
|
@@ -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/console-bridge.d.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { type ConsoleMethodName, type ConsoleMethods } from "./console-state.js";
|
|
2
2
|
import type { Logger } from "./logger.js";
|
|
3
3
|
import type { LoggerOptions, LogMessage } from "./types.js";
|
|
4
|
+
/** Options for {@link installConsoleBridge}. */
|
|
4
5
|
export type ConsoleBridgeOptions = {
|
|
6
|
+
/** Which console methods to patch. Default: log, info, warn, error, debug. */
|
|
5
7
|
methods?: readonly ConsoleMethodName[];
|
|
6
8
|
};
|
|
9
|
+
/** Result of {@link installStructuredLogging}: logger plus uninstall handle. */
|
|
7
10
|
export type StructuredLoggingInstall = {
|
|
8
11
|
logger: Logger;
|
|
9
12
|
uninstall: () => void;
|
|
10
13
|
};
|
|
14
|
+
/** True when console methods are currently routed through a logger. */
|
|
11
15
|
export declare function isConsoleBridgeInstalled(): boolean;
|
|
16
|
+
/** Restore original console methods if a bridge was installed. */
|
|
12
17
|
export declare function uninstallConsoleBridge(): void;
|
|
13
18
|
/**
|
|
14
19
|
* Routes console.log/info/warn/error/debug through `logger` as structured JSON.
|
|
@@ -21,6 +26,7 @@ export declare function installConsoleBridge(logger: Logger, options?: ConsoleBr
|
|
|
21
26
|
* Prefer this when the goal is "app + console.* become structured JSON lines".
|
|
22
27
|
*/
|
|
23
28
|
export declare function installStructuredLogging(options?: LoggerOptions & ConsoleBridgeOptions): StructuredLoggingInstall;
|
|
29
|
+
/** Map `console.*` arguments into a {@link LogMessage} for structured emit. */
|
|
24
30
|
export declare function messageFromConsoleArgs(args: unknown[]): LogMessage;
|
|
25
31
|
export type { ConsoleMethods, ConsoleMethodName };
|
|
26
32
|
//# sourceMappingURL=console-bridge.d.ts.map
|
package/dist/console-bridge.js
CHANGED
|
@@ -11,9 +11,11 @@ const LEVEL_BY_METHOD = {
|
|
|
11
11
|
debug: "debug",
|
|
12
12
|
};
|
|
13
13
|
let installedMethods = null;
|
|
14
|
+
/** True when console methods are currently routed through a logger. */
|
|
14
15
|
export function isConsoleBridgeInstalled() {
|
|
15
16
|
return getBridgeOriginals() !== null;
|
|
16
17
|
}
|
|
18
|
+
/** Restore original console methods if a bridge was installed. */
|
|
17
19
|
export function uninstallConsoleBridge() {
|
|
18
20
|
const originals = getBridgeOriginals();
|
|
19
21
|
if (!originals || !installedMethods) {
|
|
@@ -73,6 +75,7 @@ export function installStructuredLogging(options = {}) {
|
|
|
73
75
|
const uninstall = installConsoleBridge(logger, methods ? { methods } : {});
|
|
74
76
|
return { logger, uninstall };
|
|
75
77
|
}
|
|
78
|
+
/** Map `console.*` arguments into a {@link LogMessage} for structured emit. */
|
|
76
79
|
export function messageFromConsoleArgs(args) {
|
|
77
80
|
if (args.length === 0) {
|
|
78
81
|
return "";
|
package/dist/console-state.d.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
/** Console method names the bridge may patch. */
|
|
1
2
|
export type ConsoleMethodName = "log" | "info" | "warn" | "error" | "debug";
|
|
3
|
+
/** Bound console method bag used by sinks and the bridge. */
|
|
2
4
|
export type ConsoleMethods = {
|
|
3
5
|
[K in ConsoleMethodName]: (...args: unknown[]) => void;
|
|
4
6
|
};
|
|
7
|
+
/** Store or clear the pre-patch console methods while the bridge is installed. */
|
|
5
8
|
export declare function setBridgeOriginals(methods: ConsoleMethods | null): void;
|
|
9
|
+
/** Pre-patch console methods, or null when the bridge is not installed. */
|
|
6
10
|
export declare function getBridgeOriginals(): ConsoleMethods | null;
|
|
11
|
+
/** Enter a re-entrancy guard so bridge writes use original console methods. */
|
|
7
12
|
export declare function enterBridge(): void;
|
|
13
|
+
/** Leave the re-entrancy guard opened by {@link enterBridge}. */
|
|
8
14
|
export declare function leaveBridge(): void;
|
|
15
|
+
/** True while a bridged console write is in progress. */
|
|
9
16
|
export declare function isInBridge(): boolean;
|
|
17
|
+
/** Snapshot console methods from `target` (defaults to global `console`). */
|
|
10
18
|
export declare function captureConsoleMethods(target?: Partial<ConsoleMethods> & object): ConsoleMethods;
|
|
11
19
|
//# sourceMappingURL=console-state.d.ts.map
|
package/dist/console-state.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
let bridgeOriginals = null;
|
|
2
2
|
let bridgeDepth = 0;
|
|
3
|
+
/** Store or clear the pre-patch console methods while the bridge is installed. */
|
|
3
4
|
export function setBridgeOriginals(methods) {
|
|
4
5
|
bridgeOriginals = methods;
|
|
5
6
|
}
|
|
7
|
+
/** Pre-patch console methods, or null when the bridge is not installed. */
|
|
6
8
|
export function getBridgeOriginals() {
|
|
7
9
|
return bridgeOriginals;
|
|
8
10
|
}
|
|
11
|
+
/** Enter a re-entrancy guard so bridge writes use original console methods. */
|
|
9
12
|
export function enterBridge() {
|
|
10
13
|
bridgeDepth += 1;
|
|
11
14
|
}
|
|
15
|
+
/** Leave the re-entrancy guard opened by {@link enterBridge}. */
|
|
12
16
|
export function leaveBridge() {
|
|
13
17
|
if (bridgeDepth > 0) {
|
|
14
18
|
bridgeDepth -= 1;
|
|
15
19
|
}
|
|
16
20
|
}
|
|
21
|
+
/** True while a bridged console write is in progress. */
|
|
17
22
|
export function isInBridge() {
|
|
18
23
|
return bridgeDepth > 0;
|
|
19
24
|
}
|
|
25
|
+
/** Snapshot console methods from `target` (defaults to global `console`). */
|
|
20
26
|
export function captureConsoleMethods(target = console) {
|
|
21
27
|
return {
|
|
22
28
|
log: bindMethod(target, "log"),
|
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/http-ambient.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Logger } from "./logger.js";
|
|
2
|
+
/** Set the process-wide ambient logger used by HTTP adapters. */
|
|
2
3
|
export declare function bindHttpLogger(logger: Logger): void;
|
|
4
|
+
/** Current ambient HTTP logger, if {@link bindHttpLogger} was called. */
|
|
3
5
|
export declare function getAmbientHttpLogger(): Logger | undefined;
|
|
4
6
|
/** Bind options.logger once when building an adapter factory. */
|
|
5
7
|
export declare function bindLoggerFromOptions(options?: {
|
package/dist/http-ambient.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
let ambientLogger;
|
|
2
|
+
/** Set the process-wide ambient logger used by HTTP adapters. */
|
|
2
3
|
export function bindHttpLogger(logger) {
|
|
3
4
|
ambientLogger = logger;
|
|
4
5
|
}
|
|
6
|
+
/** Current ambient HTTP logger, if {@link bindHttpLogger} was called. */
|
|
5
7
|
export function getAmbientHttpLogger() {
|
|
6
8
|
return ambientLogger;
|
|
7
9
|
}
|
package/dist/http-context.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Logger } from "./logger.js";
|
|
2
2
|
import type { LoggerOptions } from "./types.js";
|
|
3
|
+
/** Headers-like object with a `get` method (Fetch `Headers`, etc.). */
|
|
3
4
|
export type HeaderBag = {
|
|
4
5
|
get(name: string): string | null;
|
|
5
6
|
};
|
|
7
|
+
/** Request fields used to build ambient HTTP context. */
|
|
6
8
|
export type HttpRequestInput = {
|
|
7
9
|
method?: string;
|
|
8
10
|
url?: string;
|
|
@@ -10,9 +12,13 @@ export type HttpRequestInput = {
|
|
|
10
12
|
headers?: HeaderBag | Record<string, string | string[] | undefined>;
|
|
11
13
|
requestId?: string;
|
|
12
14
|
};
|
|
15
|
+
/** Options for {@link withHttpContext} and framework adapters. */
|
|
13
16
|
export type WithHttpContextOptions = {
|
|
17
|
+
/** Header name for inbound request id. Default `x-request-id`. */
|
|
14
18
|
headerName?: string;
|
|
19
|
+
/** Explicit logger; otherwise ambient or a new logger from loggerOptions. */
|
|
15
20
|
logger?: Logger;
|
|
21
|
+
/** Used when creating a logger if none is bound. */
|
|
16
22
|
loggerOptions?: LoggerOptions;
|
|
17
23
|
/** Optional allowlisted fields merged under http fields (http fields win). */
|
|
18
24
|
extra?: Record<string, unknown>;
|
|
@@ -24,6 +30,7 @@ export type WithHttpContextOptions = {
|
|
|
24
30
|
/** When true, adapters emit one completion line after the request. */
|
|
25
31
|
logCompletion?: boolean;
|
|
26
32
|
};
|
|
33
|
+
/** Fields for {@link logRequestCompletion}. */
|
|
27
34
|
export type RequestCompletionFields = {
|
|
28
35
|
method?: string;
|
|
29
36
|
path?: string;
|
|
@@ -33,17 +40,26 @@ export type RequestCompletionFields = {
|
|
|
33
40
|
};
|
|
34
41
|
/** Accept inbound request ids that are short and correlation-safe. */
|
|
35
42
|
export declare const REQUEST_ID_PATTERN: RegExp;
|
|
43
|
+
/** Monotonic fallback request id when crypto UUID is unavailable. */
|
|
36
44
|
export declare function fallbackRequestId(now?: () => number): string;
|
|
45
|
+
/** Prefer `crypto.randomUUID`, else {@link fallbackRequestId}. */
|
|
37
46
|
export declare function defaultRandomId(randomUUID?: (() => string) | null): string;
|
|
47
|
+
/** True when `value` matches {@link REQUEST_ID_PATTERN}. */
|
|
38
48
|
export declare function isValidRequestId(value: string): boolean;
|
|
49
|
+
/** Keep a valid inbound id, otherwise allocate a new one. */
|
|
39
50
|
export declare function allocateRequestId(existing: string | null | undefined, randomId?: () => string): string;
|
|
51
|
+
/** Pathname from an absolute or relative URL string. */
|
|
40
52
|
export declare function requestPathFromUrl(url: string): string | undefined;
|
|
53
|
+
/** Pick named keys from a bag; missing keys are omitted. */
|
|
41
54
|
export declare function pickContextKeys(bag: Record<string, unknown> | undefined, keys: readonly string[] | undefined): Record<string, unknown>;
|
|
55
|
+
/** Build requestId / method / path fields from {@link HttpRequestInput}. */
|
|
42
56
|
export declare function httpFieldsFrom(input: HttpRequestInput, headerName?: string): Record<string, unknown>;
|
|
57
|
+
/** Copy allowlisted header names into a sanitized string map. */
|
|
43
58
|
export declare function pickAllowlistedHeaders(headers: HeaderBag | Record<string, string | string[] | undefined> | undefined, names: readonly string[] | undefined): Record<string, unknown>;
|
|
44
59
|
/**
|
|
45
60
|
* One structured access line. Level is error when status >= 500 or error is set.
|
|
46
61
|
*/
|
|
47
62
|
export declare function logRequestCompletion(logger: Logger, fields: RequestCompletionFields): void;
|
|
63
|
+
/** Run `fn` under ALS context built from the HTTP request input. */
|
|
48
64
|
export declare function withHttpContext<T>(input: HttpRequestInput, fn: () => T | Promise<T>, options?: WithHttpContextOptions): T | Promise<T>;
|
|
49
65
|
//# sourceMappingURL=http-context.d.ts.map
|
package/dist/http-context.js
CHANGED
|
@@ -3,10 +3,12 @@ import { sanitizeError, sanitizeString } from "./sanitize.js";
|
|
|
3
3
|
/** Accept inbound request ids that are short and correlation-safe. */
|
|
4
4
|
export const REQUEST_ID_PATTERN = /^[\w.\-:]{1,128}$/;
|
|
5
5
|
let fallbackCounter = 0;
|
|
6
|
+
/** Monotonic fallback request id when crypto UUID is unavailable. */
|
|
6
7
|
export function fallbackRequestId(now = Date.now) {
|
|
7
8
|
fallbackCounter += 1;
|
|
8
9
|
return `req_${now().toString(36)}_${fallbackCounter.toString(36)}`;
|
|
9
10
|
}
|
|
11
|
+
/** Prefer `crypto.randomUUID`, else {@link fallbackRequestId}. */
|
|
10
12
|
export function defaultRandomId(randomUUID) {
|
|
11
13
|
const resolved = randomUUID === undefined ? globalThis.crypto?.randomUUID?.bind(globalThis.crypto) : randomUUID;
|
|
12
14
|
if (typeof resolved === "function") {
|
|
@@ -14,9 +16,11 @@ export function defaultRandomId(randomUUID) {
|
|
|
14
16
|
}
|
|
15
17
|
return fallbackRequestId();
|
|
16
18
|
}
|
|
19
|
+
/** True when `value` matches {@link REQUEST_ID_PATTERN}. */
|
|
17
20
|
export function isValidRequestId(value) {
|
|
18
21
|
return REQUEST_ID_PATTERN.test(value);
|
|
19
22
|
}
|
|
23
|
+
/** Keep a valid inbound id, otherwise allocate a new one. */
|
|
20
24
|
export function allocateRequestId(existing, randomId = defaultRandomId) {
|
|
21
25
|
if (existing && isValidRequestId(existing)) {
|
|
22
26
|
return existing;
|
|
@@ -28,6 +32,7 @@ export function allocateRequestId(existing, randomId = defaultRandomId) {
|
|
|
28
32
|
return fallbackRequestId();
|
|
29
33
|
}
|
|
30
34
|
}
|
|
35
|
+
/** Pathname from an absolute or relative URL string. */
|
|
31
36
|
export function requestPathFromUrl(url) {
|
|
32
37
|
if (url.length === 0) {
|
|
33
38
|
return undefined;
|
|
@@ -66,6 +71,7 @@ function headerGet(headers, name) {
|
|
|
66
71
|
}
|
|
67
72
|
return typeof direct === "string" ? direct : null;
|
|
68
73
|
}
|
|
74
|
+
/** Pick named keys from a bag; missing keys are omitted. */
|
|
69
75
|
export function pickContextKeys(bag, keys) {
|
|
70
76
|
if (!bag || !keys || keys.length === 0) {
|
|
71
77
|
return {};
|
|
@@ -78,6 +84,7 @@ export function pickContextKeys(bag, keys) {
|
|
|
78
84
|
}
|
|
79
85
|
return out;
|
|
80
86
|
}
|
|
87
|
+
/** Build requestId / method / path fields from {@link HttpRequestInput}. */
|
|
81
88
|
export function httpFieldsFrom(input, headerName = "x-request-id") {
|
|
82
89
|
const fromHeader = headerGet(input.headers, headerName);
|
|
83
90
|
const requestId = allocateRequestId(input.requestId ?? fromHeader);
|
|
@@ -91,6 +98,7 @@ export function httpFieldsFrom(input, headerName = "x-request-id") {
|
|
|
91
98
|
}
|
|
92
99
|
return fields;
|
|
93
100
|
}
|
|
101
|
+
/** Copy allowlisted header names into a sanitized string map. */
|
|
94
102
|
export function pickAllowlistedHeaders(headers, names) {
|
|
95
103
|
if (!headers || !names || names.length === 0) {
|
|
96
104
|
return {};
|
|
@@ -135,6 +143,7 @@ export function logRequestCompletion(logger, fields) {
|
|
|
135
143
|
logger.info(payload);
|
|
136
144
|
}
|
|
137
145
|
}
|
|
146
|
+
/** Run `fn` under ALS context built from the HTTP request input. */
|
|
138
147
|
export function withHttpContext(input, fn, options = {}) {
|
|
139
148
|
const headerName = options.headerName ?? "x-request-id";
|
|
140
149
|
const fields = {
|
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/next.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export type WithRequestLoggingOptions = {
|
|
|
31
31
|
logger?: Logger;
|
|
32
32
|
loggerOptions?: LoggerOptions;
|
|
33
33
|
};
|
|
34
|
+
/** Re-export request-id helpers for Next entrypoint consumers. */
|
|
34
35
|
export { allocateRequestId, defaultRandomId, fallbackRequestId };
|
|
35
36
|
/**
|
|
36
37
|
* Wrap a Next.js middleware or route handler with request-scoped context.
|
package/dist/next.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { bindLoggerFromOptions } from "./http-ambient.js";
|
|
17
17
|
import { allocateRequestId, defaultRandomId, fallbackRequestId, withHttpContext, } from "./http-context.js";
|
|
18
|
+
/** Re-export request-id helpers for Next entrypoint consumers. */
|
|
18
19
|
export { allocateRequestId, defaultRandomId, fallbackRequestId };
|
|
19
20
|
/**
|
|
20
21
|
* Wrap a Next.js middleware or route handler with request-scoped context.
|
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/sanitize.d.ts
CHANGED
|
@@ -9,12 +9,17 @@ export declare const MAX_DEPTH = 10;
|
|
|
9
9
|
export declare const MAX_BACKTRACE_LINES = 20;
|
|
10
10
|
/** Case-insensitive sensitive key names (password, secret, token, authorization, api_key, ...). */
|
|
11
11
|
export declare const SENSITIVE_KEY_PATTERN: RegExp;
|
|
12
|
+
/** True when a key name matches {@link SENSITIVE_KEY_PATTERN}. */
|
|
12
13
|
export declare function isSensitiveKey(key: string): boolean;
|
|
14
|
+
/** Strip control characters and truncate to {@link MAX_STRING_LENGTH}. */
|
|
13
15
|
export declare function sanitizeString(value: string): string;
|
|
16
|
+
/** Coerce tags to non-empty sanitized strings, dropping nullish entries. */
|
|
14
17
|
export declare function prepareTags(tags: readonly unknown[]): string[];
|
|
18
|
+
/** Shape an Error under an `error` key for log payloads (name, message, stack, cause). */
|
|
15
19
|
export declare function sanitizeError(error: Error): {
|
|
16
20
|
[key: string]: JsonValue;
|
|
17
21
|
};
|
|
18
22
|
export declare function sanitizeValue(value: unknown, depth: number, seen: WeakSet<object>): JsonValue;
|
|
23
|
+
/** Sanitize a plain object: filter sensitive keys, bound depth/size, mark cycles. */
|
|
19
24
|
export declare function sanitizeHash(hash: Record<string, unknown>, depth?: number): Record<string, unknown>;
|
|
20
25
|
//# sourceMappingURL=sanitize.d.ts.map
|
package/dist/sanitize.js
CHANGED
|
@@ -12,9 +12,11 @@ const CONTROL_CHARS = /[\x00-\x1F\x7F]/g;
|
|
|
12
12
|
export const SENSITIVE_KEY_PATTERN = /(passw|pwd|secret|token|authorization|auth|api[_-]?key|_key$|crypt|salt|certificate|otp|ssn|cvv|cvc|credential)/i;
|
|
13
13
|
const FILTERED = "[FILTERED]";
|
|
14
14
|
const CIRCULAR = "[Circular]";
|
|
15
|
+
/** True when a key name matches {@link SENSITIVE_KEY_PATTERN}. */
|
|
15
16
|
export function isSensitiveKey(key) {
|
|
16
17
|
return SENSITIVE_KEY_PATTERN.test(key);
|
|
17
18
|
}
|
|
19
|
+
/** Strip control characters and truncate to {@link MAX_STRING_LENGTH}. */
|
|
18
20
|
export function sanitizeString(value) {
|
|
19
21
|
try {
|
|
20
22
|
let result = value.replace(CONTROL_CHARS, "");
|
|
@@ -27,6 +29,7 @@ export function sanitizeString(value) {
|
|
|
27
29
|
return "<sanitization_error>";
|
|
28
30
|
}
|
|
29
31
|
}
|
|
32
|
+
/** Coerce tags to non-empty sanitized strings, dropping nullish entries. */
|
|
30
33
|
export function prepareTags(tags) {
|
|
31
34
|
const prepared = [];
|
|
32
35
|
for (const tag of tags) {
|
|
@@ -40,6 +43,7 @@ export function prepareTags(tags) {
|
|
|
40
43
|
}
|
|
41
44
|
return prepared;
|
|
42
45
|
}
|
|
46
|
+
/** Shape an Error under an `error` key for log payloads (name, message, stack, cause). */
|
|
43
47
|
export function sanitizeError(error) {
|
|
44
48
|
try {
|
|
45
49
|
const body = {
|
|
@@ -134,6 +138,7 @@ function sanitizeObject(value, depth, seen) {
|
|
|
134
138
|
}
|
|
135
139
|
return result;
|
|
136
140
|
}
|
|
141
|
+
/** Sanitize a plain object: filter sensitive keys, bound depth/size, mark cycles. */
|
|
137
142
|
export function sanitizeHash(hash, depth = 0) {
|
|
138
143
|
try {
|
|
139
144
|
if (depth > MAX_DEPTH) {
|
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";
|
|
@@ -6,6 +6,7 @@ export { currentTimestamp, defaultPerformance, formatTimestamp, } from "./timest
|
|
|
6
6
|
export declare const SYSTEM_KEYS: ReadonlySet<string>;
|
|
7
7
|
/** Normalize a log message into root fields (string, object, or Error). */
|
|
8
8
|
export declare function parseMessage(message: LogMessage): Record<string, unknown>;
|
|
9
|
+
/** Inputs for {@link buildRecord} on the log hot path. */
|
|
9
10
|
export type BuildLineInput = {
|
|
10
11
|
message: LogMessage;
|
|
11
12
|
level: Level;
|
|
@@ -17,11 +18,15 @@ export type BuildLineInput = {
|
|
|
17
18
|
preparedTags?: boolean;
|
|
18
19
|
clock?: () => Date;
|
|
19
20
|
redact?: readonly string[];
|
|
20
|
-
|
|
21
|
+
/** Precompiled path list (RFC-0002) or full plan (RFC-0005). */
|
|
22
|
+
compiledRedact?: readonly CompiledRedactPath[] | CompiledRedact;
|
|
21
23
|
serializers?: Readonly<Record<string, FieldSerializer>>;
|
|
22
24
|
contextProvider?: ContextProvider;
|
|
23
25
|
};
|
|
26
|
+
/** Assemble one log record: message, severity, tags, context, serializers, redact. */
|
|
24
27
|
export declare function buildRecord(input: BuildLineInput): LogRecord;
|
|
28
|
+
/** Serialize a record to one JSON line with a trailing newline. */
|
|
25
29
|
export declare function toJsonLine(record: LogRecord): string;
|
|
30
|
+
/** Never-raise fallback record when the primary emit path throws. */
|
|
26
31
|
export declare function buildFallbackRecord(level: Level, message: unknown, error: unknown, clock?: () => Date): LogRecord;
|
|
27
32
|
//# sourceMappingURL=serialize.d.ts.map
|
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)) {
|
|
@@ -82,6 +85,7 @@ function mergeProviderContext(payload, provider) {
|
|
|
82
85
|
...readObjectField(payload, "context"),
|
|
83
86
|
};
|
|
84
87
|
}
|
|
88
|
+
/** Assemble one log record: message, severity, tags, context, serializers, redact. */
|
|
85
89
|
export function buildRecord(input) {
|
|
86
90
|
const payload = {};
|
|
87
91
|
if (input.base && Object.keys(input.base).length > 0) {
|
|
@@ -145,9 +149,15 @@ export function buildRecord(input) {
|
|
|
145
149
|
applySerializers(payload, input.serializers);
|
|
146
150
|
}
|
|
147
151
|
}
|
|
148
|
-
if (input.compiledRedact
|
|
149
|
-
const redacted =
|
|
150
|
-
|
|
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)) {
|
|
151
161
|
for (const key of Object.keys(payload)) {
|
|
152
162
|
delete payload[key];
|
|
153
163
|
}
|
|
@@ -176,11 +186,13 @@ function uniqueTags(tags) {
|
|
|
176
186
|
}
|
|
177
187
|
return result;
|
|
178
188
|
}
|
|
189
|
+
/** Serialize a record to one JSON line with a trailing newline. */
|
|
179
190
|
export function toJsonLine(record) {
|
|
180
191
|
const seen = new WeakSet();
|
|
181
192
|
const safe = sanitizeValue(record, 0, seen);
|
|
182
193
|
return `${JSON.stringify(safe)}\n`;
|
|
183
194
|
}
|
|
195
|
+
/** Never-raise fallback record when the primary emit path throws. */
|
|
184
196
|
export function buildFallbackRecord(level, message, error, clock) {
|
|
185
197
|
const err = error instanceof Error ? error : new Error(typeof error === "string" ? error : "unknown");
|
|
186
198
|
return {
|
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. */
|