export const DIAGNOSTIC_LEVELS = Object.freeze([ "debug", "info", "warn", "error", ] as const); export { queueSizeBucket } from "./diagnostic-buckets.ts"; export type DiagnosticLevel = (typeof DIAGNOSTIC_LEVELS)[number]; export const DIAGNOSTIC_EVENT_REGISTRY = Object.freeze({ "app.boot.failed": Object.freeze({ level: "error" }), "http.request.completed": Object.freeze({ level: "info" }), "cache.operation.failed": Object.freeze({ level: "warn" }), "storage.operation.failed": Object.freeze({ level: "warn" }), "route.changed": Object.freeze({ level: "info" }), "ui.render.failed": Object.freeze({ level: "error" }), "release.mismatch.detected": Object.freeze({ level: "warn" }), "telemetry.delivery.dropped": Object.freeze({ level: "warn" }), }); export type DiagnosticEventId = keyof typeof DIAGNOSTIC_EVENT_REGISTRY; export const DIAGNOSTIC_CONTEXT_ALLOWLIST = Object.freeze([ "app_version", "build_id", "release_id", "active_release_id", "config_schema_version", "api_contract_version", "route_id", "operation_id", "correlation_id", "error_kind", "outcome", "http_status_group", "attempt_count_bucket", "duration_bucket", "component_boundary", "mismatch_kind", "operation", "reason", "queue_size_bucket", ] as const); export type DiagnosticContextKey = (typeof DIAGNOSTIC_CONTEXT_ALLOWLIST)[number]; export type DiagnosticContext = Readonly< Partial> >; export type DiagnosticRecordInput = Readonly<{ level: DiagnosticLevel; eventId: DiagnosticEventId; context?: Readonly>; }>; export type DiagnosticRecord = Readonly<{ level: DiagnosticLevel; eventId: DiagnosticEventId; timestamp: string; context: DiagnosticContext; }>; const SAFE_VALUE = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,63}$/; function projectDiagnosticRecordUnsafe( input: DiagnosticRecordInput, now: () => number = Date.now, ): | Readonly<{ success: true; record: DiagnosticRecord }> | Readonly<{ success: false; reason: string }> { if (!Object.hasOwn(DIAGNOSTIC_EVENT_REGISTRY, input.eventId)) { return { success: false, reason: "unregistered-event" }; } if (!DIAGNOSTIC_LEVELS.includes(input.level)) { return { success: false, reason: "invalid-level" }; } const contextEntries = Object.entries(input.context ?? {}); if (contextEntries.length > DIAGNOSTIC_CONTEXT_ALLOWLIST.length) { return { success: false, reason: "invalid-context" }; } const projected: Partial< Record > = {}; for (const [key, value] of contextEntries) { if ( !DIAGNOSTIC_CONTEXT_ALLOWLIST.includes(key as DiagnosticContextKey) ) { return { success: false, reason: "unknown-context" }; } if (typeof value === "string") { if (!SAFE_VALUE.test(value)) { return { success: false, reason: "invalid-context" }; } projected[key as DiagnosticContextKey] = value; } else if (typeof value === "number" && Number.isFinite(value)) { projected[key as DiagnosticContextKey] = value; } else if (typeof value === "boolean") { projected[key as DiagnosticContextKey] = value; } else { return { success: false, reason: "invalid-context" }; } } let timestamp: string; try { timestamp = new Date(now()).toISOString(); } catch { timestamp = new Date(0).toISOString(); } return { success: true, record: Object.freeze({ level: input.level, eventId: input.eventId, timestamp, context: Object.freeze(projected), }), }; } export function projectDiagnosticRecord( input: DiagnosticRecordInput, now: () => number = Date.now, ): ReturnType { try { return projectDiagnosticRecordUnsafe(input, now); } catch { return { success: false, reason: "serialization-failure" }; } } export function safeErrorKind(error: unknown): string { try { if (error && typeof error === "object") { const record = error as Readonly>; if ( typeof record.kind === "string" && /^[A-Z][A-Z0-9_]{0,63}$/.test(record.kind) ) { return record.kind; } if ( typeof record.name === "string" && /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(record.name) ) { const normalized = record.name .replace(/([a-z0-9])([A-Z])/g, "$1_$2") .toUpperCase(); return /^[A-Z][A-Z0-9_]{0,63}$/.test(normalized) ? normalized : "UNKNOWN_FAILURE"; } } } catch { return "UNKNOWN_FAILURE"; } return "UNKNOWN_FAILURE"; } export function statusGroup(status: number | undefined): string { return typeof status === "number" && Number.isFinite(status) ? `${Math.max(0, Math.min(9, Math.floor(status / 100)))}xx` : "none"; } export function attemptBucket(attemptCount: number): string { if (attemptCount <= 1) return "1"; if (attemptCount === 2) return "2"; if (attemptCount <= 4) return "3-4"; return "5+"; } export function durationBucket(durationMs: number): string { if (!Number.isFinite(durationMs) || durationMs < 0) return "unknown"; if (durationMs < 100) return "lt100ms"; if (durationMs < 500) return "100-499ms"; if (durationMs < 2_000) return "500-1999ms"; return "gte2000ms"; }