A scheduled refresh runs every 21 minutes and most of what it printed was
identical every pass. On a real daemon (20,746 log lines/day) the refresh block
was ~10% of the file: four INFO phase lines per pass plus one WARN per
unreachable provider, with the same 26-provider failure set repeated across
consecutive passes.
Three changes, one per source of noise:
1. `ROUTSTRD_LOG_LEVEL` (default `info`) makes the level meaningful. `writeLog`
wrote every level unconditionally, so the SDK's `debug` channel was not
actually filterable — which is what blocked moving recurring detail down a
level. `ROUTSTRD_LOG_LEVEL=debug` restores everything; an unparseable value
falls back to the default instead of muting the log. `error` is never
suppressed. The Cashu wallet-engine sink keeps `debug` under its own
`ROUTSTRD_COCO_LOG_LEVEL`, because it is ~87% debug by design and an explicit
`ROUTSTRD_LOG_LEVEL=info` must not silently drop wallet detail.
2. The refresh pass collapses into one line:
[INFO] Scheduled refresh: 214 models, 3 client integration(s) in 33.4s
The phase markers ("Running scheduled Nostr event refresh...", "Running
scheduled model refresh...", "Refreshing 3 client integration(s)...", "Client
integrations refreshed.") move to `debug`. The caller owns the line and
measures the whole pass (Nostr events + models + integrations), so
`refreshModelsAndIntegrations` now returns a result instead of logging.
Integration failures are counted into the line, and `runIntegrationsForClients`
reports them via `logger.error` instead of `console.error` so they land in the
dated log with a level.
3. `Using existing API key for <client>` moves to `debug`. That is the steady
state for an already-configured client, and the `*/5 * * * * routstrd clients
add --pi-agent` cron printed it 290 times/day. `Created new API key` stays at
`info`.
The provider warnings themselves still come from @routstr/sdk 0.4.9 one per
provider; Routstr/routstr-sdk#89 replaces them with a transition-based summary,
and this level filter is what lets that detail stay available at `debug`.
Nothing parsed these strings: `routstrd logs` only tails and filters lines by
request id.
Tests: src/utils/logger.test.ts (level parsing, filtering, argument passthrough,
error never suppressed) and src/integrations/index.test.ts (summary formatting).
Verified end-to-end against a temp ROUTSTRD_DIR that each level writes exactly
the expected lines.
2.8 KiB
Logging
The daemon appends to dated files under ~/.routstrd/logs/<YYYY-MM-DD>.log
(one per local day) and to ~/.routstrd/coco-logs/ for Cashu wallet-engine
diagnostics. Writes are synchronous on purpose: the daemon calls
process.exit() right after logger.error(...) on fatal paths, and async
writes were silently dropped, making startup failures invisible.
Levels
ROUTSTRD_LOG_LEVEL sets the minimum level written to disk:
| Value | Written |
|---|---|
debug |
everything, including per-provider and per-pass diagnostics |
info (default) |
info, warn, error |
warn |
warn, error |
error |
error |
An unparseable value falls back to info rather than muting the log.
The daemon repeats work on timers, so anything that would otherwise print once
per pass is emitted at debug and summarized into a single info line. A
scheduled refresh therefore costs one line by default:
[2026-10-05T15:42:27.044Z] [INFO] Scheduled refresh: 214 models, 3 client integration(s) in 33.4s
Set ROUTSTRD_LOG_LEVEL=debug to see the phases (Running scheduled model refresh...), the per-provider detail behind a failure summary, and the
Nostr-discovery markers. The daemon inherits the variable from whatever starts
it (routstrd start, pm2, systemd), so set it there, not per request.
coco-logs is the Cashu wallet-engine sink and writes debug regardless of
ROUTSTRD_LOG_LEVEL, because those diagnostics are verbose by design (roughly
87% of those lines are debug) and live in their own directory precisely so
they stay out of the main log. ROUTSTRD_COCO_LOG_LEVEL is its own knob, using
the same values, if you do want it quieter (it is ~19k lines/day on a busy
daemon).
What a healthy pass looks like
$ ROUTSTRD_LOG_LEVEL=info
[INFO] Routstr daemon listening on http://127.0.0.1:8008/v1
[INFO] Initial refresh: 214 models, 3 client integration(s) in 41.2s
[INFO] Model refresh: 32/58 providers ok, 26 unavailable (0 new, 26 unchanged) in 9.8s — maebarai.xyz(530), api.voltai.top(connect), +20 more
[INFO] Scheduled refresh: 214 models, 3 client integration(s) in 33.4s
The Model refresh: line comes from the SDK (@routstr/sdk): it reports
reachability as state transitions plus one bounded summary, so a provider that
has been down for days is a count, not a repeated warning. A provider that newly
fails — or starts failing differently — still gets its own [WARN] Provider <url> unreachable: ... line, so anything worth alerting on stays greppable.
Inspecting
routstrd logs -n 50 # last 50 lines
routstrd logs -f # follow (from debug.log, the daemon's stdout)
routstrd logs -c # Cashu wallet-engine log instead
ROUTSTRD_LOG_LEVEL=debug routstrd start
ROUTSTRD_COCO_LOG_LEVEL=info routstrd start