Add support bundle and trace tooling
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { addLogListener, removeLogListener } from "./logger";
|
||||
import type { SupportTraceConfig } from "../shared/types";
|
||||
|
||||
type TraceLevel = "INFO" | "WARN" | "ERROR";
|
||||
|
||||
const TRACE_LOG_FLUSH_INTERVAL_MS = 200;
|
||||
const TRACE_CONFIG_FILE = "trace_config.json";
|
||||
|
||||
const DEFAULT_TRACE_CONFIG: SupportTraceConfig = {
|
||||
enabled: false,
|
||||
includeMainLog: true,
|
||||
includeAudit: true,
|
||||
logDebugRequests: true,
|
||||
updatedAt: new Date(0).toISOString()
|
||||
};
|
||||
|
||||
let traceLogPath: string | null = null;
|
||||
let traceConfigPath: string | null = null;
|
||||
let traceConfig: SupportTraceConfig = { ...DEFAULT_TRACE_CONFIG };
|
||||
let pendingLines: string[] = [];
|
||||
let flushTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
function sanitizeFieldValue(value: unknown): string {
|
||||
if (value === undefined || value === null) {
|
||||
return "";
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value.replace(/\r?\n/g, "\\n");
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value).replace(/\r?\n/g, "\\n");
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function formatFields(fields?: Record<string, unknown>): string {
|
||||
if (!fields) {
|
||||
return "";
|
||||
}
|
||||
const parts = Object.entries(fields)
|
||||
.filter(([, value]) => value !== undefined && value !== null && sanitizeFieldValue(value) !== "")
|
||||
.map(([key, value]) => `${key}=${sanitizeFieldValue(value)}`);
|
||||
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
||||
}
|
||||
|
||||
function flushPending(): void {
|
||||
if (!traceLogPath || pendingLines.length === 0) {
|
||||
return;
|
||||
}
|
||||
const chunk = pendingLines.join("");
|
||||
pendingLines = [];
|
||||
try {
|
||||
fs.appendFileSync(traceLogPath, chunk, "utf8");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleFlush(): void {
|
||||
if (flushTimer) {
|
||||
return;
|
||||
}
|
||||
flushTimer = setTimeout(() => {
|
||||
flushTimer = null;
|
||||
flushPending();
|
||||
}, TRACE_LOG_FLUSH_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function appendTraceLine(line: string): void {
|
||||
if (!traceLogPath) {
|
||||
return;
|
||||
}
|
||||
pendingLines.push(line);
|
||||
scheduleFlush();
|
||||
}
|
||||
|
||||
function normalizeTraceConfig(raw: unknown): SupportTraceConfig {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
return { ...DEFAULT_TRACE_CONFIG };
|
||||
}
|
||||
const value = raw as Partial<SupportTraceConfig>;
|
||||
return {
|
||||
enabled: Boolean(value.enabled),
|
||||
includeMainLog: value.includeMainLog === undefined ? DEFAULT_TRACE_CONFIG.includeMainLog : Boolean(value.includeMainLog),
|
||||
includeAudit: value.includeAudit === undefined ? DEFAULT_TRACE_CONFIG.includeAudit : Boolean(value.includeAudit),
|
||||
logDebugRequests: value.logDebugRequests === undefined ? DEFAULT_TRACE_CONFIG.logDebugRequests : Boolean(value.logDebugRequests),
|
||||
updatedAt: typeof value.updatedAt === "string" && value.updatedAt.trim()
|
||||
? value.updatedAt
|
||||
: DEFAULT_TRACE_CONFIG.updatedAt
|
||||
};
|
||||
}
|
||||
|
||||
function loadTraceConfig(): SupportTraceConfig {
|
||||
if (!traceConfigPath) {
|
||||
return { ...DEFAULT_TRACE_CONFIG };
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(traceConfigPath, "utf8")) as unknown;
|
||||
return normalizeTraceConfig(parsed);
|
||||
} catch {
|
||||
return { ...DEFAULT_TRACE_CONFIG };
|
||||
}
|
||||
}
|
||||
|
||||
function persistTraceConfig(): void {
|
||||
if (!traceConfigPath) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
fs.writeFileSync(traceConfigPath, `${JSON.stringify(traceConfig, null, 2)}\n`, "utf8");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const mainLogListener = (line: string): void => {
|
||||
if (!traceConfig.enabled || !traceConfig.includeMainLog) {
|
||||
return;
|
||||
}
|
||||
appendTraceLine(line);
|
||||
};
|
||||
|
||||
export function initTraceLog(baseDir: string): void {
|
||||
traceLogPath = path.join(baseDir, "trace.log");
|
||||
traceConfigPath = path.join(baseDir, TRACE_CONFIG_FILE);
|
||||
try {
|
||||
fs.mkdirSync(baseDir, { recursive: true });
|
||||
if (!fs.existsSync(traceLogPath)) {
|
||||
fs.writeFileSync(traceLogPath, "", "utf8");
|
||||
}
|
||||
traceConfig = loadTraceConfig();
|
||||
persistTraceConfig();
|
||||
fs.appendFileSync(traceLogPath, `=== Trace-Log Start: ${new Date().toISOString()} ===\n`, "utf8");
|
||||
} catch {
|
||||
traceLogPath = null;
|
||||
traceConfigPath = null;
|
||||
traceConfig = { ...DEFAULT_TRACE_CONFIG };
|
||||
return;
|
||||
}
|
||||
addLogListener(mainLogListener);
|
||||
}
|
||||
|
||||
export function getTraceLogPath(): string | null {
|
||||
if (!traceLogPath) {
|
||||
return null;
|
||||
}
|
||||
return fs.existsSync(traceLogPath) ? traceLogPath : null;
|
||||
}
|
||||
|
||||
export function getTraceConfigPath(): string | null {
|
||||
if (!traceConfigPath) {
|
||||
return null;
|
||||
}
|
||||
return fs.existsSync(traceConfigPath) ? traceConfigPath : null;
|
||||
}
|
||||
|
||||
export function getTraceConfig(): SupportTraceConfig {
|
||||
return { ...traceConfig };
|
||||
}
|
||||
|
||||
export function updateTraceConfig(patch: Partial<SupportTraceConfig>): SupportTraceConfig {
|
||||
traceConfig = normalizeTraceConfig({
|
||||
...traceConfig,
|
||||
...patch,
|
||||
updatedAt: new Date().toISOString()
|
||||
});
|
||||
persistTraceConfig();
|
||||
appendTraceLine(`${new Date().toISOString()} [INFO] [trace] Konfiguration aktualisiert${formatFields(traceConfig)}\n`);
|
||||
return getTraceConfig();
|
||||
}
|
||||
|
||||
export function setTraceEnabled(enabled: boolean, note = ""): SupportTraceConfig {
|
||||
const next = updateTraceConfig({ enabled });
|
||||
appendTraceLine(`${new Date().toISOString()} [INFO] [trace] Support-Trace ${enabled ? "aktiviert" : "deaktiviert"}${formatFields({ note })}\n`);
|
||||
return next;
|
||||
}
|
||||
|
||||
export function logTraceEvent(
|
||||
level: TraceLevel,
|
||||
category: string,
|
||||
message: string,
|
||||
fields?: Record<string, unknown>
|
||||
): void {
|
||||
if (!traceConfig.enabled) {
|
||||
return;
|
||||
}
|
||||
if (category === "audit" && !traceConfig.includeAudit) {
|
||||
return;
|
||||
}
|
||||
appendTraceLine(`${new Date().toISOString()} [${level}] [${category}] ${message}${formatFields(fields)}\n`);
|
||||
}
|
||||
|
||||
export function shutdownTraceLog(): void {
|
||||
removeLogListener(mainLogListener);
|
||||
if (!traceLogPath) {
|
||||
return;
|
||||
}
|
||||
if (flushTimer) {
|
||||
clearTimeout(flushTimer);
|
||||
flushTimer = null;
|
||||
}
|
||||
flushPending();
|
||||
try {
|
||||
fs.appendFileSync(traceLogPath, `=== Trace-Log Ende: ${new Date().toISOString()} ===\n`, "utf8");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
traceLogPath = null;
|
||||
traceConfigPath = null;
|
||||
traceConfig = { ...DEFAULT_TRACE_CONFIG };
|
||||
}
|
||||
Reference in New Issue
Block a user