Observability¶
Operator guide for wiring kneo-serv's structured logs, request tracing,
and OpenTelemetry exports into a production observability stack.
This page is the setup view. For symptoms and recovery when observability
itself misbehaves, see
troubleshooting.md § 7. For the
full env-var list, see
environment.md § Observability.
Three signals, three surfaces¶
| Signal | What it is | Where it comes from |
|---|---|---|
| Structured request logs | One JSON record per HTTP request, redacted | RequestLoggingMiddleware (always on by default) |
| Service-side trace events | Per-run trace and checkpoint records, queryable via the API | TracingMiddleware, exposed at /v1/runs/{run_id}/trace |
| OpenTelemetry spans | Distributed-tracing spans across SDK-driven agent / tool calls and platform-side operations (queue dispatch, worker lease, continuation lock) | kneo_agent.observability.OpenTelemetryMiddleware + kneo_serv.observability.platform_tracer, opt-in via KNEO_SERV_OTEL_ENABLED |
| Prometheus metrics | Run-queue gauges + per-process run & token counters | GET /metrics (since 0.5.0), opt-out via KNEO_SERV_METRICS_ENABLED |
Prometheus /metrics¶
Since 0.5.0 the service exposes a Prometheus scrape endpoint at GET /metrics
(root path only — not under /v1). It is unauthenticated, like /healthz:
it carries operational counts, not run content or secrets. Mount it only on a
network your monitoring stack can reach (bind the service behind a reverse proxy
that does not expose /metrics publicly), or disable it with
KNEO_SERV_METRICS_ENABLED=false.
| Metric | Type | Meaning |
|---|---|---|
kneo_runs_started_total |
counter | Runs that began execution. |
kneo_runs_completed_total |
counter | Runs that completed successfully. |
kneo_runs_failed_total |
counter | Runs that failed / timed out / hit max-iterations. |
kneo_runs_dead_lettered_total |
counter | Runs dead-lettered after exceeding KNEO_SERV_QUEUE_MAX_ATTEMPTS. |
kneo_runs_rejected_total |
counter | Runs rejected by queue-depth backpressure (terminalized failed{queue_full} → 503). The direct load-shed signal. |
kneo_tokens_input_total |
counter | Input/prompt tokens consumed across runs (from each run's metadata["usage"], when the runtime reports it). |
kneo_tokens_output_total |
counter | Output/completion tokens produced across runs. |
kneo_tokens_total |
counter | Total tokens (input + output) across runs. |
kneo_runs_queued |
gauge | Runs queued and awaiting a worker (your backlog / backpressure signal). |
kneo_runs_running |
gauge | Runs currently leased by a worker. |
kneo_worker_count |
gauge | Live worker threads in this process. |
Counters are per-process and reset on restart — use rate() in Prometheus.
In a multi-process deployment each instance exposes its own counters; aggregate
across instances with sum(). Latency percentiles are not exported here; read
them from the OTel spans or your reverse-proxy access logs.
Structured request logs¶
Shape¶
Each request emits a single JSON record on the kneo_serv.service logger:
{
"client_ip": "10.0.0.7",
"duration_ms": 18.214,
"event": "http_request",
"method": "POST",
"path": "/v1/runs",
"request_id": "f3b3…",
"run_id": "run_…",
"status_code": 200,
"user_agent": "kneo-serv-client/0.2.2"
}
Fields the middleware always emits: event, request_id, method,
path, status_code, duration_ms. Optional fields when available:
client_ip, user_agent. Route-derived fields when the path includes
them: run_id, continuation_id. When the request raises:
error (exception class name) and message (exception message).
Redaction is applied to every payload before it reaches the log line.
(kneo_serv/observability/structured_logging.py)
Configuration¶
| Variable | Default | Purpose |
|---|---|---|
KNEO_SERV_REQUEST_LOGS |
true |
Enable the JSON request log middleware. |
KNEO_SERV_LOG_LEVEL |
INFO |
Stack-wide level: kneo_serv.service + kneo_serv.platform + kneo_agent SDK. |
request_id is generated server-side as a UUID unless the client sends
X-Request-ID; either way the service echoes it back on the response
header.
Production tuning¶
- Keep
KNEO_SERV_LOG_LEVEL=INFOin production.DEBUGdoubles log volume and can leak diagnostic payloads from middleware that wraps the request logger. Note it is stack-wide: it sets the request logger, the platform worker / lease / drain logger (kneo_serv.platform), and thekneo_agentSDK logger together, soDEBUGturns all three up at once — useful for diagnosing a stuck run end-to-end, noisy as a default. - Configure your container runtime's log driver (Docker
json-filewith rotation, Kuberneteskubectl logsrotation, journald) — the service writes to stdout and relies on the runtime to rotate.
Log aggregation wiring¶
- ELK / OpenSearch. Ship stdout via Filebeat or Vector. The records
are already JSON; map
request_idandrun_idas indexed fields. Pinservice.name=kneo-servfrom the shipper for cross-deployment search. - Loki. A Promtail pipeline with a
jsonstage will liftrequest_id,run_id,status_code, andduration_msto labels. Keep label cardinality bounded — don't promoterequest_idto a Loki label, query it as content. - Cloud-managed (CloudWatch Logs, GCP Logging). Forward stdout; the managed pipeline parses JSON automatically.
The reverse proxy in front of kneo-serv (tls_and_proxy.md)
has the true client IP. The service logs the immediate TCP peer; correlate
to the proxy's access logs by request_id (forward X-Request-ID upstream).
Service-side trace events¶
Service-side trace events are persisted as part of run state and
returned at GET /v1/runs/{run_id}/trace. They cover workflow step
transitions, tool calls, checkpoints, and audit boundaries. These events
are emitted by TracingMiddleware independent of any OTel exporter, so
they are always available even without OpenTelemetry.
See
service_api.md § Audit events and
service_api.md § Replay and checkpoint diff
for the contract.
Audit-event export¶
Audit events are always queryable via GET /v1/audit-events and stored in the
run-state store. For compliance retention you can additionally stream them
out: set KNEO_SERV_AUDIT_EXPORT_ENABLED=true and each persisted event is
emitted as a JSON line on the dedicated kneo_serv.audit logger, from the same
record_audit_event chokepoint (so the payload is already redacted).
It is off by default — enabling it is a zero-behavior-change opt-in. The transport is plain stdlib logging, so wire it to your sink the usual way:
import logging
from logging.handlers import SysLogHandler
audit = logging.getLogger("kneo_serv.audit")
audit.addHandler(SysLogHandler(address=("siem.internal", 514)))
audit.setLevel(logging.INFO)
With no dedicated handler the events propagate to the root logger and land in your normal JSON logs alongside the request logs. This is the v1 sink; a direct SIEM/OTLP-logs exporter may follow.
OpenTelemetry spans¶
When the deployment includes the SDK telemetry support (the [telemetry]
or [deploy] extras), set KNEO_SERV_OTEL_ENABLED=true to attach
kneo_agent.observability.OpenTelemetryMiddleware. Argument and result
capture (KNEO_SERV_OTEL_RECORD_ARGUMENTS,
KNEO_SERV_OTEL_RECORD_RESULTS) are off by default because tool inputs
and outputs frequently contain user payloads — enable them only after
the deployment's data classification has approved payload capture.
See environment.md § Observability for
the full env-var reference.
Platform-side spans¶
The SDK's OpenTelemetryMiddleware covers the agent boundary — runs,
tool calls, model calls. The platform also instruments operations that
happen outside the agent's execution:
| Span name | Where | Attributes |
|---|---|---|
kneo.queue.dispatch |
PlatformManager.dispatch_run — when a run is enqueued for an async worker |
kneo.run.id |
kneo.worker.lease |
Async worker loop — one span per lease attempt against the queue | kneo.worker.id, kneo.worker.lease_seconds, kneo.worker.claimed (bool), kneo.run.id (if claimed) |
kneo.continuation.lock |
PlatformManager.resume_human_task — when the per-continuation lock is acquired before resume |
kneo.continuation.id, kneo.lock.name, kneo.lock.ttl_seconds, kneo.lock.acquired (bool) |
These spans share the same KNEO_SERV_OTEL_ENABLED flag — they're a
clean no-op when telemetry is off (no overhead beyond a single env-var
check). Span names use the kneo.<area>.<operation> convention so they
sort cleanly alongside SDK-owned spans in tracing UIs.
GenAI semantic conventions (gen_ai.*). The OpenTelemetry GenAI attributes
(gen_ai.system, gen_ai.operation.name, token counts, …) are emitted by the
SDK's OpenTelemetryMiddleware on the agent / model-call spans — that is where
the model provider is known. The platform-side spans above are provider-agnostic
infrastructure operations (queue / lease / lock) and deliberately do not carry
gen_ai.*; querying GenAI telemetry uses the SDK-owned model spans.
Lease spans with kneo.worker.claimed=false indicate an empty queue —
useful for measuring how often workers idle. Continuation lock spans
with kneo.lock.acquired=false correlate with the LockAcquisitionError
shown in troubleshooting.md § 8.1.
Exporter configuration¶
The service uses the OpenTelemetry global tracer provider; exporters are
configured with standard OTEL_* environment variables that the OTel
SDK reads. Example for OTLP/HTTP to any compatible backend (Honeycomb,
Grafana Tempo, Tempo Cloud, Datadog, an OTel Collector):
export KNEO_SERV_OTEL_ENABLED=true
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io
export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=$HONEYCOMB_API_KEY"
export OTEL_SERVICE_NAME=kneo-serv
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=prod"
For a self-hosted OTel Collector running as a sidecar:
If OTel does not appear to be exporting, see
troubleshooting.md § 7.2.
What to watch in production¶
A minimal alerting baseline covers the failure modes that page on-call:
| Signal | What it means | Where to read it |
|---|---|---|
/readyz returns 503 for more than 1 probe interval |
A dependency check is failing | Reverse proxy / load balancer health checks |
Sustained 5xx rate above baseline |
Service-side errors | Proxy access logs; status_code from JSON logs |
duration_ms p95 climbing over baseline |
Latency regression — provider, queue, or DB pressure | JSON log records |
Queue depth (status=queued) growing unbounded |
Workers stuck or backpressured | /readyz queue check; curl "$BASE/v1/runs?status=queued&limit=20" |
Spike in event=http_request records with error |
Application-level exceptions | JSON log records |
Wire your alerting against these signals from the proxy and the aggregated logs; the service does not push its own alerts.
What this page does not cover¶
- Per-IP rate limiting and traffic shaping. The reverse proxy's job
(
tls_and_proxy.md). - Request-level latency histograms on
/metrics. The/metricsendpoint (since 0.5.0) exports run-queue gauges and run counters, not per-request latency percentiles. Derive request latency from OTel spans or the reverse-proxy access logs. - Tracing internals. For the design of the in-process tracer and
checkpoint events, see
docs/dev/design.mdanddocs/dev/implementation_map.md.