From e6ae46fa2136ddca5be618da6c7769e56ca02a9a Mon Sep 17 00:00:00 2001 From: Phantom Date: Fri, 3 Apr 2026 13:19:50 +0000 Subject: [PATCH 1/2] feat: add rotating file logger Adds a simple append-only file logger at data/logs/phantom.log with 10 MB rotation. Never throws - logging silently degrades if the directory can't be created. Designed to be imported once and used as a singleton. Co-Authored-By: Claude Sonnet 4.6 --- src/core/logger.ts | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/core/logger.ts diff --git a/src/core/logger.ts b/src/core/logger.ts new file mode 100644 index 0000000..4c4833c --- /dev/null +++ b/src/core/logger.ts @@ -0,0 +1,68 @@ +/** + * Rotating file logger. Writes to data/logs/phantom.log. + * Rotates to phantom.log.1 when the file exceeds MAX_SIZE_BYTES. + * Designed to never throw - logging must not crash the main process. + */ + +import { appendFileSync, existsSync, mkdirSync, renameSync, statSync } from "node:fs"; +import { dirname, resolve } from "node:path"; + +const MAX_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB + +type LogLevel = "ERROR" | "WARN" | "INFO"; + +export class Logger { + private path: string; + private ready = false; + + constructor(logPath: string) { + this.path = resolve(logPath); + try { + mkdirSync(dirname(this.path), { recursive: true }); + this.ready = true; + } catch { + // Can't create log directory - silently degrade + } + } + + error(tag: string, message: string): void { + this.write("ERROR", tag, message); + } + + warn(tag: string, message: string): void { + this.write("WARN", tag, message); + } + + info(tag: string, message: string): void { + this.write("INFO", tag, message); + } + + getPath(): string { + return this.path; + } + + private write(level: LogLevel, tag: string, message: string): void { + if (!this.ready) return; + try { + this.maybeRotate(); + const line = `${new Date().toISOString()} [${level}] [${tag}] ${message}\n`; + appendFileSync(this.path, line, "utf-8"); + } catch { + // Silently degrade + } + } + + private maybeRotate(): void { + try { + if (!existsSync(this.path)) return; + const { size } = statSync(this.path); + if (size >= MAX_SIZE_BYTES) { + renameSync(this.path, `${this.path}.1`); + } + } catch { + // Ignore rotation errors + } + } +} + +export const logger = new Logger("data/logs/phantom.log"); From bed287b6762444b80b2a2b1c7cc49e3a785d58b1 Mon Sep 17 00:00:00 2001 From: Phantom Date: Sat, 4 Apr 2026 00:56:02 +0000 Subject: [PATCH 2/2] feat: wire rotating file logger into index.ts Intercepts console.error/warn to mirror into the log file so all existing code gets file logging without needing direct imports. Logs the log file path at startup. Co-Authored-By: Claude Sonnet 4.6 --- src/index.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/index.ts b/src/index.ts index a6e0066..172bc57 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { TelegramChannel } from "./channels/telegram.ts"; import { WebhookChannel } from "./channels/webhook.ts"; import { loadChannelsConfig, loadConfig } from "./config/loader.ts"; import { installShutdownHandlers, onShutdown } from "./core/graceful.ts"; +import { logger } from "./core/logger.ts"; import { setChannelHealthProvider, setEvolutionVersionProvider, @@ -53,10 +54,24 @@ import { createSecretToolServer } from "./secrets/tools.ts"; import { setPublicDir, setSecretSavedCallback, setSecretsDb } from "./ui/serve.ts"; import { createWebUiToolServer } from "./ui/tools.ts"; +// Intercept console.error and console.warn to mirror into the log file. +// All existing code benefits without needing to import the logger directly. +const _origError = console.error.bind(console); +const _origWarn = console.warn.bind(console); +console.error = (...args: unknown[]) => { + _origError(...args); + logger.error("console", args.map(String).join(" ")); +}; +console.warn = (...args: unknown[]) => { + _origWarn(...args); + logger.warn("console", args.map(String).join(" ")); +}; + async function main(): Promise { const startedAt = Date.now(); console.log("[phantom] Starting..."); + logger.info("phantom", `Starting - log file: ${logger.getPath()}`); const config = loadConfig(); console.log(`[phantom] Config loaded: ${config.name} (${config.model}, effort: ${config.effort})`);