Files
WRNexusJS/packages/observability/src/logging.ts
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

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);
}