diff --git a/src/index.ts b/src/index.ts index 408b94a89..dd4a76ea2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { autoReplyIfConfigured, getReplyFromConfig, } from "./auto-reply/reply.js"; +import { enableConsoleCapture } from "./logging.js"; import { applyTemplate } from "./auto-reply/templating.js"; import { createDefaultDeps, monitorTwilio } from "./cli/deps.js"; import { promptYesNo } from "./cli/prompt.js"; @@ -56,6 +57,9 @@ import { assertProvider, normalizeE164, toWhatsappJid } from "./utils.js"; dotenv.config({ quiet: true }); +// Capture all console output into pino logs while keeping stdout/stderr behavior. +enableConsoleCapture(); + import { buildProgram } from "./cli/program.js"; const program = buildProgram(); diff --git a/src/logging.ts b/src/logging.ts index 85a8c26ae..d9158231b 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import util from "node:util"; import pino, { type Bindings, type LevelWithSilent, type Logger } from "pino"; import { loadConfig, type WarelayConfig } from "./config/config.js"; @@ -37,9 +38,10 @@ export type LoggerResolvedSettings = ResolvedSettings; let cachedLogger: Logger | null = null; let cachedSettings: ResolvedSettings | null = null; let overrideSettings: LoggerSettings | null = null; +let consolePatched = false; function normalizeLevel(level?: string): LevelWithSilent { - if (isVerbose()) return "debug"; + if (isVerbose()) return "trace"; const candidate = level ?? "info"; return ALLOWED_LEVELS.includes(candidate as LevelWithSilent) ? (candidate as LevelWithSilent) @@ -113,6 +115,58 @@ export function resetLogger() { overrideSettings = null; } +/** + * Route console.* calls through pino while still emitting to stdout/stderr. + * This keeps user-facing output unchanged but guarantees every console call is captured in log files. + */ +export function enableConsoleCapture(): void { + if (consolePatched) return; + consolePatched = true; + + const logger = getLogger(); + + const original = { + log: console.log, + info: console.info, + warn: console.warn, + error: console.error, + debug: console.debug, + trace: console.trace, + }; + + const forward = + (level: LevelWithSilent, orig: (...args: unknown[]) => void) => + (...args: unknown[]) => { + const formatted = util.format(...args); + try { + // Map console levels to pino + if (level === "trace") { + logger.trace(formatted); + } else if (level === "debug") { + logger.debug(formatted); + } else if (level === "info") { + logger.info(formatted); + } else if (level === "warn") { + logger.warn(formatted); + } else if (level === "error" || level === "fatal") { + logger.error(formatted); + } else { + logger.info(formatted); + } + } catch { + // never block console output on logging failures + } + orig.apply(console, args as []); + }; + + console.log = forward("info", original.log); + console.info = forward("info", original.info); + console.warn = forward("warn", original.warn); + console.error = forward("error", original.error); + console.debug = forward("debug", original.debug); + console.trace = forward("trace", original.trace); +} + function defaultRollingPathForToday(): string { const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD return path.join(DEFAULT_LOG_DIR, `${LOG_PREFIX}-${today}${LOG_SUFFIX}`);