telemetry block makes the Sidecar push OpenTelemetry signals to a collector you run. One Grafana, Honeycomb or Datadog then shows every Sidecar: statements per lane and verdict, denials by source, and a statement’s latency split into the Sidecar’s own policy cost, the AI analyzer, a human approval hold, and the upstream itself.
Nothing is built when the block is absent. The data path has no telemetry code in it until you add one.
config.yaml
sample_ratio. Events — the audit trail as log records — turn on with events.enabled.
Configuration
The block is validated with the rest of the file, so
--validate and the Control Plane both refuse a bad endpoint before anything starts. The SDK’s OTEL_EXPORTER_OTLP_* environment variables are not read: the block is the configuration.
A Sidecar managed by a Control Plane reports the
telemetry capability in its handshake. The Control Plane serves the block only to a Sidecar whose build decodes it, so an older Sidecar never receives a key it would refuse.What every signal carries
The resource identifies the process:
The schema URL is
https://hoop.dev/schemas/sidecar-telemetry/1.0. An attribute or series added bumps the minor version. One renamed or removed bumps the major, because a dashboard written against the old name goes dark.
Attributes shared by the signals:
Principal, session id, tables and statement text are never on a metric: they are unbounded, and they belong on the span and the log record.
Metrics
Counters survive a hot reload. The three analyzer families are the ones
GET /metrics serves in the Prometheus text format, which stays as it is; see Risk Analysis.
Through a collector’s Prometheus exporter the names read hoop_sidecar_statements_total, hoop_sidecar_statement_duration_seconds_bucket, and the attributes hoop_lane, hoop_stage, and so on.
Where a statement’s time goes
hoop.sidecar.statement.duration has one series per stage of a statement’s life, so you can read what the Sidecar costs apart from what the database or API costs:
review is its own stage so a ten-minute approval does not read as a slow policy. A listener in return mode (review_mode: return) shows no wait here: the client is denied at once and resends after approval, and only the Control Plane knows the time between filing and approval.
hoop.pairing on the upstream stage says how exact the measurement is:
exact— the protocol pairs request and response itself: one gRPC call, or HTTP/1, where responses answer requests in order.unpipelined—postgres,mysql,mssql,mongodb,oracle,clickhouse. Their codecs do not expose which response answers which request, so the sample is taken only when one request is outstanding and dropped when the client pipelines. The series describes unpipelined exchanges and never guesses. An extended-protocol client that sends two statements per query is therefore not sampled.
hoop.outcome as allow or deny.
Traces
traces.sample_ratio above zero opens one span per exchange, with the statements and their stages under it:
hoop.direction, hoop.operation, the HTTP method, path and status, the database and tables, then at the end hoop.verdict, hoop.deny.source, hoop.rule, hoop.risk_level. A denied statement’s span has an error status naming the rule. The exchange span carries hoop.principal and hoop.session_id. The upstream measurement is an event on the enclosing span.
The OPA decision and the analyzer’s provider call carry the stage’s span as a W3C traceparent header, so a Rego service or a model gateway that is itself traced joins the same trace.
Statement text is never on a span.
Events
events.enabled adds a sink to the audit chain that sends every event as a log record. The body is the kind: session_start, statement, violation, masked, error, session_end. Severity is WARN for violation and masked, ERROR for error, INFO otherwise.
A record written under a sampled span carries its trace and span id, so a backend that stores both joins the
violation to the trace that produced it.
The record goes through the same redaction as the audit file and the Control Plane copy. hoop.statement is a sha256: fingerprint unless events.statement_text: true, because the collector is a new place audit data leaves the process; the file keeps the text under the audit block’s own redact_statements. The sink never fails a write and never blocks a statement: a bounded queue drops the oldest record when the collector falls behind, and GET /stats reports the count:
Alerts
Alerts live in your backend. Three worth having, in PromQL:violation burst for one hoop.principal, which no metric can express because the principal is only on the record.
When the collector is down
Nothing changes for the client. Exports fail, one warning per 30 seconds per message reaches the Sidecar’s log, and traffic is unaffected. With atelemetry block the data path costs one instrument record per hook, with attribute sets built once per lane; without one it costs nothing. Shutdown flushes once, bounded by three seconds, after every listener has closed.
A local collector
The Envoy compose stack in thehoop repository, deploy/docker-compose/envoy-stack, ships an OpenTelemetry Collector, Prometheus and a Grafana with a provisioned hoop sidecar dashboard. Its sidecar/config.yaml carries the block above with every signal on, and docker compose logs otel-collector shows the spans and the audit records arrive. For your own collector, this is the receiving side:
otel-collector.yaml
Next
Risk Analysis
The analyzer families in Prometheus text format, and the queries that tune
timeout_sec.Config File Reference
Every section, inheritance between listeners, and what startup refuses.
Agent Approvals
Hold and return modes, which decide whether the
review stage shows a wait.Architecture
The trace of one connection through the gate, which is what the stages measure.