real-debrid-downloader/src/main/trace-log.ts
Sucukdeluxe 3ed3877ac9 chore: remove all source code comments and internal artifacts
Strip every comment from the source (parsed with the TypeScript compiler so
strings, template literals, regex literals and JSX are never touched), and drop
internal/working artifacts that do not belong in the public repository
(design mockups, internal analysis docs, a stray backup file and an old log).
No functional change: build is green, the full test suite passes.
2026-06-06 04:53:54 +02:00

313 lines
8.7 KiB
TypeScript

import fs from "node:fs";
import { logTimestamp } from "./log-timestamp";
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 TRACE_LOG_MAX_FILE_BYTES = Number(process.env.RD_TRACE_LOG_MAX_BYTES || 10 * 1024 * 1024);
const TRACE_LOG_RETENTION_DAYS = Number(process.env.RD_TRACE_LOG_RETENTION_DAYS || 30);
const TRACE_DEFAULT_AUTO_DISABLE_MS = Number(process.env.RD_TRACE_AUTO_DISABLE_MS || 2 * 60 * 60 * 1000);
const DEFAULT_TRACE_CONFIG: SupportTraceConfig = {
enabled: false,
includeMainLog: true,
includeAudit: true,
logDebugRequests: true,
autoDisableAt: null,
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;
let autoDisableTimer: 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 {
}
}
function rotateIfNeeded(filePath: string): void {
try {
const stat = fs.statSync(filePath);
if (stat.size < TRACE_LOG_MAX_FILE_BYTES) {
return;
}
const backup = `${filePath}.old`;
try {
fs.rmSync(backup, { force: true });
} catch {
}
fs.renameSync(filePath, backup);
} catch {
}
}
function cleanupOldBackup(filePath: string): void {
const backup = `${filePath}.old`;
try {
const stat = fs.statSync(backup);
const cutoff = Date.now() - TRACE_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
if (stat.mtimeMs < cutoff) {
fs.rmSync(backup, { force: true });
}
} catch {
}
}
function scheduleFlush(): void {
if (flushTimer) {
return;
}
flushTimer = setTimeout(() => {
flushTimer = null;
flushPending();
}, TRACE_LOG_FLUSH_INTERVAL_MS);
}
function appendTraceLine(line: string): void {
if (!traceLogPath) {
return;
}
rotateIfNeeded(traceLogPath);
if (!fs.existsSync(traceLogPath)) {
try {
fs.writeFileSync(traceLogPath, "", "utf8");
} catch {
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),
autoDisableAt: typeof value.autoDisableAt === "string" && value.autoDisableAt.trim()
? value.autoDisableAt
: null,
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 {
}
}
const mainLogListener = (line: string): void => {
if (!traceConfig.enabled || !traceConfig.includeMainLog) {
return;
}
appendTraceLine(line);
};
function clearAutoDisableTimer(): void {
if (autoDisableTimer) {
clearTimeout(autoDisableTimer);
autoDisableTimer = null;
}
}
function disableTraceDueToExpiry(): void {
clearAutoDisableTimer();
if (!traceConfig.enabled) {
return;
}
traceConfig = normalizeTraceConfig({
...traceConfig,
enabled: false,
autoDisableAt: null,
updatedAt: logTimestamp()
});
persistTraceConfig();
appendTraceLine(`${logTimestamp()} [INFO] [trace] Support-Trace automatisch deaktiviert | reason=expired\n`);
}
function scheduleAutoDisable(): void {
clearAutoDisableTimer();
if (!traceConfig.enabled || !traceConfig.autoDisableAt) {
return;
}
const until = Date.parse(traceConfig.autoDisableAt);
if (!Number.isFinite(until)) {
return;
}
const remainingMs = until - Date.now();
if (remainingMs <= 0) {
disableTraceDueToExpiry();
return;
}
autoDisableTimer = setTimeout(() => {
autoDisableTimer = null;
disableTraceDueToExpiry();
}, Math.min(remainingMs, 2_147_483_647));
}
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 });
cleanupOldBackup(traceLogPath);
if (!fs.existsSync(traceLogPath)) {
fs.writeFileSync(traceLogPath, "", "utf8");
}
rotateIfNeeded(traceLogPath);
if (!fs.existsSync(traceLogPath)) {
fs.writeFileSync(traceLogPath, "", "utf8");
}
traceConfig = loadTraceConfig();
persistTraceConfig();
fs.appendFileSync(traceLogPath, `=== Trace-Log Start: ${logTimestamp()} ===\n`, "utf8");
} catch {
traceLogPath = null;
traceConfigPath = null;
traceConfig = { ...DEFAULT_TRACE_CONFIG };
return;
}
addLogListener(mainLogListener);
scheduleAutoDisable();
}
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: logTimestamp()
});
persistTraceConfig();
scheduleAutoDisable();
appendTraceLine(`${logTimestamp()} [INFO] [trace] Konfiguration aktualisiert${formatFields(traceConfig as unknown as Record<string, unknown>)}\n`);
return getTraceConfig();
}
export function setTraceEnabled(enabled: boolean, note = "", durationMs: number = TRACE_DEFAULT_AUTO_DISABLE_MS): SupportTraceConfig {
const autoDisableAt = enabled && durationMs > 0
? new Date(Date.now() + durationMs).toISOString()
: null;
const next = updateTraceConfig({ enabled, autoDisableAt });
appendTraceLine(`${logTimestamp()} [INFO] [trace] Support-Trace ${enabled ? "aktiviert" : "deaktiviert"}${formatFields({ note, autoDisableAt })}\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(`${logTimestamp()} [${level}] [${category}] ${message}${formatFields(fields)}\n`);
}
export function shutdownTraceLog(): void {
removeLogListener(mainLogListener);
clearAutoDisableTimer();
if (!traceLogPath) {
return;
}
if (flushTimer) {
clearTimeout(flushTimer);
flushTimer = null;
}
flushPending();
try {
fs.appendFileSync(traceLogPath, `=== Trace-Log Ende: ${logTimestamp()} ===\n`, "utf8");
} catch {
}
traceLogPath = null;
traceConfigPath = null;
traceConfig = { ...DEFAULT_TRACE_CONFIG };
}