feat(logs): route coco wallet-engine diagnostics to separate coco-logs

Refactor the file logger into a createLogger(logsDir) factory so the
daemon can maintain two independent sinks: the main daemon logs at
~/.routstrd/logs/ and the Cashu wallet-engine (coco) diagnostics at
~/.routstrd/coco-logs/.

Coco diagnostics were polluting the main daemon logs even though their
metadata is already scrubbed through the allowlist. Writing them to a
separate directory keeps the main logs clean while retaining startup
counts and operation IDs for debugging.

- src/utils/logger.ts: add COCO_LOGS_DIR, FileLogger interface, and
  createLogger(); export logger and cocoLogger
- src/daemon/wallet/coco-client.ts: log coco diagnostics via cocoLogger
- src/cli.ts: add -c/--coco flag to the logs command to view wallet
  engine logs; make getLogFileForDate dir-aware
- src/start-daemon.ts: drop unused existsSync import
- SKILL.md: document the new flag and coco-logs directory
This commit is contained in:
redshift
2026-08-13 21:05:28 +01:00
parent 970206df5b
commit 35fdd0e8ca
5 changed files with 47 additions and 38 deletions
+4 -2
View File
@@ -191,9 +191,10 @@ View daemon logs.
| Option | Default | Description |
|--------|---------|-------------|
| `-f, --follow` | false | Follow log output (like `tail -f`) |
| `-c, --coco` | false | Show Cashu wallet-engine (coco) logs instead of daemon logs |
| `-n, --lines <number>` | 50 | Number of lines to show |
Log files are stored at `~/.routstrd/logs/YYYY-MM-DD.log`.
Log files are stored at `~/.routstrd/logs/YYYY-MM-DD.log`. Wallet-engine (Cashu/coco) diagnostics go to a separate `~/.routstrd/coco-logs/YYYY-MM-DD.log` so they don't pollute the main daemon logs.
## Wallet Commands
@@ -317,4 +318,5 @@ When `routstrd onboard` runs, it automatically configures a `routstr` provider i
| `~/.routstrd/routstr.db` | SQLite database |
| `~/.routstrd/routstrd.sock` | IPC socket |
| `~/.routstrd/routstrd.pid` | PID file |
| `~/.routstrd/logs/YYYY-MM-DD.log` | Daily log files |
| `~/.routstrd/logs/YYYY-MM-DD.log` | Daily daemon log files |
| `~/.routstrd/coco-logs/YYYY-MM-DD.log` | Daily Cashu wallet-engine (coco) log files |
+9 -7
View File
@@ -25,7 +25,7 @@ import {
LOGS_DIR,
type RoutstrdConfig,
} from "./utils/config";
import { logger } from "./utils/logger";
import { COCO_LOGS_DIR, logger } from "./utils/logger";
import { setupIntegration, runIntegrationsForClients } from "./integrations";
import { stopLegacyCocod } from "./daemon/wallet/coco-client";
import { getClientsList } from "./utils/clients";
@@ -1948,11 +1948,11 @@ program
});
// Logs
function getLogFileForDate(date: Date = new Date()): string {
function getLogFileForDate(logsDir: string, date: Date = new Date()): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${LOGS_DIR}/${year}-${month}-${day}.log`;
return `${logsDir}/${year}-${month}-${day}.log`;
}
function readLastLines(file: string, lines: number): string {
@@ -1993,17 +1993,19 @@ program
.command("logs")
.description("View daemon logs")
.option("-f, --follow", "Follow log output", false)
.option("-c, --coco", "Show Cashu wallet-engine (coco) logs instead of daemon logs", false)
.option("-n, --lines <number>", "Number of lines to show", "50")
.action(async (options: { follow: boolean; lines: string }) => {
.action(async (options: { follow: boolean; lines: string; coco: boolean }) => {
await requireLocalDaemon();
const todayFile = getLogFileForDate();
const logsDir = options.coco ? COCO_LOGS_DIR : LOGS_DIR;
const todayFile = getLogFileForDate(logsDir);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayFile = getLogFileForDate(yesterday);
const yesterdayFile = getLogFileForDate(logsDir, yesterday);
if (!existsSync(todayFile) && !existsSync(yesterdayFile)) {
console.log("No log files found. Daemon may not have started yet.");
console.log(`Logs directory: ${LOGS_DIR}`);
console.log(`Logs directory: ${logsDir}`);
process.exit(1);
}
+4 -3
View File
@@ -30,7 +30,7 @@ import type {
NpcAddress,
NpcUsernameResult,
} from "./cocod-client";
import { logger } from "../../utils/logger";
import { cocoLogger, logger } from "../../utils/logger";
const NPC_DEFAULT_BASE_URL = "https://npubx.cash";
@@ -148,9 +148,10 @@ function createCocoLogger(bindings: Record<string, unknown> = {}): CocoLogger {
) => {
// Coco diagnostics may contain proof secrets or encoded tokens. Keep only
// an explicit metadata allowlist; startup counts and operation IDs remain
// useful without copying wallet material into routstrd's logs.
// useful without copying wallet material into routstrd's logs. Written to
// ~/.routstrd/coco-logs/ so wallet-engine noise stays out of the main logs.
const metadata = safeCocoMetadata([bindings, ...meta]);
logger[level](
cocoLogger[level](
`[coco] ${message}`,
...(Object.keys(metadata).length > 0 ? [metadata] : []),
);
-1
View File
@@ -9,7 +9,6 @@ import { logger } from "./utils/logger";
import { CONFIG_DIR, LOGS_DIR } from "./utils/config";
import { withCrossProcessLock } from "./utils/process-lock";
import { fileURLToPath } from "url";
import { existsSync } from "fs";
const DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`;
const DEBUG_LOG_PATH = `${CONFIG_DIR}/debug.log`;
+30 -25
View File
@@ -4,25 +4,27 @@ import { join } from "path";
const HOME = process.env.HOME || process.env.USERPROFILE || "";
const LOG_DIR = process.env.ROUTSTRD_DIR || `${HOME}/.routstrd`;
const LOGS_DIR = join(LOG_DIR, "logs");
/** Wallet-engine (coco-core / Cashu) diagnostics land in their own directory. */
export const COCO_LOGS_DIR = join(LOG_DIR, "coco-logs");
function getLogFileForDate(date: Date = new Date()): string {
function getLogFileForDate(logsDir: string, date: Date = new Date()): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return join(LOGS_DIR, `${year}-${month}-${day}.log`);
return join(logsDir, `${year}-${month}-${day}.log`);
}
function ensureLogDir() {
if (!existsSync(LOGS_DIR)) {
mkdirSync(LOGS_DIR, { recursive: true });
function ensureDir(dir: string) {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
// NOTE: writes are synchronous on purpose — the daemon calls process.exit()
// right after logger.error(...) on fatal paths, and async writes were being
// silently dropped, making startup failures invisible.
function writeLog(level: string, ...args: unknown[]) {
ensureLogDir();
function writeLog(logsDir: string, level: string, ...args: unknown[]) {
ensureDir(logsDir);
const timestamp = new Date().toISOString();
const message = args
.map((a) => {
@@ -40,7 +42,7 @@ function writeLog(level: string, ...args: unknown[]) {
})
.join(" ");
const line = `[${timestamp}] [${level}] ${message}\n`;
const logFile = getLogFileForDate(new Date(timestamp));
const logFile = getLogFileForDate(logsDir, new Date(timestamp));
try {
appendFileSync(logFile, line);
} catch (error) {
@@ -48,20 +50,23 @@ function writeLog(level: string, ...args: unknown[]) {
}
}
export const logger = {
log: (...args: unknown[]) => {
writeLog("INFO", ...args);
},
debug: (...args: unknown[]) => {
writeLog("DEBUG", ...args);
},
warn: (...args: unknown[]) => {
writeLog("WARN", ...args);
},
error: (...args: unknown[]) => {
writeLog("ERROR", ...args);
},
info: (...args: unknown[]) => {
writeLog("INFO", ...args);
},
};
export interface FileLogger {
log: (...args: unknown[]) => void;
debug: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
info: (...args: unknown[]) => void;
}
function createLogger(logsDir: string): FileLogger {
return {
log: (...args) => writeLog(logsDir, "INFO", ...args),
debug: (...args) => writeLog(logsDir, "DEBUG", ...args),
warn: (...args) => writeLog(logsDir, "WARN", ...args),
error: (...args) => writeLog(logsDir, "ERROR", ...args),
info: (...args) => writeLog(logsDir, "INFO", ...args),
};
}
export const logger = createLogger(LOGS_DIR);
export const cocoLogger = createLogger(COCO_LOGS_DIR);