import { appendFileSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync, } from "node:fs"; import { join } from "node:path"; export const LOG_SIZE_LIMIT_BYTES = 10 * 1024 * 1024; export const LOG_FILE_LIMIT = 10; export const LOG_RETENTION_DAYS = 30; const statusCategories = new Set([ "starting", "ready", "completed", "failed", "unavailable", "degraded", "blocked", "stopping", "stopped", ]); const errorCategories = new Set([ "none", "invalid_input", "storage_unavailable", "log_write_failed", "service_unavailable", "timeout", "internal_error", ]); const identifierPattern = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/; const codePattern = /^[a-z][a-z0-9_]{0,63}$/; export interface StructuredLogInput { correlation_id?: string; duration_ms?: number; error_category?: string; object_id?: string; status_category: string; } export interface StructuredJsonlLoggerOptions { component: "api"; directory: string; now?: () => Date; onWriteFailure?: () => void; } export class LogWriteError extends Error { constructor(options?: ErrorOptions) { super("log_write_failed", options); } } export class StructuredJsonlLogger { private readonly component: "api"; private readonly directory: string; private readonly now: () => Date; private readonly onWriteFailure: () => void; private initialized = false; private currentSize = 0; constructor(options: StructuredJsonlLoggerOptions) { this.component = options.component; this.directory = options.directory; this.now = options.now ?? (() => new Date()); this.onWriteFailure = options.onWriteFailure ?? (() => undefined); } write(input: StructuredLogInput | Record) { try { this.initialize(); const line = `${JSON.stringify(sanitizeLogEntry(this.component, input, this.now()))}\n`; const bytes = Buffer.byteLength(line); if (bytes > LOG_SIZE_LIMIT_BYTES) throw new Error("log_entry_too_large"); if (this.currentSize > 0 && this.currentSize + bytes > LOG_SIZE_LIMIT_BYTES) this.rotate(); appendFileSync(this.activePath(), line, { encoding: "utf8" }); this.currentSize += bytes; } catch (error) { if (error instanceof LogWriteError) throw error; this.fail(error); } } maintain() { try { mkdirSync(this.directory, { recursive: true }); this.maintainFiles(); this.currentSize = existsSync(this.activePath()) ? statSync(this.activePath()).size : 0; this.initialized = true; } catch (error) { if (error instanceof LogWriteError) throw error; this.fail(error); } } private initialize() { if (!this.initialized) this.maintain(); } private maintainFiles() { const cutoff = this.now().getTime() - LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000; for (const file of this.files()) { const path = join(this.directory, file.name); if (file.index > LOG_FILE_LIMIT - 1 || statSync(path).mtimeMs < cutoff) rmSync(path, { force: true }); } const remaining = this.files(); for (const file of remaining.slice(LOG_FILE_LIMIT)) rmSync(join(this.directory, file.name), { force: true }); } private rotate() { const last = join(this.directory, `${this.component}.${LOG_FILE_LIMIT - 1}.jsonl`); rmSync(last, { force: true }); for (let index = LOG_FILE_LIMIT - 2; index >= 1; index -= 1) { const source = join(this.directory, `${this.component}.${index}.jsonl`); if (existsSync(source)) renameSync(source, join(this.directory, `${this.component}.${index + 1}.jsonl`)); } if (existsSync(this.activePath())) renameSync(this.activePath(), join(this.directory, `${this.component}.1.jsonl`)); this.currentSize = 0; } private files() { if (!existsSync(this.directory)) return []; const pattern = new RegExp(`^${this.component}(?:\\.(\\d+))?\\.jsonl$`); return readdirSync(this.directory) .map((name) => ({ match: pattern.exec(name), name })) .filter((entry): entry is { match: RegExpExecArray; name: string } => entry.match !== null) .map(({ match, name }) => ({ index: match[1] === undefined ? 0 : Number(match[1]), name })) .sort((left, right) => left.index - right.index); } private activePath() { return join(this.directory, `${this.component}.jsonl`); } private fail(error: unknown): never { try { this.onWriteFailure(); } catch { // State persistence failure must not replace the stable redacted error. } throw new LogWriteError({ cause: error }); } } export function sanitizeLogEntry(component: "api", input: StructuredLogInput | Record, now: Date) { const value = input as Record; const entry: Record = { component, schema_version: "1.0", status_category: statusCategories.has(String(value.status_category)) ? value.status_category : "failed", timestamp: now.toISOString(), }; if (typeof value.correlation_id === "string" && identifierPattern.test(value.correlation_id)) entry.correlation_id = value.correlation_id; if (typeof value.object_id === "string" && identifierPattern.test(value.object_id)) entry.object_id = value.object_id; if (typeof value.duration_ms === "number" && Number.isSafeInteger(value.duration_ms) && value.duration_ms >= 0) entry.duration_ms = value.duration_ms; if (typeof value.error_category === "string") { entry.error_category = errorCategories.has(value.error_category) ? value.error_category : "internal_error"; } return entry; } export function sanitizeDiagnosticRecord(input: Record) { return { app_version: typeof input.app_version === "string" && /^[0-9]+(?:\.[0-9]+){2,3}$/.test(input.app_version) ? input.app_version : "0.0.0", checked_at: typeof input.checked_at === "string" && !Number.isNaN(Date.parse(input.checked_at)) ? input.checked_at : new Date(0).toISOString(), component: input.component === "api" || input.component === "worker" || input.component === "supervisor" ? input.component : "supervisor", message_key: typeof input.message_key === "string" && codePattern.test(input.message_key) ? input.message_key : "diagnostic_redacted", result: input.result === "pass" || input.result === "warning" || input.result === "fail" ? input.result : "fail", stable_check_code: typeof input.stable_check_code === "string" && codePattern.test(input.stable_check_code) ? input.stable_check_code : "invalid_check", }; }