Status:🤷 UNKNOWN
Date:📅 unknown
Decision Makers:unknown
ADR-0008: Structured logging via charmbracelet/log as an slog.Handler
- Status: Accepted
- Date: 2026-06-27
Context
msgbrowse runs as several long- and short-lived processes — serve, mcp,
signal-import, imessage-import, embed, watch, journal — that all need
leveled, readable logging. Two constraints shape the choice:
mcpspeaks JSON-RPC over stdio. Anything written to stdout corrupts that stream, so logs must go to stderr exclusively.- The code already logs through
log/slog. Handlers across the codebase use the standard*slog.Logger; we want pretty, colorized, leveled output without touching every call site.
Decision
Install charmbracelet/log as the slog.Handler behind the default
*slog.Logger, writing to stderr, and key actionable error hints on sentinel
errors.
- One handler swap, everywhere.
configureLogger(internal/cli/root.go) doesslog.SetDefault(slog.New(newLogHandler(level))). Becausecharmbracelet/logimplementsslog.Handler, every existingslogcall across import/serve/mcp gets colorized, leveled, timestamped output with no call-site changes. - stderr only.
newLogHandlerconstructs the handler withcharmlog.NewWithOptions(os.Stderr, …)— keeping stdout clean for themcpsubcommand's JSON-RPC stream. - Configured twice, by design.
Executeinstalls a prettyinfologger up front so even config-load failures (which happen before per-command config resolution) render nicely;resolveConfigre-installs it at the resolvedlog_levelonce config is known. - Level mapping.
newLogHandlermaps thelog_levelstring (debug/info/warn/error) tocharmloglevels; the level is validated inconfig.Validate. - Actionable error hints on sentinels.
renderError/errorHint(root.go) match failures witherrors.Isagainst sentinel errors (ingest.ErrExportDirNotFound,imessage.ErrArchiveNotFound) and log ahintattribute telling the user the concrete next step (e.g. pointarchive_rootat the folder that containsexport/).mainonly sets the exit code so the error isn't printed twice.
Why these choices
- slog.Handler adapter over a logging facade: the standard library's
slogis already the call-site API; makingcharmbracelet/logthe handler gives styled output for free and keeps the door open to swap the handler (e.g. JSON) without re-touching code. - stderr-only is non-negotiable for MCP: the stdio JSON-RPC transport owns stdout; a single stray stdout log line would break a client. stderr is the only safe sink for a process that may be an MCP server.
- Sentinel-keyed hints over string matching:
errors.Isstays correct as error messages are reworded, and ties the hint to the actual failure mode users hit most (mis-pointed archive roots).
Consequences
Positive
- Consistent, colorized, leveled logs across every subcommand from one place.
mcpover stdio stays correct — logs never pollute the JSON-RPC stream.- The two most common setup mistakes self-explain via
hintattributes.
Negative
- The logger is configured twice (early default, then at resolved level); the
early-
infowindow means very-earlydebuglines won't appear until config resolves. This is intentional but worth knowing. - Output is human-oriented (pretty/colorized) rather than machine-parseable JSON; a future deployment wanting JSON logs would add a handler option.
Operational
- Logs go to stderr; container/process supervisors should capture stderr.
- New user-facing failure modes should get a sentinel error + an
errorHintcase rather than an inline message, to keep hints discoverable.
Alternatives considered
- Plain
slog.NewTextHandler/JSONHandler. Rejected: functional but drab;charmbracelet/loggives leveled color for local, single-user use at the cost of one dependency, and still satisfies theslog.Handlercontract. - A logging facade (zap/zerolog) at call sites. Rejected: the code already
speaks
slog; introducing a second logging API everywhere is churn for no gain. - Logging to stdout (or splitting by level). Rejected: unsafe for the
mcpstdio transport; stderr-only is simpler and correct.
References
internal/cli/root.go(configureLogger,newLogHandler,renderError,errorHint)cmd/msgbrowse/main.go(exit-only, no double print)- 📝 ADR-0004: MCP SDK & RAG (the stdio JSON-RPC stream this protects)