80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
|
|
export interface LogRecord {
|
|
timestamp: string;
|
|
level: LogLevel;
|
|
message: string;
|
|
service: string;
|
|
traceId?: string;
|
|
spanId?: string;
|
|
requestId?: string;
|
|
attributes: Record<string, unknown>;
|
|
}
|
|
|
|
export interface StructuredLogger {
|
|
log(level: LogLevel, message: string, attributes?: Record<string, unknown>): void;
|
|
debug(message: string, attributes?: Record<string, unknown>): void;
|
|
info(message: string, attributes?: Record<string, unknown>): void;
|
|
warn(message: string, attributes?: Record<string, unknown>): void;
|
|
error(message: string, attributes?: Record<string, unknown>): void;
|
|
child(attributes: Record<string, unknown>): StructuredLogger;
|
|
}
|
|
|
|
export interface StructuredLoggerOptions {
|
|
service?: string;
|
|
level?: LogLevel;
|
|
now?: () => Date;
|
|
sink?: (record: LogRecord, line: string) => void;
|
|
redact?: readonly string[];
|
|
attributes?: Record<string, unknown>;
|
|
}
|
|
|
|
const LEVELS: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
|
|
function scrub(value: Record<string, unknown>, keys: Set<string>): Record<string, unknown> {
|
|
return Object.fromEntries(
|
|
Object.entries(value).map(([key, item]) => [
|
|
key,
|
|
keys.has(key.toLowerCase()) ? "[REDACTED]" : item,
|
|
]),
|
|
);
|
|
}
|
|
|
|
export function createStructuredLogger(options: StructuredLoggerOptions = {}): StructuredLogger {
|
|
const threshold = LEVELS[options.level ?? "info"];
|
|
const now = options.now ?? (() => new Date());
|
|
const sink = options.sink ?? ((_record, line) => console.log(line));
|
|
const redacted = new Set(
|
|
(options.redact ?? ["authorization", "cookie", "password", "secret", "token"]).map((key) =>
|
|
key.toLowerCase(),
|
|
),
|
|
);
|
|
const base = { ...(options.attributes ?? {}) };
|
|
const logger = (attributes: Record<string, unknown>): StructuredLogger => {
|
|
const log = (level: LogLevel, message: string, values: Record<string, unknown> = {}) => {
|
|
if (LEVELS[level] < threshold) return;
|
|
const combined = scrub({ ...attributes, ...values }, redacted);
|
|
const record: LogRecord = {
|
|
timestamp: now().toISOString(),
|
|
level,
|
|
message,
|
|
service: options.service ?? "wrnexus",
|
|
...(typeof combined.traceId === "string" ? { traceId: combined.traceId } : {}),
|
|
...(typeof combined.spanId === "string" ? { spanId: combined.spanId } : {}),
|
|
...(typeof combined.requestId === "string" ? { requestId: combined.requestId } : {}),
|
|
attributes: combined,
|
|
};
|
|
sink(record, JSON.stringify(record));
|
|
};
|
|
return {
|
|
log,
|
|
debug: (message, values) => log("debug", message, values),
|
|
info: (message, values) => log("info", message, values),
|
|
warn: (message, values) => log("warn", message, values),
|
|
error: (message, values) => log("error", message, values),
|
|
child: (values) => logger({ ...attributes, ...values }),
|
|
};
|
|
};
|
|
return logger(base);
|
|
}
|