@amkisko/json-logging 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +231 -2
- package/dist/bun-serve.d.ts +30 -0
- package/dist/bun-serve.js +60 -0
- package/dist/console-bridge.d.ts +32 -0
- package/dist/console-bridge.js +158 -0
- package/dist/console-state.d.ts +19 -0
- package/dist/console-state.js +46 -0
- package/dist/console.d.ts +18 -0
- package/dist/console.js +17 -0
- package/dist/context.d.ts +40 -0
- package/dist/context.js +162 -0
- package/dist/express.d.ts +41 -0
- package/dist/express.js +48 -0
- package/dist/fastify.d.ts +37 -0
- package/dist/fastify.js +53 -0
- package/dist/hono.d.ts +37 -0
- package/dist/hono.js +50 -0
- package/dist/http-ambient.d.ts +12 -0
- package/dist/http-ambient.js +20 -0
- package/dist/http-context.d.ts +65 -0
- package/dist/http-context.js +156 -0
- package/dist/http.d.ts +24 -0
- package/dist/http.js +34 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +26 -0
- package/dist/json-logging.d.ts +9 -0
- package/dist/json-logging.js +12 -0
- package/dist/koa.d.ts +34 -0
- package/dist/koa.js +54 -0
- package/dist/logger.d.ts +46 -0
- package/dist/logger.js +130 -0
- package/dist/next.d.ts +41 -0
- package/dist/next.js +35 -0
- package/dist/objects.d.ts +12 -0
- package/dist/objects.js +18 -0
- package/dist/redact.d.ts +9 -0
- package/dist/redact.js +77 -0
- package/dist/remix.d.ts +43 -0
- package/dist/remix.js +38 -0
- package/dist/sanitize.d.ts +25 -0
- package/dist/sanitize.js +153 -0
- package/dist/serialize.d.ts +31 -0
- package/dist/serialize.js +203 -0
- package/dist/severity.d.ts +10 -0
- package/dist/severity.js +25 -0
- package/dist/sinks.d.ts +14 -0
- package/dist/sinks.js +66 -0
- package/dist/timestamp.d.ts +15 -0
- package/dist/timestamp.js +47 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +2 -0
- package/package.json +98 -4
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Dot-path segments after {@link compileRedactPaths} (config-time). */
|
|
2
|
+
export type CompiledRedactPath = readonly string[];
|
|
3
|
+
/** Compile redact paths once at logger construction. Config-time only. */
|
|
4
|
+
export declare function compileRedactPaths(paths: readonly string[]): CompiledRedactPath[];
|
|
5
|
+
/** Apply precompiled redact paths to a value tree. */
|
|
6
|
+
export declare function applyCompiledRedactPaths(value: unknown, paths: readonly CompiledRedactPath[]): unknown;
|
|
7
|
+
/** Apply string paths (compiles each call). Prefer compileRedactPaths on hot paths. */
|
|
8
|
+
export declare function applyRedactPaths(value: unknown, paths: readonly string[]): unknown;
|
|
9
|
+
//# sourceMappingURL=redact.d.ts.map
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { plainObjectFrom } from "./objects.js";
|
|
2
|
+
const FILTERED = "[FILTERED]";
|
|
3
|
+
function splitPath(path) {
|
|
4
|
+
if (path.length === 0) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
const parts = path.split(".");
|
|
8
|
+
if (parts.some((part) => part.length === 0)) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
return parts;
|
|
12
|
+
}
|
|
13
|
+
/** Compile redact paths once at logger construction. Config-time only. */
|
|
14
|
+
export function compileRedactPaths(paths) {
|
|
15
|
+
const compiled = [];
|
|
16
|
+
for (const path of paths) {
|
|
17
|
+
const parts = splitPath(path);
|
|
18
|
+
if (parts) {
|
|
19
|
+
compiled.push(parts);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return compiled;
|
|
23
|
+
}
|
|
24
|
+
function redactArray(value, parts, index, key) {
|
|
25
|
+
if (key === "*") {
|
|
26
|
+
return value.map((item) => redactAt(item, parts, index + 1));
|
|
27
|
+
}
|
|
28
|
+
const idx = Number(key);
|
|
29
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= value.length) {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
const copy = value.slice();
|
|
33
|
+
copy[idx] = redactAt(copy[idx], parts, index + 1);
|
|
34
|
+
return copy;
|
|
35
|
+
}
|
|
36
|
+
function redactObject(value, parts, index, key) {
|
|
37
|
+
if (key === "*") {
|
|
38
|
+
const out = {};
|
|
39
|
+
for (const childKey of Object.keys(value)) {
|
|
40
|
+
out[childKey] = redactAt(value[childKey], parts, index + 1);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
if (!(key in value)) {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...value,
|
|
49
|
+
[key]: redactAt(value[key], parts, index + 1),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function redactAt(value, parts, index) {
|
|
53
|
+
if (index >= parts.length) {
|
|
54
|
+
return FILTERED;
|
|
55
|
+
}
|
|
56
|
+
if (typeof value !== "object" || value === null) {
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
const key = String(parts[index]);
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
return redactArray(value, parts, index, key);
|
|
62
|
+
}
|
|
63
|
+
return redactObject(plainObjectFrom(value), parts, index, key);
|
|
64
|
+
}
|
|
65
|
+
/** Apply precompiled redact paths to a value tree. */
|
|
66
|
+
export function applyCompiledRedactPaths(value, paths) {
|
|
67
|
+
let current = value;
|
|
68
|
+
for (const parts of paths) {
|
|
69
|
+
current = redactAt(current, parts, 0);
|
|
70
|
+
}
|
|
71
|
+
return current;
|
|
72
|
+
}
|
|
73
|
+
/** Apply string paths (compiles each call). Prefer compileRedactPaths on hot paths. */
|
|
74
|
+
export function applyRedactPaths(value, paths) {
|
|
75
|
+
return applyCompiledRedactPaths(value, compileRedactPaths(paths));
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=redact.js.map
|
package/dist/remix.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remix helpers: run handleRequest / loader bodies with request-scoped log context.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { withRemixContext } from "@amkisko/json-logging/remix";
|
|
7
|
+
* import { httpLogger } from "@amkisko/json-logging/http";
|
|
8
|
+
*
|
|
9
|
+
* return withRemixContext({ request, context }, () => {
|
|
10
|
+
* httpLogger().info("loading");
|
|
11
|
+
* return handle(request);
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import type { Logger } from "./logger.js";
|
|
18
|
+
import type { LoggerOptions } from "./types.js";
|
|
19
|
+
/** Minimal Remix request shape (no Remix runtime dependency). */
|
|
20
|
+
export type RemixRequestLike = {
|
|
21
|
+
method?: string;
|
|
22
|
+
url?: string;
|
|
23
|
+
headers?: Headers;
|
|
24
|
+
};
|
|
25
|
+
/** Load / app context bag passed into Remix. */
|
|
26
|
+
export type RemixContextBag = Record<string, unknown>;
|
|
27
|
+
/** Options for {@link withRemixContext}. */
|
|
28
|
+
export type WithRemixContextOptions = {
|
|
29
|
+
logger?: Logger;
|
|
30
|
+
loggerOptions?: LoggerOptions;
|
|
31
|
+
requestIdHeader?: string;
|
|
32
|
+
/** Allowlisted keys copied from load context into log context. Default: none. */
|
|
33
|
+
contextKeys?: readonly string[];
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Run a Remix handleRequest / middleware / loader body with ALS context.
|
|
37
|
+
* Bind loggers via `@amkisko/json-logging/http` (`bindHttpLogger` / `httpLogger`).
|
|
38
|
+
*/
|
|
39
|
+
export declare function withRemixContext<T>(input: {
|
|
40
|
+
request?: RemixRequestLike;
|
|
41
|
+
context?: RemixContextBag;
|
|
42
|
+
}, fn: () => T | Promise<T>, options?: WithRemixContextOptions): T | Promise<T>;
|
|
43
|
+
//# sourceMappingURL=remix.d.ts.map
|
package/dist/remix.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remix helpers: run handleRequest / loader bodies with request-scoped log context.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { withRemixContext } from "@amkisko/json-logging/remix";
|
|
7
|
+
* import { httpLogger } from "@amkisko/json-logging/http";
|
|
8
|
+
*
|
|
9
|
+
* return withRemixContext({ request, context }, () => {
|
|
10
|
+
* httpLogger().info("loading");
|
|
11
|
+
* return handle(request);
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { bindLoggerFromOptions } from "./http-ambient.js";
|
|
18
|
+
import { pickContextKeys, withHttpContext } from "./http-context.js";
|
|
19
|
+
/**
|
|
20
|
+
* Run a Remix handleRequest / middleware / loader body with ALS context.
|
|
21
|
+
* Bind loggers via `@amkisko/json-logging/http` (`bindHttpLogger` / `httpLogger`).
|
|
22
|
+
*/
|
|
23
|
+
export function withRemixContext(input, fn, options = {}) {
|
|
24
|
+
bindLoggerFromOptions(options);
|
|
25
|
+
const requestId = input.context?.requestId != null ? String(input.context.requestId) : undefined;
|
|
26
|
+
return withHttpContext({
|
|
27
|
+
headers: input.request?.headers,
|
|
28
|
+
method: input.request?.method,
|
|
29
|
+
url: input.request?.url,
|
|
30
|
+
requestId,
|
|
31
|
+
}, fn, {
|
|
32
|
+
headerName: options.requestIdHeader,
|
|
33
|
+
logger: options.logger,
|
|
34
|
+
loggerOptions: options.loggerOptions,
|
|
35
|
+
extra: pickContextKeys(input.context, options.contextKeys),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=remix.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { JsonValue } from "./types.js";
|
|
2
|
+
/** Maximum string length kept after sanitization (longer values are truncated). */
|
|
3
|
+
export declare const MAX_STRING_LENGTH = 10000;
|
|
4
|
+
/** Maximum number of keys kept in a sanitized object. */
|
|
5
|
+
export declare const MAX_CONTEXT_SIZE = 50;
|
|
6
|
+
/** Maximum nesting depth when sanitizing structured values. */
|
|
7
|
+
export declare const MAX_DEPTH = 10;
|
|
8
|
+
/** Maximum stack frames kept on sanitized Error backtraces. */
|
|
9
|
+
export declare const MAX_BACKTRACE_LINES = 20;
|
|
10
|
+
/** Case-insensitive sensitive key names (password, secret, token, authorization, api_key, ...). */
|
|
11
|
+
export declare const SENSITIVE_KEY_PATTERN: RegExp;
|
|
12
|
+
/** True when a key name matches {@link SENSITIVE_KEY_PATTERN}. */
|
|
13
|
+
export declare function isSensitiveKey(key: string): boolean;
|
|
14
|
+
/** Strip control characters and truncate to {@link MAX_STRING_LENGTH}. */
|
|
15
|
+
export declare function sanitizeString(value: string): string;
|
|
16
|
+
/** Coerce tags to non-empty sanitized strings, dropping nullish entries. */
|
|
17
|
+
export declare function prepareTags(tags: readonly unknown[]): string[];
|
|
18
|
+
/** Shape an Error under an `error` key for log payloads (name, message, stack, cause). */
|
|
19
|
+
export declare function sanitizeError(error: Error): {
|
|
20
|
+
[key: string]: JsonValue;
|
|
21
|
+
};
|
|
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. */
|
|
24
|
+
export declare function sanitizeHash(hash: Record<string, unknown>, depth?: number): Record<string, unknown>;
|
|
25
|
+
//# sourceMappingURL=sanitize.d.ts.map
|
package/dist/sanitize.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { plainObjectFrom } from "./objects.js";
|
|
2
|
+
/** Maximum string length kept after sanitization (longer values are truncated). */
|
|
3
|
+
export const MAX_STRING_LENGTH = 10_000;
|
|
4
|
+
/** Maximum number of keys kept in a sanitized object. */
|
|
5
|
+
export const MAX_CONTEXT_SIZE = 50;
|
|
6
|
+
/** Maximum nesting depth when sanitizing structured values. */
|
|
7
|
+
export const MAX_DEPTH = 10;
|
|
8
|
+
/** Maximum stack frames kept on sanitized Error backtraces. */
|
|
9
|
+
export const MAX_BACKTRACE_LINES = 20;
|
|
10
|
+
const CONTROL_CHARS = /[\x00-\x1F\x7F]/g;
|
|
11
|
+
/** Case-insensitive sensitive key names (password, secret, token, authorization, api_key, ...). */
|
|
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
|
+
const FILTERED = "[FILTERED]";
|
|
14
|
+
const CIRCULAR = "[Circular]";
|
|
15
|
+
/** True when a key name matches {@link SENSITIVE_KEY_PATTERN}. */
|
|
16
|
+
export function isSensitiveKey(key) {
|
|
17
|
+
return SENSITIVE_KEY_PATTERN.test(key);
|
|
18
|
+
}
|
|
19
|
+
/** Strip control characters and truncate to {@link MAX_STRING_LENGTH}. */
|
|
20
|
+
export function sanitizeString(value) {
|
|
21
|
+
try {
|
|
22
|
+
let result = value.replace(CONTROL_CHARS, "");
|
|
23
|
+
if (result.length > MAX_STRING_LENGTH) {
|
|
24
|
+
result = `${result.slice(0, MAX_STRING_LENGTH)}...[truncated]`;
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return "<sanitization_error>";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Coerce tags to non-empty sanitized strings, dropping nullish entries. */
|
|
33
|
+
export function prepareTags(tags) {
|
|
34
|
+
const prepared = [];
|
|
35
|
+
for (const tag of tags) {
|
|
36
|
+
if (tag === null || tag === undefined) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const text = sanitizeString(String(tag));
|
|
40
|
+
if (text.length > 0) {
|
|
41
|
+
prepared.push(text);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return prepared;
|
|
45
|
+
}
|
|
46
|
+
/** Shape an Error under an `error` key for log payloads (name, message, stack, cause). */
|
|
47
|
+
export function sanitizeError(error) {
|
|
48
|
+
try {
|
|
49
|
+
const body = {
|
|
50
|
+
name: sanitizeString(error.name || "Error"),
|
|
51
|
+
message: sanitizeString(String(error.message ?? "")),
|
|
52
|
+
};
|
|
53
|
+
if (typeof error.stack === "string" && error.stack.length > 0) {
|
|
54
|
+
body.stack = sanitizeBacktrace(error.stack);
|
|
55
|
+
}
|
|
56
|
+
const cause = Reflect.get(error, "cause");
|
|
57
|
+
if (cause !== undefined) {
|
|
58
|
+
body.cause = sanitizeValue(cause, 0, new WeakSet());
|
|
59
|
+
}
|
|
60
|
+
return { error: body };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return {
|
|
64
|
+
error: { name: "Error", message: "<sanitization_failed>" },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function sanitizeBacktrace(stack) {
|
|
69
|
+
const lines = stack.split("\n").slice(0, MAX_BACKTRACE_LINES);
|
|
70
|
+
return sanitizeString(lines.join("\n"));
|
|
71
|
+
}
|
|
72
|
+
export function sanitizeValue(value, depth, seen) {
|
|
73
|
+
try {
|
|
74
|
+
if (value === null) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
if (value === undefined) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (typeof value === "string") {
|
|
81
|
+
return sanitizeString(value);
|
|
82
|
+
}
|
|
83
|
+
if (typeof value === "number") {
|
|
84
|
+
return Number.isFinite(value) ? value : String(value);
|
|
85
|
+
}
|
|
86
|
+
if (typeof value === "boolean") {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
if (typeof value === "bigint") {
|
|
90
|
+
return value.toString();
|
|
91
|
+
}
|
|
92
|
+
if (typeof value === "symbol") {
|
|
93
|
+
return sanitizeString(String(value));
|
|
94
|
+
}
|
|
95
|
+
if (typeof value === "function") {
|
|
96
|
+
return sanitizeString(`[Function ${value.name || "anonymous"}]`);
|
|
97
|
+
}
|
|
98
|
+
if (value instanceof Error) {
|
|
99
|
+
return sanitizeError(value);
|
|
100
|
+
}
|
|
101
|
+
if (depth > MAX_DEPTH) {
|
|
102
|
+
return { error: "max_depth_exceeded" };
|
|
103
|
+
}
|
|
104
|
+
// Remaining values are objects (null already returned above).
|
|
105
|
+
if (seen.has(value)) {
|
|
106
|
+
return CIRCULAR;
|
|
107
|
+
}
|
|
108
|
+
seen.add(value);
|
|
109
|
+
if (Array.isArray(value)) {
|
|
110
|
+
return sanitizeArray(value, depth, seen);
|
|
111
|
+
}
|
|
112
|
+
return sanitizeObject(plainObjectFrom(value), depth, seen);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return "<unprintable>";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function sanitizeArray(value, depth, seen) {
|
|
119
|
+
const limited = value.slice(0, MAX_CONTEXT_SIZE);
|
|
120
|
+
const result = limited.map((item) => sanitizeValue(item, depth + 1, seen));
|
|
121
|
+
if (value.length > MAX_CONTEXT_SIZE) {
|
|
122
|
+
result.push("[truncated]");
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
function sanitizeObject(value, depth, seen) {
|
|
127
|
+
const result = {};
|
|
128
|
+
const keys = Object.keys(value).slice(0, MAX_CONTEXT_SIZE);
|
|
129
|
+
for (const key of keys) {
|
|
130
|
+
if (isSensitiveKey(key)) {
|
|
131
|
+
result[key] = FILTERED;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
result[key] = sanitizeValue(value[key], depth + 1, seen);
|
|
135
|
+
}
|
|
136
|
+
if (Object.keys(value).length > MAX_CONTEXT_SIZE) {
|
|
137
|
+
result.__truncated__ = true;
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
/** Sanitize a plain object: filter sensitive keys, bound depth/size, mark cycles. */
|
|
142
|
+
export function sanitizeHash(hash, depth = 0) {
|
|
143
|
+
try {
|
|
144
|
+
if (depth > MAX_DEPTH) {
|
|
145
|
+
return { error: "max_depth_exceeded" };
|
|
146
|
+
}
|
|
147
|
+
return sanitizeObject(hash, depth, new WeakSet());
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return { sanitization_error: true };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=sanitize.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type CompiledRedactPath } from "./redact.js";
|
|
2
|
+
import type { ContextProvider, FieldSerializer, Level, LogMessage, LogRecord } from "./types.js";
|
|
3
|
+
export type { PerformanceLike } from "./timestamp.js";
|
|
4
|
+
export { currentTimestamp, defaultPerformance, formatTimestamp, } from "./timestamp.js";
|
|
5
|
+
/** Reserved root keys; collisions from user fields move under `context`. */
|
|
6
|
+
export declare const SYSTEM_KEYS: ReadonlySet<string>;
|
|
7
|
+
/** Normalize a log message into root fields (string, object, or Error). */
|
|
8
|
+
export declare function parseMessage(message: LogMessage): Record<string, unknown>;
|
|
9
|
+
/** Inputs for {@link buildRecord} on the log hot path. */
|
|
10
|
+
export type BuildLineInput = {
|
|
11
|
+
message: LogMessage;
|
|
12
|
+
level: Level;
|
|
13
|
+
stickyTags: readonly string[];
|
|
14
|
+
base?: Record<string, unknown>;
|
|
15
|
+
/** When true, base was sanitized at Logger construction. */
|
|
16
|
+
preparedBase?: boolean;
|
|
17
|
+
/** When true, stickyTags were prepared at Logger construction. */
|
|
18
|
+
preparedTags?: boolean;
|
|
19
|
+
clock?: () => Date;
|
|
20
|
+
redact?: readonly string[];
|
|
21
|
+
compiledRedact?: readonly CompiledRedactPath[];
|
|
22
|
+
serializers?: Readonly<Record<string, FieldSerializer>>;
|
|
23
|
+
contextProvider?: ContextProvider;
|
|
24
|
+
};
|
|
25
|
+
/** Assemble one log record: message, severity, tags, context, serializers, redact. */
|
|
26
|
+
export declare function buildRecord(input: BuildLineInput): LogRecord;
|
|
27
|
+
/** Serialize a record to one JSON line with a trailing newline. */
|
|
28
|
+
export declare function toJsonLine(record: LogRecord): string;
|
|
29
|
+
/** Never-raise fallback record when the primary emit path throws. */
|
|
30
|
+
export declare function buildFallbackRecord(level: Level, message: unknown, error: unknown, clock?: () => Date): LogRecord;
|
|
31
|
+
//# sourceMappingURL=serialize.d.ts.map
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { peekLogStore } from "./context.js";
|
|
2
|
+
import { isPlainObject, readObjectField } from "./objects.js";
|
|
3
|
+
import { applyCompiledRedactPaths, applyRedactPaths } from "./redact.js";
|
|
4
|
+
import { prepareTags, sanitizeError, sanitizeHash, sanitizeString, sanitizeValue, } from "./sanitize.js";
|
|
5
|
+
import { severityName } from "./severity.js";
|
|
6
|
+
import { currentTimestamp } from "./timestamp.js";
|
|
7
|
+
export { currentTimestamp, defaultPerformance, formatTimestamp, } from "./timestamp.js";
|
|
8
|
+
/** Reserved root keys; collisions from user fields move under `context`. */
|
|
9
|
+
export const SYSTEM_KEYS = new Set([
|
|
10
|
+
"tags",
|
|
11
|
+
"severity",
|
|
12
|
+
"timestamp",
|
|
13
|
+
"message",
|
|
14
|
+
"context",
|
|
15
|
+
]);
|
|
16
|
+
/** Normalize a log message into root fields (string, object, or Error). */
|
|
17
|
+
export function parseMessage(message) {
|
|
18
|
+
if (message instanceof Error) {
|
|
19
|
+
return sanitizeError(message);
|
|
20
|
+
}
|
|
21
|
+
if (typeof message === "string") {
|
|
22
|
+
return { message: sanitizeString(message) };
|
|
23
|
+
}
|
|
24
|
+
return mergeUserFields({}, message);
|
|
25
|
+
}
|
|
26
|
+
function mergeUserFields(payload, fields, alreadySanitized = false) {
|
|
27
|
+
const source = alreadySanitized ? fields : sanitizeHash(fields);
|
|
28
|
+
const conflicts = {};
|
|
29
|
+
for (const [key, value] of Object.entries(source)) {
|
|
30
|
+
if (key === "context" && isPlainObject(value)) {
|
|
31
|
+
payload.context = {
|
|
32
|
+
...readObjectField(payload, "context"),
|
|
33
|
+
...value,
|
|
34
|
+
};
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (SYSTEM_KEYS.has(key) || key in payload) {
|
|
38
|
+
conflicts[key] = value;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
payload[key] = value;
|
|
42
|
+
}
|
|
43
|
+
if (Object.keys(conflicts).length > 0) {
|
|
44
|
+
payload.context = {
|
|
45
|
+
...readObjectField(payload, "context"),
|
|
46
|
+
...conflicts,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return payload;
|
|
50
|
+
}
|
|
51
|
+
function applySerializers(payload, serializers) {
|
|
52
|
+
for (const key of Object.keys(serializers)) {
|
|
53
|
+
if (!Object.hasOwn(payload, key)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const serialize = serializers[key];
|
|
57
|
+
if (typeof serialize !== "function") {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
payload[key] = serialize(payload[key]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function mergeProviderContext(payload, provider) {
|
|
64
|
+
const provided = provider();
|
|
65
|
+
if (!provided) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const sanitized = sanitizeHash(provided);
|
|
69
|
+
const filtered = {};
|
|
70
|
+
for (const [key, value] of Object.entries(sanitized)) {
|
|
71
|
+
if (SYSTEM_KEYS.has(key)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
filtered[key] = value;
|
|
75
|
+
}
|
|
76
|
+
if (Object.keys(filtered).length === 0) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Call-site / message context wins over provider.
|
|
80
|
+
payload.context = {
|
|
81
|
+
...filtered,
|
|
82
|
+
...readObjectField(payload, "context"),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Assemble one log record: message, severity, tags, context, serializers, redact. */
|
|
86
|
+
export function buildRecord(input) {
|
|
87
|
+
const payload = {};
|
|
88
|
+
if (input.base && Object.keys(input.base).length > 0) {
|
|
89
|
+
mergeUserFields(payload, input.base, input.preparedBase === true);
|
|
90
|
+
}
|
|
91
|
+
const parsed = parseMessage(input.message);
|
|
92
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
93
|
+
if (key === "context" && isPlainObject(value)) {
|
|
94
|
+
payload.context = {
|
|
95
|
+
...readObjectField(payload, "context"),
|
|
96
|
+
...value,
|
|
97
|
+
};
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
payload[key] = value;
|
|
101
|
+
}
|
|
102
|
+
payload.severity = severityName(input.level);
|
|
103
|
+
payload.timestamp = currentTimestamp(input.clock);
|
|
104
|
+
const store = peekLogStore();
|
|
105
|
+
if (input.stickyTags.length > 0 || store.tags.length > 0) {
|
|
106
|
+
const sticky = input.preparedTags ? [...input.stickyTags] : prepareTags(input.stickyTags);
|
|
107
|
+
const tags = uniqueTags([...sticky, ...prepareTags(store.tags)]);
|
|
108
|
+
if (tags.length > 0) {
|
|
109
|
+
payload.tags = tags;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const context = store.context;
|
|
113
|
+
if (context) {
|
|
114
|
+
let hasContext = false;
|
|
115
|
+
for (const _key in context) {
|
|
116
|
+
hasContext = true;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
if (hasContext) {
|
|
120
|
+
const sanitizedContext = sanitizeHash(context);
|
|
121
|
+
const filtered = {};
|
|
122
|
+
for (const [key, value] of Object.entries(sanitizedContext)) {
|
|
123
|
+
if (SYSTEM_KEYS.has(key)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
filtered[key] = value;
|
|
127
|
+
}
|
|
128
|
+
if (Object.keys(filtered).length > 0) {
|
|
129
|
+
payload.context = {
|
|
130
|
+
...filtered,
|
|
131
|
+
...readObjectField(payload, "context"),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (input.contextProvider) {
|
|
137
|
+
mergeProviderContext(payload, input.contextProvider);
|
|
138
|
+
}
|
|
139
|
+
if (input.serializers) {
|
|
140
|
+
let hasSerializers = false;
|
|
141
|
+
for (const _key in input.serializers) {
|
|
142
|
+
hasSerializers = true;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
if (hasSerializers) {
|
|
146
|
+
applySerializers(payload, input.serializers);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (input.compiledRedact && input.compiledRedact.length > 0) {
|
|
150
|
+
const redacted = applyCompiledRedactPaths(payload, input.compiledRedact);
|
|
151
|
+
if (isPlainObject(redacted)) {
|
|
152
|
+
for (const key of Object.keys(payload)) {
|
|
153
|
+
delete payload[key];
|
|
154
|
+
}
|
|
155
|
+
Object.assign(payload, redacted);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (input.redact && input.redact.length > 0) {
|
|
159
|
+
const redacted = applyRedactPaths(payload, input.redact);
|
|
160
|
+
if (isPlainObject(redacted)) {
|
|
161
|
+
for (const key of Object.keys(payload)) {
|
|
162
|
+
delete payload[key];
|
|
163
|
+
}
|
|
164
|
+
Object.assign(payload, redacted);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return payload;
|
|
168
|
+
}
|
|
169
|
+
function uniqueTags(tags) {
|
|
170
|
+
const seen = new Set();
|
|
171
|
+
const result = [];
|
|
172
|
+
for (const tag of tags) {
|
|
173
|
+
if (!seen.has(tag)) {
|
|
174
|
+
seen.add(tag);
|
|
175
|
+
result.push(tag);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
/** Serialize a record to one JSON line with a trailing newline. */
|
|
181
|
+
export function toJsonLine(record) {
|
|
182
|
+
const seen = new WeakSet();
|
|
183
|
+
const safe = sanitizeValue(record, 0, seen);
|
|
184
|
+
return `${JSON.stringify(safe)}\n`;
|
|
185
|
+
}
|
|
186
|
+
/** Never-raise fallback record when the primary emit path throws. */
|
|
187
|
+
export function buildFallbackRecord(level, message, error, clock) {
|
|
188
|
+
const err = error instanceof Error ? error : new Error(typeof error === "string" ? error : "unknown");
|
|
189
|
+
return {
|
|
190
|
+
timestamp: currentTimestamp(clock),
|
|
191
|
+
severity: severityName(level),
|
|
192
|
+
message: sanitizeString(typeof message === "string"
|
|
193
|
+
? message
|
|
194
|
+
: message instanceof Error
|
|
195
|
+
? message.message
|
|
196
|
+
: "<unprintable>"),
|
|
197
|
+
logger_error: {
|
|
198
|
+
name: sanitizeString(err.name),
|
|
199
|
+
message: sanitizeString(String(err.message ?? "")),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=serialize.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Level, SeverityName } from "./types.js";
|
|
2
|
+
/** Numeric order used to compare levels (higher is more severe). */
|
|
3
|
+
export declare const LEVEL_ORDER: Record<Level, number>;
|
|
4
|
+
/** Map from {@link Level} to the severity string written on each line. */
|
|
5
|
+
export declare const SEVERITY_NAMES: Record<Level, SeverityName>;
|
|
6
|
+
/** Return the uppercase severity name for a level. */
|
|
7
|
+
export declare function severityName(level: Level): SeverityName;
|
|
8
|
+
/** True when `requested` is at or above the configured minimum level. */
|
|
9
|
+
export declare function isEnabled(configured: Level, requested: Level): boolean;
|
|
10
|
+
//# sourceMappingURL=severity.d.ts.map
|
package/dist/severity.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Numeric order used to compare levels (higher is more severe). */
|
|
2
|
+
export const LEVEL_ORDER = {
|
|
3
|
+
debug: 0,
|
|
4
|
+
info: 1,
|
|
5
|
+
warn: 2,
|
|
6
|
+
error: 3,
|
|
7
|
+
fatal: 4,
|
|
8
|
+
};
|
|
9
|
+
/** Map from {@link Level} to the severity string written on each line. */
|
|
10
|
+
export const SEVERITY_NAMES = {
|
|
11
|
+
debug: "DEBUG",
|
|
12
|
+
info: "INFO",
|
|
13
|
+
warn: "WARN",
|
|
14
|
+
error: "ERROR",
|
|
15
|
+
fatal: "FATAL",
|
|
16
|
+
};
|
|
17
|
+
/** Return the uppercase severity name for a level. */
|
|
18
|
+
export function severityName(level) {
|
|
19
|
+
return SEVERITY_NAMES[level];
|
|
20
|
+
}
|
|
21
|
+
/** True when `requested` is at or above the configured minimum level. */
|
|
22
|
+
export function isEnabled(configured, requested) {
|
|
23
|
+
return LEVEL_ORDER[requested] >= LEVEL_ORDER[configured];
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=severity.js.map
|
package/dist/sinks.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ConsoleMethods } from "./console-state.js";
|
|
2
|
+
import type { Sink } from "./types.js";
|
|
3
|
+
/** Writes JSON lines using the given console method table (usually pre-bridge originals). */
|
|
4
|
+
export declare function consoleSinkFrom(methods: ConsoleMethods): Sink;
|
|
5
|
+
/**
|
|
6
|
+
* Default console sink. When a console bridge is installed, writes through the
|
|
7
|
+
* captured originals so logger calls do not re-enter the bridge.
|
|
8
|
+
*/
|
|
9
|
+
export declare function consoleSink(): Sink;
|
|
10
|
+
/** Sink that writes JSON lines to a Web Streams `WritableStream`. */
|
|
11
|
+
export declare function streamSink(stream: WritableStream<Uint8Array>, options?: {
|
|
12
|
+
encoding?: "utf-8";
|
|
13
|
+
}): Sink;
|
|
14
|
+
//# sourceMappingURL=sinks.d.ts.map
|