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 the request id comes first
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, since usage events and the audit log both reference it.
Ids, logs, metrics, and health
Request ids
Every HTTP request to the server is assigned a request id. It 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-Id header 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, and nothing is threaded through parameters. 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 directly, 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.
Tracing a request and scraping metrics
To trace a request, take the X-Request-Id from 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. To propagate a correlation id from your own system, send a valid X-Request-Id on the request; an invalid one is silently replaced with a freshly minted id.
To scrape metrics, point Prometheus (or curl) at GET /metrics on the server. The endpoint is unauthenticated and carries no user data (counts and tool names only), but the shipped production edge configurations deliberately answer 404 for /metrics. Scrape it from inside the deployment's network instead, or remove that block and put your own protection in front of it. Track increases in promoted_rules_lookup_failures_total to alert on rule-delivery failures: the request itself remains available by design, so logs and this counter are the visible failure signal.
When running over stdio, set LOG_STDERR=1 so logs never corrupt the protocol stream.
Left out on purpose
There is 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.
AsyncLocalStorage is used instead of parameter threading. Passing requestId explicitly through every signature is precisely the invasive retrofit this design avoids; ALS attaches context to the execution flow without touching any interface.
There is 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.