import type { SupportLogEntryDto, SupportLogFiltersDto, SupportLogListDto, SupportLogSummaryDto } from "@mrp/shared"; import { randomUUID } from "node:crypto"; const SUPPORT_LOG_LIMIT = 500; const SUPPORT_LOG_RETENTION_DAYS = 14; const supportLogs: SupportLogEntryDto[] = []; function serializeContext(context?: Record) { if (!context) { return "{}"; } try { return JSON.stringify(context); } catch { return JSON.stringify({ serializationError: "Unable to serialize support log context." }); } } function getRetentionCutoff(now = new Date()) { return new Date(now.getTime() - SUPPORT_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000); } function pruneSupportLogs(now = new Date()) { const cutoff = getRetentionCutoff(now).getTime(); const retained = supportLogs.filter((entry) => new Date(entry.createdAt).getTime() >= cutoff); supportLogs.length = 0; supportLogs.push(...retained.slice(0, SUPPORT_LOG_LIMIT)); } function normalizeFilters(filters?: SupportLogFiltersDto): SupportLogFiltersDto { return { level: filters?.level, source: filters?.source?.trim() || undefined, query: filters?.query?.trim() || undefined, start: filters?.start, end: filters?.end, limit: filters?.limit, }; } function filterSupportLogs(filters?: SupportLogFiltersDto) { pruneSupportLogs(); const normalized = normalizeFilters(filters); const startMs = normalized.start ? new Date(normalized.start).getTime() : null; const endMs = normalized.end ? new Date(normalized.end).getTime() : null; const query = normalized.query?.toLowerCase(); const limit = Math.max(0, Math.min(normalized.limit ?? 100, SUPPORT_LOG_LIMIT)); return supportLogs .filter((entry) => { if (normalized.level && entry.level !== normalized.level) { return false; } if (normalized.source && entry.source !== normalized.source) { return false; } const createdAtMs = new Date(entry.createdAt).getTime(); if (startMs != null && createdAtMs < startMs) { return false; } if (endMs != null && createdAtMs > endMs) { return false; } if (!query) { return true; } return [entry.source, entry.message, entry.contextJson].some((value) => value.toLowerCase().includes(query)); }) .slice(0, limit); } function buildSupportLogSummary(entries: SupportLogEntryDto[], totalCount: number, availableSources: string[]): SupportLogSummaryDto { return { totalCount, filteredCount: entries.length, sourceCount: availableSources.length, retentionDays: SUPPORT_LOG_RETENTION_DAYS, oldestEntryAt: entries.length > 0 ? entries[entries.length - 1]?.createdAt ?? null : null, newestEntryAt: entries.length > 0 ? entries[0]?.createdAt ?? null : null, levelCounts: { INFO: entries.filter((entry) => entry.level === "INFO").length, WARN: entries.filter((entry) => entry.level === "WARN").length, ERROR: entries.filter((entry) => entry.level === "ERROR").length, }, }; } export function recordSupportLog(entry: { level: SupportLogEntryDto["level"]; source: string; message: string; context?: Record; }) { pruneSupportLogs(); supportLogs.unshift({ id: randomUUID(), level: entry.level, source: entry.source, message: entry.message, contextJson: serializeContext(entry.context), createdAt: new Date().toISOString(), }); if (supportLogs.length > SUPPORT_LOG_LIMIT) { supportLogs.length = SUPPORT_LOG_LIMIT; } } export function listSupportLogs(filters?: SupportLogFiltersDto): SupportLogListDto { pruneSupportLogs(); const normalized = normalizeFilters(filters); const availableSources = [...new Set(supportLogs.map((entry) => entry.source))].sort(); const entries = filterSupportLogs(normalized); return { entries, summary: buildSupportLogSummary(entries, supportLogs.length, availableSources), availableSources, filters: normalized, }; } export function getSupportLogCount() { pruneSupportLogs(); return supportLogs.length; } export function getSupportLogRetentionDays() { return SUPPORT_LOG_RETENTION_DAYS; }