140 lines
4.1 KiB
TypeScript
140 lines
4.1 KiB
TypeScript
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<string, unknown>) {
|
|
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<string, unknown>;
|
|
}) {
|
|
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;
|
|
}
|