@amkisko/json-logging 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.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 +26 -0
- package/dist/console-bridge.js +155 -0
- package/dist/console-state.d.ts +11 -0
- package/dist/console-state.js +40 -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 +10 -0
- package/dist/http-ambient.js +18 -0
- package/dist/http-context.d.ts +49 -0
- package/dist/http-context.js +147 -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 +40 -0
- package/dist/next.js +34 -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 +20 -0
- package/dist/sanitize.js +148 -0
- package/dist/serialize.d.ts +27 -0
- package/dist/serialize.js +200 -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
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
export declare function isSensitiveKey(key: string): boolean;
|
|
13
|
+
export declare function sanitizeString(value: string): string;
|
|
14
|
+
export declare function prepareTags(tags: readonly unknown[]): string[];
|
|
15
|
+
export declare function sanitizeError(error: Error): {
|
|
16
|
+
[key: string]: JsonValue;
|
|
17
|
+
};
|
|
18
|
+
export declare function sanitizeValue(value: unknown, depth: number, seen: WeakSet<object>): JsonValue;
|
|
19
|
+
export declare function sanitizeHash(hash: Record<string, unknown>, depth?: number): Record<string, unknown>;
|
|
20
|
+
//# sourceMappingURL=sanitize.d.ts.map
|
package/dist/sanitize.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
export function isSensitiveKey(key) {
|
|
16
|
+
return SENSITIVE_KEY_PATTERN.test(key);
|
|
17
|
+
}
|
|
18
|
+
export function sanitizeString(value) {
|
|
19
|
+
try {
|
|
20
|
+
let result = value.replace(CONTROL_CHARS, "");
|
|
21
|
+
if (result.length > MAX_STRING_LENGTH) {
|
|
22
|
+
result = `${result.slice(0, MAX_STRING_LENGTH)}...[truncated]`;
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return "<sanitization_error>";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function prepareTags(tags) {
|
|
31
|
+
const prepared = [];
|
|
32
|
+
for (const tag of tags) {
|
|
33
|
+
if (tag === null || tag === undefined) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const text = sanitizeString(String(tag));
|
|
37
|
+
if (text.length > 0) {
|
|
38
|
+
prepared.push(text);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return prepared;
|
|
42
|
+
}
|
|
43
|
+
export function sanitizeError(error) {
|
|
44
|
+
try {
|
|
45
|
+
const body = {
|
|
46
|
+
name: sanitizeString(error.name || "Error"),
|
|
47
|
+
message: sanitizeString(String(error.message ?? "")),
|
|
48
|
+
};
|
|
49
|
+
if (typeof error.stack === "string" && error.stack.length > 0) {
|
|
50
|
+
body.stack = sanitizeBacktrace(error.stack);
|
|
51
|
+
}
|
|
52
|
+
const cause = Reflect.get(error, "cause");
|
|
53
|
+
if (cause !== undefined) {
|
|
54
|
+
body.cause = sanitizeValue(cause, 0, new WeakSet());
|
|
55
|
+
}
|
|
56
|
+
return { error: body };
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return {
|
|
60
|
+
error: { name: "Error", message: "<sanitization_failed>" },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function sanitizeBacktrace(stack) {
|
|
65
|
+
const lines = stack.split("\n").slice(0, MAX_BACKTRACE_LINES);
|
|
66
|
+
return sanitizeString(lines.join("\n"));
|
|
67
|
+
}
|
|
68
|
+
export function sanitizeValue(value, depth, seen) {
|
|
69
|
+
try {
|
|
70
|
+
if (value === null) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (value === undefined) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (typeof value === "string") {
|
|
77
|
+
return sanitizeString(value);
|
|
78
|
+
}
|
|
79
|
+
if (typeof value === "number") {
|
|
80
|
+
return Number.isFinite(value) ? value : String(value);
|
|
81
|
+
}
|
|
82
|
+
if (typeof value === "boolean") {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
if (typeof value === "bigint") {
|
|
86
|
+
return value.toString();
|
|
87
|
+
}
|
|
88
|
+
if (typeof value === "symbol") {
|
|
89
|
+
return sanitizeString(String(value));
|
|
90
|
+
}
|
|
91
|
+
if (typeof value === "function") {
|
|
92
|
+
return sanitizeString(`[Function ${value.name || "anonymous"}]`);
|
|
93
|
+
}
|
|
94
|
+
if (value instanceof Error) {
|
|
95
|
+
return sanitizeError(value);
|
|
96
|
+
}
|
|
97
|
+
if (depth > MAX_DEPTH) {
|
|
98
|
+
return { error: "max_depth_exceeded" };
|
|
99
|
+
}
|
|
100
|
+
// Remaining values are objects (null already returned above).
|
|
101
|
+
if (seen.has(value)) {
|
|
102
|
+
return CIRCULAR;
|
|
103
|
+
}
|
|
104
|
+
seen.add(value);
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
return sanitizeArray(value, depth, seen);
|
|
107
|
+
}
|
|
108
|
+
return sanitizeObject(plainObjectFrom(value), depth, seen);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return "<unprintable>";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function sanitizeArray(value, depth, seen) {
|
|
115
|
+
const limited = value.slice(0, MAX_CONTEXT_SIZE);
|
|
116
|
+
const result = limited.map((item) => sanitizeValue(item, depth + 1, seen));
|
|
117
|
+
if (value.length > MAX_CONTEXT_SIZE) {
|
|
118
|
+
result.push("[truncated]");
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
function sanitizeObject(value, depth, seen) {
|
|
123
|
+
const result = {};
|
|
124
|
+
const keys = Object.keys(value).slice(0, MAX_CONTEXT_SIZE);
|
|
125
|
+
for (const key of keys) {
|
|
126
|
+
if (isSensitiveKey(key)) {
|
|
127
|
+
result[key] = FILTERED;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
result[key] = sanitizeValue(value[key], depth + 1, seen);
|
|
131
|
+
}
|
|
132
|
+
if (Object.keys(value).length > MAX_CONTEXT_SIZE) {
|
|
133
|
+
result.__truncated__ = true;
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
export function sanitizeHash(hash, depth = 0) {
|
|
138
|
+
try {
|
|
139
|
+
if (depth > MAX_DEPTH) {
|
|
140
|
+
return { error: "max_depth_exceeded" };
|
|
141
|
+
}
|
|
142
|
+
return sanitizeObject(hash, depth, new WeakSet());
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return { sanitization_error: true };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=sanitize.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
export type BuildLineInput = {
|
|
10
|
+
message: LogMessage;
|
|
11
|
+
level: Level;
|
|
12
|
+
stickyTags: readonly string[];
|
|
13
|
+
base?: Record<string, unknown>;
|
|
14
|
+
/** When true, base was sanitized at Logger construction. */
|
|
15
|
+
preparedBase?: boolean;
|
|
16
|
+
/** When true, stickyTags were prepared at Logger construction. */
|
|
17
|
+
preparedTags?: boolean;
|
|
18
|
+
clock?: () => Date;
|
|
19
|
+
redact?: readonly string[];
|
|
20
|
+
compiledRedact?: readonly CompiledRedactPath[];
|
|
21
|
+
serializers?: Readonly<Record<string, FieldSerializer>>;
|
|
22
|
+
contextProvider?: ContextProvider;
|
|
23
|
+
};
|
|
24
|
+
export declare function buildRecord(input: BuildLineInput): LogRecord;
|
|
25
|
+
export declare function toJsonLine(record: LogRecord): string;
|
|
26
|
+
export declare function buildFallbackRecord(level: Level, message: unknown, error: unknown, clock?: () => Date): LogRecord;
|
|
27
|
+
//# sourceMappingURL=serialize.d.ts.map
|
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
export function buildRecord(input) {
|
|
86
|
+
const payload = {};
|
|
87
|
+
if (input.base && Object.keys(input.base).length > 0) {
|
|
88
|
+
mergeUserFields(payload, input.base, input.preparedBase === true);
|
|
89
|
+
}
|
|
90
|
+
const parsed = parseMessage(input.message);
|
|
91
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
92
|
+
if (key === "context" && isPlainObject(value)) {
|
|
93
|
+
payload.context = {
|
|
94
|
+
...readObjectField(payload, "context"),
|
|
95
|
+
...value,
|
|
96
|
+
};
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
payload[key] = value;
|
|
100
|
+
}
|
|
101
|
+
payload.severity = severityName(input.level);
|
|
102
|
+
payload.timestamp = currentTimestamp(input.clock);
|
|
103
|
+
const store = peekLogStore();
|
|
104
|
+
if (input.stickyTags.length > 0 || store.tags.length > 0) {
|
|
105
|
+
const sticky = input.preparedTags ? [...input.stickyTags] : prepareTags(input.stickyTags);
|
|
106
|
+
const tags = uniqueTags([...sticky, ...prepareTags(store.tags)]);
|
|
107
|
+
if (tags.length > 0) {
|
|
108
|
+
payload.tags = tags;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const context = store.context;
|
|
112
|
+
if (context) {
|
|
113
|
+
let hasContext = false;
|
|
114
|
+
for (const _key in context) {
|
|
115
|
+
hasContext = true;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (hasContext) {
|
|
119
|
+
const sanitizedContext = sanitizeHash(context);
|
|
120
|
+
const filtered = {};
|
|
121
|
+
for (const [key, value] of Object.entries(sanitizedContext)) {
|
|
122
|
+
if (SYSTEM_KEYS.has(key)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
filtered[key] = value;
|
|
126
|
+
}
|
|
127
|
+
if (Object.keys(filtered).length > 0) {
|
|
128
|
+
payload.context = {
|
|
129
|
+
...filtered,
|
|
130
|
+
...readObjectField(payload, "context"),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (input.contextProvider) {
|
|
136
|
+
mergeProviderContext(payload, input.contextProvider);
|
|
137
|
+
}
|
|
138
|
+
if (input.serializers) {
|
|
139
|
+
let hasSerializers = false;
|
|
140
|
+
for (const _key in input.serializers) {
|
|
141
|
+
hasSerializers = true;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
if (hasSerializers) {
|
|
145
|
+
applySerializers(payload, input.serializers);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (input.compiledRedact && input.compiledRedact.length > 0) {
|
|
149
|
+
const redacted = applyCompiledRedactPaths(payload, input.compiledRedact);
|
|
150
|
+
if (isPlainObject(redacted)) {
|
|
151
|
+
for (const key of Object.keys(payload)) {
|
|
152
|
+
delete payload[key];
|
|
153
|
+
}
|
|
154
|
+
Object.assign(payload, redacted);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (input.redact && input.redact.length > 0) {
|
|
158
|
+
const redacted = applyRedactPaths(payload, input.redact);
|
|
159
|
+
if (isPlainObject(redacted)) {
|
|
160
|
+
for (const key of Object.keys(payload)) {
|
|
161
|
+
delete payload[key];
|
|
162
|
+
}
|
|
163
|
+
Object.assign(payload, redacted);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return payload;
|
|
167
|
+
}
|
|
168
|
+
function uniqueTags(tags) {
|
|
169
|
+
const seen = new Set();
|
|
170
|
+
const result = [];
|
|
171
|
+
for (const tag of tags) {
|
|
172
|
+
if (!seen.has(tag)) {
|
|
173
|
+
seen.add(tag);
|
|
174
|
+
result.push(tag);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
export function toJsonLine(record) {
|
|
180
|
+
const seen = new WeakSet();
|
|
181
|
+
const safe = sanitizeValue(record, 0, seen);
|
|
182
|
+
return `${JSON.stringify(safe)}\n`;
|
|
183
|
+
}
|
|
184
|
+
export function buildFallbackRecord(level, message, error, clock) {
|
|
185
|
+
const err = error instanceof Error ? error : new Error(typeof error === "string" ? error : "unknown");
|
|
186
|
+
return {
|
|
187
|
+
timestamp: currentTimestamp(clock),
|
|
188
|
+
severity: severityName(level),
|
|
189
|
+
message: sanitizeString(typeof message === "string"
|
|
190
|
+
? message
|
|
191
|
+
: message instanceof Error
|
|
192
|
+
? message.message
|
|
193
|
+
: "<unprintable>"),
|
|
194
|
+
logger_error: {
|
|
195
|
+
name: sanitizeString(err.name),
|
|
196
|
+
message: sanitizeString(String(err.message ?? "")),
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
//# 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
|
package/dist/sinks.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { captureConsoleMethods, getBridgeOriginals } from "./console-state.js";
|
|
2
|
+
function pickMethod(methods, severity) {
|
|
3
|
+
const name = typeof severity === "string" ? severity.toUpperCase() : "INFO";
|
|
4
|
+
switch (name) {
|
|
5
|
+
case "ERROR":
|
|
6
|
+
case "FATAL":
|
|
7
|
+
return methods.error;
|
|
8
|
+
case "WARN":
|
|
9
|
+
return methods.warn;
|
|
10
|
+
case "DEBUG":
|
|
11
|
+
return methods.debug;
|
|
12
|
+
default:
|
|
13
|
+
return methods.info;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Writes JSON lines using the given console method table (usually pre-bridge originals). */
|
|
17
|
+
export function consoleSinkFrom(methods) {
|
|
18
|
+
return {
|
|
19
|
+
write(line, record) {
|
|
20
|
+
const method = pickMethod(methods, record.severity);
|
|
21
|
+
const trimmed = line.endsWith("\n") ? line.slice(0, -1) : line;
|
|
22
|
+
method(trimmed);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Default console sink. When a console bridge is installed, writes through the
|
|
28
|
+
* captured originals so logger calls do not re-enter the bridge.
|
|
29
|
+
*/
|
|
30
|
+
export function consoleSink() {
|
|
31
|
+
return {
|
|
32
|
+
write(line, record) {
|
|
33
|
+
const methods = getBridgeOriginals() ?? captureConsoleMethods(console);
|
|
34
|
+
const method = pickMethod(methods, record.severity);
|
|
35
|
+
const trimmed = line.endsWith("\n") ? line.slice(0, -1) : line;
|
|
36
|
+
method(trimmed);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Sink that writes JSON lines to a Web Streams `WritableStream`. */
|
|
41
|
+
export function streamSink(stream, options) {
|
|
42
|
+
const encoding = options?.encoding ?? "utf-8";
|
|
43
|
+
const encoder = new TextEncoder();
|
|
44
|
+
let chain = Promise.resolve();
|
|
45
|
+
return {
|
|
46
|
+
write(line, _record) {
|
|
47
|
+
const chunk = line.endsWith("\n") ? line : `${line}\n`;
|
|
48
|
+
const bytes = encoding === "utf-8" ? encoder.encode(chunk) : encoder.encode(chunk);
|
|
49
|
+
chain = chain
|
|
50
|
+
.then(async () => {
|
|
51
|
+
const writer = stream.getWriter();
|
|
52
|
+
try {
|
|
53
|
+
await writer.write(bytes);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
writer.releaseLock();
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.catch(() => {
|
|
60
|
+
// Never throw from the sink chain; swallow write failures.
|
|
61
|
+
});
|
|
62
|
+
return chain;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=sinks.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Format a Date as an ISO-8601 UTC string ending in `Z`. */
|
|
2
|
+
export declare function formatTimestamp(date: Date): string;
|
|
3
|
+
/** Subset of the Performance API used for microsecond timestamps. */
|
|
4
|
+
export type PerformanceLike = {
|
|
5
|
+
now: () => number;
|
|
6
|
+
timeOrigin: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Prefer microsecond precision via performance.timeOrigin when no test clock is set.
|
|
10
|
+
* Pass perf=null to force the Date millisecond fallback.
|
|
11
|
+
*/
|
|
12
|
+
export declare function currentTimestamp(clock?: () => Date, perf?: PerformanceLike | null): string;
|
|
13
|
+
/** Resolve `globalThis.performance` when it looks like {@link PerformanceLike}. */
|
|
14
|
+
export declare function defaultPerformance(globalPerf?: unknown | null): PerformanceLike | undefined;
|
|
15
|
+
//# sourceMappingURL=timestamp.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Format a Date as an ISO-8601 UTC string ending in `Z`. */
|
|
2
|
+
export function formatTimestamp(date) {
|
|
3
|
+
return date.toISOString();
|
|
4
|
+
}
|
|
5
|
+
function isPerformanceLike(value) {
|
|
6
|
+
if (typeof value !== "object" || value === null) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
return (typeof Reflect.get(value, "now") === "function" &&
|
|
10
|
+
typeof Reflect.get(value, "timeOrigin") === "number");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Prefer microsecond precision via performance.timeOrigin when no test clock is set.
|
|
14
|
+
* Pass perf=null to force the Date millisecond fallback.
|
|
15
|
+
*/
|
|
16
|
+
export function currentTimestamp(clock, perf) {
|
|
17
|
+
if (clock) {
|
|
18
|
+
return formatTimestamp(clock());
|
|
19
|
+
}
|
|
20
|
+
const resolved = perf === undefined ? defaultPerformance() : (perf ?? undefined);
|
|
21
|
+
try {
|
|
22
|
+
if (resolved) {
|
|
23
|
+
const epochMs = resolved.timeOrigin + resolved.now();
|
|
24
|
+
const wholeSeconds = Math.floor(epochMs / 1000);
|
|
25
|
+
const micros = Math.round((epochMs % 1000) * 1000);
|
|
26
|
+
const isoSecond = new Date(wholeSeconds * 1000).toISOString().replace(/\.\d{3}Z$/, "");
|
|
27
|
+
return `${isoSecond}.${String(micros).padStart(6, "0")}Z`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// fall through to Date
|
|
32
|
+
}
|
|
33
|
+
return formatTimestamp(new Date());
|
|
34
|
+
}
|
|
35
|
+
/** Resolve `globalThis.performance` when it looks like {@link PerformanceLike}. */
|
|
36
|
+
export function defaultPerformance(globalPerf) {
|
|
37
|
+
const resolved = globalPerf === undefined
|
|
38
|
+
? typeof performance !== "undefined"
|
|
39
|
+
? performance
|
|
40
|
+
: undefined
|
|
41
|
+
: globalPerf;
|
|
42
|
+
if (isPerformanceLike(resolved)) {
|
|
43
|
+
return resolved;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=timestamp.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Minimum severity that will be emitted. */
|
|
2
|
+
export type Level = "debug" | "info" | "warn" | "error" | "fatal";
|
|
3
|
+
/** Uppercase severity string written on each log line. */
|
|
4
|
+
export type SeverityName = "DEBUG" | "INFO" | "WARN" | "ERROR" | "FATAL";
|
|
5
|
+
/** Value accepted by severity methods: plain text, structured fields, or an Error. */
|
|
6
|
+
export type LogMessage = string | Record<string, unknown> | Error;
|
|
7
|
+
/** Sanitized object passed to sinks and JSON serialization. */
|
|
8
|
+
export type LogRecord = Record<string, unknown>;
|
|
9
|
+
/** Destination for one JSON line plus its record. */
|
|
10
|
+
export type Sink = {
|
|
11
|
+
write(line: string, record: LogRecord): void | Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
/** Root-field transform applied after message parse and before path redact. */
|
|
14
|
+
export type FieldSerializer = (value: unknown) => unknown;
|
|
15
|
+
/** Optional ambient enrichment at log time (e.g. trace ids). Must not throw; throws take never-raise fallback. */
|
|
16
|
+
export type ContextProvider = () => Record<string, unknown> | undefined;
|
|
17
|
+
/** Construction options for {@link Logger}. */
|
|
18
|
+
export type LoggerOptions = {
|
|
19
|
+
/** Minimum level to emit. Default `info`. */
|
|
20
|
+
level?: Level;
|
|
21
|
+
/** Output destination. Default console sink. */
|
|
22
|
+
sink?: Sink;
|
|
23
|
+
/** Fields merged into every line at the root. */
|
|
24
|
+
base?: Record<string, unknown>;
|
|
25
|
+
/** Clock for timestamps (tests). */
|
|
26
|
+
clock?: () => Date;
|
|
27
|
+
/** Sticky tags applied to every line from this logger. */
|
|
28
|
+
tags?: readonly string[];
|
|
29
|
+
/** Dot paths to filter (config-time only). Segment `*` matches one key or index. */
|
|
30
|
+
redact?: readonly string[];
|
|
31
|
+
/** Root field name to serializer. */
|
|
32
|
+
serializers?: Readonly<Record<string, FieldSerializer>>;
|
|
33
|
+
/** Merged under `context` after ALS ambient context; call-site context wins. */
|
|
34
|
+
contextProvider?: ContextProvider;
|
|
35
|
+
};
|
|
36
|
+
/** JSON scalar used in sanitized payloads. */
|
|
37
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
38
|
+
/** JSON-compatible value after sanitization. */
|
|
39
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
40
|
+
[key: string]: JsonValue;
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED