Skip to content

Observability

Raven splits telemetry into two layers and uses a different tool for each:

  • Application telemetry (traces, logs, metrics emitted by the Go API and the Python AI worker) lands in OpenObserve.
  • Host telemetry (CPU, memory, disk, network, per-container resource usage for the box itself) lands in Beszel.

There is no Grafana in the stack. OpenObserve provides its own dashboarding, SQL/PromQL query, and alerting; Beszel provides its own UI for host metrics. Keeping them separate means a host-level issue (disk full, agent dead) can still be diagnosed when the application stack is unhealthy.

Stack overview

ToolCoversWhere it runsDefault access
OpenObserveApp traces, logs, metrics (OTLP/gRPC)docker-compose.yml service openobservehttp://localhost:5080
Beszel HubHost CPU/RAM/disk/network, container resourcesdeploy/ansible/files/docker-compose.admin.yml (separate admin compose file)http://<host>:8090 / monitor.<your-domain> via Cloudflare tunnel
Beszel AgentReports host stats to the hubStandalone container, started by Ansible (deploy/ansible/roles/admin-tools/tasks/main.yml)n/a — talks to the hub on localhost:45876

Beszel is not bundled in the main Compose file. It lives in a separate admin Compose file because it monitors the host running Raven; it must keep running even when you tear the application stack down for maintenance.

OpenObserve — app telemetry

Default deployment (Compose)

The service block in docker-compose.yml:

yaml
openobserve:
  image: public.ecr.aws/zinclabs/openobserve:latest
  ports:
    - "5080:5080"   # UI + HTTP API
    - "5081:5081"   # gRPC OTLP receiver (traces, metrics, logs)
  environment:
    ZO_ROOT_USER_EMAIL: ${ZO_ROOT_USER_EMAIL:-admin@raven.local}
    ZO_ROOT_USER_PASSWORD: ${ZO_ROOT_USER_PASSWORD:?ZO_ROOT_USER_PASSWORD is required}
    ZO_GRPC_PORT: "5081"
  volumes:
    - openobserve-data:/data
  networks:
    - raven-internal
  restart: unless-stopped
SettingDefaultPurpose
Imagepublic.ecr.aws/zinclabs/openobserve:latestPin to a digest in production.
HTTP/UI port5080Web UI, REST API, query.
OTLP gRPC port5081Where Raven services push traces/logs/metrics.
Data volumeopenobserve-data/dataAll ingested data, dashboards, users.
Admin email${ZO_ROOT_USER_EMAIL:-admin@raven.local}Initial root user; first-login credentials.
Admin password${ZO_ROOT_USER_PASSWORD}Required at boot — Compose fails fast without it.

Open the UI at http://localhost:5080 and log in with the email and password above (configured via .env; see .env.example).

What gets emitted

Go API (raven-api)

Telemetry is wired in internal/telemetry/telemetry.go and applied via the Gin middleware in internal/middleware/otel.go.

  • Traces — one span per HTTP request (server kind), named <METHOD> <route>, with http.request.method and url.path attributes. The middleware sets an X-Trace-ID response header so a trace can be looked up from any user-facing error.

  • Logsslog is wrapped with the otelslog bridge: every structured log goes to stderr and to OpenObserve as an OTLP log record, automatically decorated with trace_id / span_id from the request context.

  • Metrics — pre-registered instruments in internal/telemetry/metrics.go:

    MetricTypeUnit
    raven.http.requests.totalcounter{request}
    http.server.request.durationhistograms
    raven.chat.completions.totalcounter{completion}
    raven.chat.tokens.totalcounter{token}
    raven.chat.completion.latencyhistogramms
    raven.voice.sessions.createdcounter{session}
    raven.voice.sessions.activeup-down counter{session}
    raven.voice.turns.totalcounter{turn}
    raven.whatsapp.calls.totalcounter{call}
    raven.whatsapp.messages.totalcounter{message}

When OTLP is not configured, all of these become no-ops with zero overhead.

Python AI worker (raven-ai-worker)

Configured in ai-worker/raven_worker/telemetry.py using opentelemetry-sdk, opentelemetry-exporter-otlp, and opentelemetry-instrumentation-grpc (versions pinned in ai-worker/pyproject.toml).

The worker emits traces for gRPC calls — one server-side span per RPC, covering ingest, embedding, retrieval, and chat completion. Logs go through structlog to stdout (collected by Docker); the worker does not currently push OTLP logs or custom metrics to OpenObserve.

OTLP wiring

Two ways to point a Raven service at OpenObserve.

Go API — Raven-specific vars

VariableDefault (Compose)Notes
RAVEN_OTEL_ENABLEDtrue (in docker-compose.yml)When unset/false, telemetry is a no-op.
RAVEN_OTEL_ENDPOINTopenobserve:5081gRPC endpoint, host:port (no scheme).
RAVEN_OTEL_SERVICE_NAMEraven-apiBecomes service.name resource attribute.

Python worker — standard OTel var

VariableDefault
OTEL_EXPORTER_OTLP_ENDPOINThttp://openobserve:5081

When connecting from outside the Compose network, replace openobserve with the host's address. TLS is not enabled by default (the exporter uses WithInsecure()); put OpenObserve behind your reverse proxy or a Cloudflare tunnel for external exposure.

See /reference/configuration/ for the full set of env vars consumed by the API.

Retention

Retention is not preconfigured in this repo. deploy/observability/ ships dashboards only — no retention or storage policy YAMLs. OpenObserve defaults apply (per-stream retention is set in the OpenObserve UI under Streams → <stream> → Settings). Recommended starting points for a single-node deployment:

  • Traces: 7 days
  • Logs: 14 days
  • Metrics: 30 days

Tighten or loosen based on the disk allocated to the openobserve-data volume.

Querying

OpenObserve supports SQL for logs and PromQL for metrics; consult its official docs for full syntax. Two examples that work against Raven's emitted telemetry:

sql
-- Logs: every error from the API in the last 15 minutes
SELECT _timestamp, body, trace_id, attributes_org_id
FROM "default"
WHERE service_name = 'raven-api'
  AND severity_text = 'ERROR'
ORDER BY _timestamp DESC
LIMIT 200
promql
# Metrics: P95 HTTP server latency, per route, over 5-minute windows
histogram_quantile(
  0.95,
  sum by (le, http_route) (rate(http_server_request_duration_bucket[5m]))
)

Beszel — host telemetry

What it covers

CPU usage, memory, disk usage and I/O, network throughput, system load, plus per-container CPU/memory for every Docker container running on the host. It does not see application metrics — that's OpenObserve's job.

Default deployment

Beszel is split into two pieces and deployed by Ansible, separately from the main application stack:

  • Beszel Hubhenrygd/beszel:0.18.7, defined in deploy/ansible/files/docker-compose.admin.yml. Runs with network_mode: host because the agent talks to it over SSH on localhost:45876.
  • Beszel Agenthenrygd/beszel-agent:0.18.7, started as a standalone container by deploy/ansible/roles/admin-tools/tasks/main.yml. Mounts /var/run/docker.sock read-only so it can report container stats. Requires a KEY obtained from the Hub UI after adding the system; the key is stored in Ansible vault as beszel_agent_key.

The agent is intentionally not in docker-compose.yml: it must survive docker compose down of the application stack so you can still see what is happening to the host while you bring services back up.

Default URL

In production deployments wired through Cloudflare tunnels (see deploy/ansible/roles/cloudflared/templates/config.yml.j2) the hub is exposed at monitor.<your-domain> — for the reference deployment that is https://monitor.ravencloak.org. On a local host without a tunnel, it is served on port 8090 of the host network.

Application logs vs host logs

You're investigating…Look at
Why a chat completion returned 500OpenObserve (logs + traces, filter by trace_id)
Why a request took 8 secondsOpenObserve (http.server.request.duration histogram)
Why the box is OOM-killing containersBeszel (memory + per-container)
Why a container is restarting on a loopBeszel (container state) then OpenObserve (logs from before the restart)
Why disk pressure is alertingBeszel (disk usage) — OpenObserve will likely also be unhappy

Alerting

OpenObserve has built-in alert rules with webhook actions. Raven ships alert rule definitions but not active alerts — they live as JSON in deploy/observability/dashboards/alerts.json and must be imported (or recreated) manually in the OpenObserve UI before they fire. The bundled rules cover:

RuleConditionSeverity
high_error_rate5xx rate > 5% over 5 mincritical
high_p95_latencyP95 http.server.request.duration > 2 s over 5 minwarning
chat_completion_failures5xx rate on /api/v1/chat/.../completions > 5% over 5 mincritical

Each rule has a ${ALERT_WEBHOOK_URL} placeholder; point it at Slack, PagerDuty, or any other receiver before enabling.

Beszel has its own alerting (CPU/memory/disk thresholds, SSH liveness) configured per-system in the Hub UI.

Sample dashboards

Four dashboards ship in deploy/observability/dashboards/:

FilePurpose
api-overview.jsonRequest rate, error rate, P50/P95/P99 latency, status code distribution
chat-completions.jsonCompletion volume, token throughput, latency by org/KB/model
voice-sessions.jsonSessions created, active gauge, turns processed
whatsapp.jsonCalls and messages by org, direction, type

These are JSON definitions, not OpenObserve native exports — you will need to recreate them in the UI (PromQL queries are reusable as-is) or import via the OpenObserve API. Treat them as a known-good starting set; build deployment-specific dashboards on top.

See also