Observability¶
ApiPi emits structured logs, Prometheus metrics, OpenTelemetry traces, and agent-layer usage events. Operators collect those signals with their own stack. ApiPi does not ship Grafana, Loki, Tempo, Alertmanager, or Sentry. It does not store USD. It does not put prompt or completion bodies in Postgres, default logs, metrics, or spans.
Laptop defaults stay quiet: APIPI_METRICS is off and
APIPI_OTEL_ENDPOINT is unset. Production turns signals on and
points exporters at the operator collector. What each series and
field means is on usage. This page is how to emit and
collect in production.
You bring¶
| Need | You run |
|---|---|
| Log store | Vector, Fluent Bit, Grafana Alloy, or a cloud log agent reading stderr |
| Metrics | Prometheus (or compatible) scraping /metrics |
| Traces | An OTLP collector, then Tempo, Jaeger, or a vendor |
| Warehouse / BI | HTTPS sink for usage export, then your warehouse |
| Dashboards and alerts | Grafana, Alertmanager, or the vendor UI |
ApiPi does not bundle those tools. Optional example dashboards are not a runtime dependency.
What to enable¶
| Config | Default | Production |
|---|---|---|
APIPI_LOG_LEVEL |
info |
info |
APIPI_LOG_FORMAT |
json |
json (use text only on a laptop) |
APIPI_METRICS |
off | on, on API and worker |
APIPI_WORKER_METRICS_HOST |
0.0.0.0 |
scrape bind on the worker |
APIPI_WORKER_METRICS_PORT |
9091 |
worker /metrics |
APIPI_GUEST_SAMPLE_INTERVAL |
unset (off) | unset, or 15s if you want vsock samples |
APIPI_OTEL_ENDPOINT |
unset | OTLP/HTTP collector, on API and worker |
APIPI_USAGE_STORE |
turns |
turns or rollups |
APIPI_USAGE_RETENTION |
15d |
keep the hot store bounded |
APIPI_USAGE_EXPORT_URL |
unset | HTTPS POST of one usage event per turn |
APIPI_USAGE_EXPORT_TOKEN |
unset | bearer for that URL |
APIPI_PAYLOAD_EXPORT_URL |
unset | only if you need message bodies outside ApiPi |
The full setting list is in configuration.
Combined apipi serve is one process: scrape its /metrics and set
OTEL on that process. Production is apipi serve --api-only plus
apipi worker. Turn series and turn/model spans are recorded on the
worker. HTTP series and worker.assign spans are on the API. Set
APIPI_METRICS and APIPI_OTEL_ENDPOINT on both.
Logs¶
Ship stderr. There is no log file in the gateway. Each line is one
JSON object with timestamp, level, logger, message, and
service (apipi). Error and warning lines that operators should
alert on also set event and error_code, plus request_id,
tenant_id, session_id, turn_id, and worker_id when known.
A typical shipper reads stderr and writes Loki, CloudWatch, or another store. Example shape (Vector):
[sources.apipi]
type = "stdin"
decoding.codec = "json"
[sinks.loki]
type = "loki"
inputs = ["apipi"]
endpoint = "http://loki:3100"
Fluent Bit, Alloy, and cloud agents work the same way: JSON lines on stderr, no ApiPi-side shipper.
The event table is in usage. Alert on
turn.failed, api.error, sandbox.boot.failed,
worker.assign.failed, worker.lease.expired, and export drops.
Prometheus¶
No bearer. Network-restrict /metrics like any scrape endpoint.
| Process | Scrape | What you get |
|---|---|---|
Combined apipi serve |
http://<api>:8000/metrics |
HTTP, turns, tokens, worker-pool gauges, sandbox series if this process runs sandboxes |
| API-only | http://<api>:8000/metrics |
HTTP requests, errors, apipi_workers and apipi_worker_leases (labeled run_mode), apipi_worker_assign_seconds |
| Worker | http://<worker>:9091/metrics |
Turns, tokens, utilization, sandbox boot/destroy, host Pi RSS/PSS, cgroup guest RAM/CPU, optional vsock samples |
Worker metric sets (same scrape, metrics on):
| Set | When | Series |
|---|---|---|
| Worker util | All run modes | apipi_worker_{capacity,sessions,memory_mib_*} |
| Sandbox lifecycle | Any spawn through PiPool |
apipi_sandbox_* |
| Host Pi | chat / none (no vm_id) |
apipi_pi_processes, apipi_pi_rss_bytes, apipi_pi_pss_bytes, apipi_pi_spawn_total, apipi_pi_kill_total |
| MicroVM guest | vm_id set |
apipi_guest_* |
apipi_worker_memory_mib_used is reserved guest budget for placement. apipi_pi_rss_bytes is actual host Pi RAM (process group, including MCP children Pi started). Guest jailer cgroup is apipi_guest_memory_bytes. Do not mix them.
Guest resource layers:
| Layer | What | Default | How |
|---|---|---|---|
| A. Host / cgroup | Jailer cgroup memory and CPU | On when worker metrics are on | Read on the host. No guest code. |
| B. Guest sample | MemAvailable, load, workspace disk | Off | Tiny JSON over vsock. Set APIPI_GUEST_SAMPLE_INTERVAL. |
| C. In-guest Prometheus | node_exporter on TAP | Out of scope | Not lightweight. Attack surface. |
Prometheus labels stay low-cardinality. tenant is allowed. Do not
put session_id or user_id on series. Scrape node_exporter on the
worker host if you need machine disk and NIC.
Traces¶
When APIPI_OTEL_ENDPOINT is set, ApiPi exports OTLP/HTTP traces.
/v1/traces is appended if missing. Spans are wait-focused:
session, worker.assign, sandbox.boot, sandbox.attach,
turn, model. Inbound traceparent is honored. The worker
command carries it so split API plus worker stays one trace.
Point the endpoint at your collector (Tempo, Jaeger, or a vendor). Use traces to see where time went on a slow turn. Use Prometheus for rates and saturation. Use logs for error codes.
Usage export¶
GET /v1/usage is the hot store: tenant-scoped session, turn, or
day. Long-term “who used what” is APIPI_USAGE_EXPORT_URL: one JSON
event per turn, tokens and counts only, never USD. Join with
tenant_id, user_id (when the auth plugin set it), agent_id,
session_id, turn_id, and request_id.
Auth plugins may return user_id. ApiPi does not invent it from
key_id. X-User-Id on HTTP responses is still key_id. See
auth and usage.
Suggested alerts¶
| Signal | Why |
|---|---|
Rate of event=turn.failed or apipi_turns_total{status="failed"} |
Turns dying |
apipi_errors_total 5xx / event=api.error |
Gateway faults |
HTTP 429 with capacity / event=worker.assign.failed |
Node or tenant full |
apipi_sandbox_boot_total{result="error"} / event=sandbox.boot.failed |
Guests not starting |
apipi_worker_sessions near apipi_worker_capacity |
Packing too tight |
apipi_pi_rss_bytes near host RAM on a chat worker |
Dense Pi packing |
apipi_worker_assign_seconds p95 |
Lease wait |
apipi_usage_export_total{result="drop"} |
Warehouse gaps |
event=worker.lease.expired |
Worker died or heartbeat failed |
Cardinality¶
| Signal | Identity |
|---|---|
| Prometheus | tenant ok. Not user_id or session_id. Guest series use size (S / M / L). |
| Logs and traces | request_id, tenant_id, session_id, turn_id, worker_id when known |
| Usage export | tenant_id, user_id, key_id, agent_id, session_id, turn_id, request_id |