Observability
Request correlation ids, structured end-to-end logging, and the built-in metrics endpoint.
zero-memory emits structured JSON logs with an end-to-end correlation id on every request, and exposes a minimal Prometheus-compatible metrics endpoint — with zero observability dependencies. Every log line produced while serving a request, from the HTTP edge down to the deepest service call, carries the same requestId, so a single grep reconstructs the full story of any request.
Why it exists
Without a correlation id, a call to an MCP tool cannot be connected to the logs of the services it fans out to — persistence, extraction, search. Retrofitting correlation later means revisiting every handler, so it is wired into the platform's foundation instead. The request id is also the join key that downstream records rely on: usage events and the audit log both reference it.
How it works
Request ids
Every HTTP request to the server is assigned a request id:
- The id is a branded, schema-validated identifier of the form
req_<random>.<timestamp>, following the same entity-id convention as the platform's other identifiers. - An incoming
X-Request-Idheader is parsed, not trusted: if it passes the schema it is adopted, otherwise a fresh id is minted. - The response always echoes
X-Request-Id, so clients can quote the id when reporting a problem.
Context propagation
The request id travels through the call tree via AsyncLocalStorage — no function signatures change, no parameter threading. A single shared request context is the one source of truth for the id; the logger does not own a second copy. Instead, the logger accepts an injected context resolver, and its write path automatically stamps the current requestId onto every record. The logger itself stays a dependency-free leaf.
The context boundary is one thin outer HTTP handler that wraps all routes: it establishes the context, measures duration, increments counters, and echoes the header. On completion, every request produces one structured log record with method, path, status, duration_ms, and requestId; failures log at error level with the stack.
The watcher daemon uses the same mechanism with a per-cycle runId, so all chunks processed within one pass are linkable.
Metrics
Metrics are deliberately minimal — in-process counters with no external dependencies:
http_requests_total{status}andhttp_errors_total, incremented at the HTTP boundary.mcp_tool_calls_total{tool}, incremented per MCP tool invocation over HTTP through an optional hook in the MCP layer (the stdio transport does not report it).promoted_rules_lookup_failures_total, incremented when rule lookup fails open during MCP initialization. Any non-zero increase means a client connected without its expected standing-rule delivery and should alert.mcp_write_refused_total{reason}, incremented when arememberis refused for a missing or unresolvable target. Thereasonlabel is one of the two stable refusal reasons (scope_target_required,project_hint_unresolvable— see Contracts and errors). This is the designed alerting signal for agents stuck without a project: a flat line is healthy, a climbing one means sessions keep hitting the same wall instead of attaching. Reported on the HTTP transport, like the tool-call counter.
GET /metrics renders the counters as text/plain in the Prometheus exposition format, so any standard scraper can consume them. Counters are in-process and monotonic for the process lifetime — a restart resets them to zero, so alert on rates and increases, never on absolute values.
Duration of expensive operations
The known-slow paths — the extraction model call, embedding generation, and hybrid search — are wrapped in timing at exactly one place each, logging duration_ms alongside the request id. Latency questions ("was it the model or the database?") are answerable from logs alone.
Health endpoints and build identity
The server answers GET /healthz (liveness) and GET /readyz (readiness).
Readiness flips to 503 only when the database is unreachable — a cold
embedder never gates it, so a scale-up does not flap. /healthz also states
which build is answering: the running image's version
(v<semver>+<commit>) and, separately, the checkout revision the compose
files came from. A host carries those two identities independently — the
image it runs and the checkout that configured it — and they advance
separately, so an incident report can name both.
The dashboard has its own GET /healthz, answering 503 when the
dashboard cannot reach the MCP server. That web-to-server leg is the one
that has failed in a real deployment while every outside check stayed green
— monitor it as a first-class probe, not as an afterthought.
stdio safety
When the MCP server runs over stdio, stdout must carry only protocol frames. Setting LOG_STDERR=1 routes all log output to stderr; the observability layer preserves this behavior unconditionally. Tool payloads are never logged in full, so request bodies and secrets do not end up in log storage.
How to use it
- Trace a request: take the
X-Request-Idfrom a response (or from a client error report) and filter logs by that value — you get the edge record plus every nested service record for that request. - Pass your own id: send a valid
X-Request-Idon the request to propagate a correlation id from your own system; an invalid one is silently replaced with a freshly minted id. - Scrape metrics: point Prometheus (or
curl) atGET /metricson the server. The endpoint is unauthenticated and carries no user data (counts and tool names only), but the shipped production edge configurations deliberately answer404for/metrics— scrape it from inside the deployment's network instead, or remove that block and put your own protection in front of it. - Alert on rule-delivery failures: track increases in
promoted_rules_lookup_failures_total; the request itself remains available by design, so logs and this counter are the visible failure signal. - Run over stdio: set
LOG_STDERR=1so logs never corrupt the protocol stream.
Design notes
- No OpenTelemetry — deliberately. A full tracing SDK is the right end state for a large deployment, but at the platform's current scale its dependency weight is not justified: structured logs plus counters cover the operational need, and the correlation-id discipline means a later move to real tracing is an upgrade, not a rewrite.
AsyncLocalStorageover parameter threading. PassingrequestIdexplicitly through every signature is precisely the invasive retrofit this design avoids; ALS attaches context to the execution flow without touching any interface.- One context, not two. Keeping the correlation id in a single shared context and injecting a resolver into the logger avoids two independently-updated sources of truth that would inevitably drift.