Monitor and troubleshoot
The Service records three signals, and each answers one kind of question:
| Question | Where to look |
|---|---|
| What happened to this request or run? | Logs, searched by request_id or run_id |
| Is the Service healthy, backed up or failing? | Metrics, with the alert rules and the operations dashboard |
| What did the agent do inside an attempt? | The attempt's trace |
| How much did a tenant use, and what did its runs do? | The usage facts in PostgreSQL |
Logs and metrics never contain credentials, request or response bodies, URLs or query strings. Metrics never name a tenant or an object: per-tenant numbers come from PostgreSQL.
Logs
Every process logs to stdout by default, as JSON or, with telemetry.log_format = "pretty", as readable text. a13n-service run can also write a JSON file that it rotates by size:
[telemetry]
log_level = "INFO"
log_file = "/var/log/a13n/service.log"
log_file_max_mb = 100 # rotate when the file reaches this size
log_file_backups = 5 # rotated files kept: service.log.1 … service.log.5
log_stdout = false # only the file
Each process needs a file of its own. Operator commands such as migrate always log to stdout.
Records carry the IDs of the work they belong to, whichever code logged them:
| While handling | Records carry |
|---|---|
| an HTTP request | request_id |
| a worker's claim loop | worker_id |
| an attempt | run_id, attempt_id |
| a sweep pass | sweep |
| a webhook or email | outbox_id, kind |
Besides failures, the Service logs these events:
| Event | Fields |
|---|---|
Request finished |
method, route (the route template), status, duration_ms; probes are not logged |
Run accepted |
run_id, thread_id, trigger |
Attempt claimed |
run_id, attempt_id, queue_wait_ms |
Attempt ended |
run_id, attempt_id, status, reason, duration_ms |
Run sealed |
run_id, status, reason (the failure code) |
Outbox delivered, Outbox delivery failed, Outbox delivery dead |
outbox_id, kind, reason |
Sweep failed |
sweep, error_type |
Configure proxies in front of the Service to redact query strings from their own access logs as well.
Metrics
Set telemetry.metrics_port, and every process serves Prometheus metrics at /metrics on server.host and that port. Prometheus and VictoriaMetrics scrape it alike. Keep the port private: it is separate from server.port so that nothing routes it with the API. The Helm chart serves it on port 9464 by default.
| Metric | Labels | Meaning |
|---|---|---|
http_server_request_duration_seconds |
http_request_method, http_route, http_response_status_code |
request duration, streams included |
a13n_runs_accepted_total |
trigger |
accepted runs |
a13n_runs_sealed_total |
status, reason |
sealed runs; reason is the failure code |
a13n_attempt_queue_wait_seconds |
— | how long a due run waited for a worker |
a13n_attempt_duration_seconds |
status |
attempt duration from claim to end |
a13n_worker_slots |
state: free, busy |
a worker's attempt slots |
a13n_backlog_size |
queue |
due runs, or due deliveries of an outbox kind |
a13n_backlog_oldest_age_seconds |
queue |
how long the oldest due item has waited |
a13n_outbox_deliveries_total |
kind, result |
webhook, email and other delivery outcomes |
a13n_sweep_passes_total |
sweep, result |
background sweep passes |
Workers also serve the Harness's metrics, such as a13n_harness_run_duration_seconds, and model token usage as gen_ai_client_token_usage; see Harness observation. Backlog counts come from a query that control replicas repeat every 15 seconds and are capped at 10,000; every control replica reports the same values, so take their maximum.
The monitoring bundle has alert rules, an operations dashboard and a usage dashboard, with instructions for Prometheus, VictoriaMetrics, their Kubernetes operators and Grafana.
Troubleshoot a request or run
Start from the X-Request-Id of the response, which error bodies repeat as request_id:
-
Search the logs for the
request_id.Request finishedgives the route and status. A request that started a run also loggedRun acceptedwith therun_id; a refused request has none, and its status and error body say why. A message queued behind an active run starts its run later, so find that run in the thread's inbox. -
Search the logs for the
run_id. Where its records stop tells where the run is:
| Last record of the run | The run | Look next at |
|---|---|---|
Run accepted |
waits for a worker | a13n_backlog_oldest_age_seconds{queue="runs"}, a13n_worker_slots and whether workers are ready |
Attempt claimed |
is executing | the attempt's trace |
Attempt ended with failed or yielded |
returned to the queue to be retried or continued | reason, then the next Attempt claimed |
Run sealed |
has ended | status and reason; GET …/runs/{run_id} carries the failure message |
-
Open the attempt's trace with
GET …/runs/{run_id}/attempts/{attempt_id}/trace, or find it in Langfuse or Logfire by theservice_run_idandrun_attempt_idmetadata. -
For a webhook or email that never arrived, search for the
Outboxrecords of itskind: a dead delivery names itsreason.
Usage
Per-tenant usage is recorded as facts in PostgreSQL, never in metrics. A workspace reads its own through the usage API. Operators can import the usage dashboard from the monitoring bundle, which reads runs and model usage per organization, workspace and model through a read-only database role.