init
This commit is contained in:
139
server/src/lib/support-log.ts
Normal file
139
server/src/lib/support-log.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user