/** * Detailed Request Logs DB Layer (#378) * * Legacy compatibility layer for detailed request logs. * New requests now store pipeline details inside unified call log artifacts. * This module remains available for reading historical request_detail_logs rows. */ import { v4 as uuidv4 } from "uuid"; import { getDbInstance } from "./core"; import { getSettings } from "./settings"; import { isNoLog } from "../compliance/noLog"; import { protectPayloadForLog, serializePayloadForStorage, parseStoredPayload, } from "../logPayloads"; import { compactStructuredStreamPayload } from "@omniroute/open-sse/utils/streamPayloadCollector.ts"; export interface RequestDetailLog { id?: string; call_log_id?: string | null; timestamp?: string; client_request?: unknown | null; translated_request?: unknown | null; provider_response?: unknown | null; client_response?: unknown | null; provider?: string | null; model?: string | null; source_format?: string | null; target_format?: string | null; duration_ms?: number; api_key_id?: string | null; no_log?: boolean; } let requestDetailLogsTableExistsCache: boolean | undefined; function requestDetailLogsTableExists(): boolean { if (requestDetailLogsTableExistsCache !== undefined) { return requestDetailLogsTableExistsCache; } const db = getDbInstance(); const row = db .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'request_detail_logs'") .get() as { name?: string } | undefined; requestDetailLogsTableExistsCache = Boolean(row?.name); return requestDetailLogsTableExistsCache; } export function resetRequestDetailLogsTableExistsCache(): void { requestDetailLogsTableExistsCache = undefined; } /** Returns true if detailed logging is enabled in settings */ export async function isDetailedLoggingEnabled(): Promise { try { const settings = await getSettings(); const val = settings.call_log_pipeline_enabled; return val === true || val === "1" || val === "true"; } catch { return false; } } /** Save a detailed log entry — caller must verify isDetailedLoggingEnabled() first */ export function saveRequestDetailLog(entry: RequestDetailLog): void { const noLogEnabled = Boolean(entry.no_log) || (entry.api_key_id ? isNoLog(entry.api_key_id) : false); if (noLogEnabled || !requestDetailLogsTableExists()) return; const db = getDbInstance(); const id = entry.id ?? uuidv4(); const timestamp = entry.timestamp ?? new Date().toISOString(); const compactProviderResponse = compactStructuredStreamPayload(entry.provider_response); const compactClientResponse = compactStructuredStreamPayload(entry.client_response); db.prepare( ` INSERT INTO request_detail_logs (id, call_log_id, timestamp, client_request, translated_request, provider_response, client_response, provider, model, source_format, target_format, duration_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` ).run( id, entry.call_log_id ?? null, timestamp, serializePayloadForStorage(protectPayloadForLog(entry.client_request)), serializePayloadForStorage(protectPayloadForLog(entry.translated_request)), serializePayloadForStorage(protectPayloadForLog(compactProviderResponse)), serializePayloadForStorage(protectPayloadForLog(compactClientResponse)), entry.provider ?? null, entry.model ?? null, entry.source_format ?? null, entry.target_format ?? null, entry.duration_ms ?? 0 ); } /** Fetch detailed logs (latest first) */ export function getRequestDetailLogs(limit = 50, offset = 0): RequestDetailLog[] { if (!requestDetailLogsTableExists()) return []; const db = getDbInstance(); const rows = db .prepare( ` SELECT * FROM request_detail_logs ORDER BY timestamp DESC LIMIT ? OFFSET ? ` ) .all(limit, offset) as Array>; return rows.map(mapDetailedLogRow); } /** Get a single detailed log by ID */ export function getRequestDetailLogById(id: string): RequestDetailLog | null { if (!requestDetailLogsTableExists()) return null; const db = getDbInstance(); const row = db.prepare("SELECT * FROM request_detail_logs WHERE id = ?").get(id) as | Record | undefined; return row ? mapDetailedLogRow(row) : null; } /** Get the most recent detailed log for a call log ID */ export function getRequestDetailLogByCallLogId(callLogId: string): RequestDetailLog | null { if (!requestDetailLogsTableExists()) return null; const db = getDbInstance(); const row = db .prepare( ` SELECT * FROM request_detail_logs WHERE call_log_id = ? ORDER BY timestamp DESC LIMIT 1 ` ) .get(callLogId) as Record | undefined; return row ? mapDetailedLogRow(row) : null; } /** Get total count of detailed logs */ export function getRequestDetailLogCount(): number { if (!requestDetailLogsTableExists()) return 0; const db = getDbInstance(); const row = db.prepare("SELECT COUNT(*) as cnt FROM request_detail_logs").get() as { cnt: number; }; return row?.cnt ?? 0; } function mapDetailedLogRow(row: Record): RequestDetailLog { return { id: typeof row.id === "string" ? row.id : undefined, call_log_id: typeof row.call_log_id === "string" ? row.call_log_id : null, timestamp: typeof row.timestamp === "string" ? row.timestamp : undefined, client_request: parseStoredPayload(row.client_request), translated_request: parseStoredPayload(row.translated_request), provider_response: parseStoredPayload(row.provider_response), client_response: parseStoredPayload(row.client_response), provider: typeof row.provider === "string" ? row.provider : null, model: typeof row.model === "string" ? row.model : null, source_format: typeof row.source_format === "string" ? row.source_format : null, target_format: typeof row.target_format === "string" ? row.target_format : null, duration_ms: typeof row.duration_ms === "number" ? row.duration_ms : 0, }; }