Chio/Docs
LOGIN · JOIN

Node Observability

Scrape a Chio node with Prometheus and join each request's trace to the signed receipt that records its decision.

Deployment chio@5f63e6e

On this page

Prometheus scrapes the sidecar's /metrics route, a collector carries its spans to Tempo and its logs to Loki, and its signed receipts stay in the receipt store for SIEM export.

The steps use the HTTP sidecar, chio api protect. The MCP edge serves the same Prometheus text at /admin/metrics.

Start
Signal pathsChoose a signal.
governed call/metricsscrapeOTLPspanslogssignedread onlyat least once/chio/live, /chio/healthacallerKchio api protectgcontrol tokenPPrometheuscOTel collectorTTempoLLokiρreceipt storeechio-wallSSIEMoorchestrator probescall/metricsscrapeOTLPspanslogssignedreaddeliver/chio/healthacallerKsidecargtokenPPrometheusccollectorTTempoLLokiρstoreeexportSSIEMoprobes

Each signal leaves the sidecar by its own route

Metrics leave through the gated /metrics route, spans and logs through OTLP to a collector, and signed receipts through the receipt store. Orchestrator probes reach the ungated /chio/live and /chio/health routes. Sidecar HTTP service.

One sidecar node and the route each signal takes out of it. Prometheus, the collector, Tempo, Loki, and the SIEM stand for whichever backends a deployment runs; the OTLP receipt ingress and the trust control plane are left out.

Metrics and traces leave the sidecar unsigned, bound for operational backends. Receipts leave signed by the kernel key, so they are the signal you hand to an auditor.

The kernel appends each signed receipt to the store named by --receipt-store, and chio-wall siem-export reads that file read-only and delivers each receipt at least once. Orchestrator probes call /chio/live for container liveness and /chio/health for platform readiness; neither route takes a token.

1. Scrape /metrics with the control token

The sidecar mounts /metrics on its ingress port, the one --listen binds, ahead of the catch-all proxy route. The route sits behind the sidecar-control middleware that gates the approval routes, so the scraper presents the control token the deployment already provisions.

prometheus.ymlyaml
scrape_configs:
  - job_name: 'chio-sidecar'
    scrape_interval: 15s
    metrics_path: /metrics
    authorization:
      type: Bearer
      credentials_file: /etc/prometheus/chio-sidecar-control-token
    static_configs:
      - targets:
          - chio-sidecar:9090

Check the target by hand with the same token.

Terminalbash
curl -s -H "Authorization: Bearer $CHIO_SIDECAR_CONTROL_TOKEN" \
  http://127.0.0.1:9090/metrics | grep -E '^chio_receipt_'
Output
chio_receipt_uncheckpointed_seq_range 0
chio_receipt_seconds_since_last_checkpoint 42

You should see the two receipt watchdog gauges in a text/plain; version=0.0.4 body; a refused request means the scraper lacks the control token. The reference ECS task definition advertises the route with "prometheus.io/path": "/metrics" and "prometheus.io/port": "9090", and the Cloud Run and Azure manifests do the same for their own container port.

2. Send spans and logs through a collector

The sidecar exports OTLP to CHIO_OTEL_RECEIPT_EXPORTER_ENDPOINT, or to OTEL_EXPORTER_OTLP_ENDPOINT when the first is unset; chio doctor reports the endpoint in that order. A standard OpenTelemetry Collector aggregates everything except receipts, which go to the receipt store first.

otel-collector.yamlyaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 5s
    send_batch_size: 1024

  # Strip high-cardinality attributes from anything bound for Prometheus.
  # These attributes are safe on spans and logs; they are not safe as
  # Prometheus label values.
  attributes/strip-cardinality:
    actions:
      - key: gen_ai.tool.call.id
        action: delete
      - key: chio.receipt.id
        action: delete
      - key: chio.replay.run_id
        action: delete
      - key: chio.tenant.id
        action: delete

exporters:
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true

  loki:
    endpoint: http://loki:3100/loki/api/v1/push

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/tempo]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [loki]

You should see traces in Tempo and log lines in Loki within the 5 s batch timeout. The attributes/strip-cardinality processor removes four attributes before anything Prometheus-shaped sees them.

3. Join a trace to its receipt

Pass a W3C traceparent header from the upstream caller. The kernel joins that trace and emits its spans as children, named gen_ai.tool.call with operation tool.call, both constants in chio_kernel::otel. The receipt then carries provenance.otel.trace_id and provenance.otel.span_id.

Tempo TraceQLtext
{ span.chio.receipt.id = "$receipt_id" }

You should see every trace that touched the receipt. This query drives the Tempo span timeline dashboard; the Jaeger dashboard pivots the same way from receipt id to trace.

4. Import the Grafana dashboards

The dashboards ship as JSON under deploy/dashboards/ in the chio repository. With Grafana running, run the import loop from a clone of it.

Terminalbash
find deploy/dashboards -name '*.json' -print0 | \
  while IFS= read -r -d '' dashboard; do
    jq -n --argjson dashboard "$(cat "$dashboard")" \
      '{dashboard: $dashboard, overwrite: true}' \
    | curl -fsS -H 'Content-Type: application/json' \
        -X POST http://admin:admin@127.0.0.1:3000/api/dashboards/db -d @- \
        >/dev/null
  done

You should see the dashboards at http://localhost:3000 under the chio tag. An empty panel usually means a span or log line lacks one of the fields the dashboards expect.

A compose stack beside a sidecar

This stack runs an OTel collector, Prometheus, Loki, Tempo, and Grafana beside a sidecar. The sidecar image's CMD is ["--help"], so its service passes a subcommand; the arguments are the ones the reference ECS task definition uses.

docker-compose.yamlyaml
services:
  chio-sidecar:
    image: chio-sidecar:latest
    command:
      - api
      - protect
      - --upstream
      - http://app:8080
      - --spec
      - /etc/chio/spec/openapi.yaml
      - --listen
      - 0.0.0.0:9090
      - --receipt-store
      - /var/lib/chio/receipts.db
      - --authority-seed-file
      - /etc/chio/seed/authority.seed
    environment:
      CHIO_LOG_LEVEL: info
      CHIO_SIDECAR_CONTROL_TOKEN: ${CHIO_SIDECAR_CONTROL_TOKEN}
      OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
    ports:
      - "9090:9090"      # proxy ingress, /chio/health, /chio/live, /metrics

  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    command: ["--config=/etc/otel-collector.yaml"]
    volumes:
      - ./otel-collector.yaml:/etc/otel-collector.yaml:ro
    ports:
      - "4317:4317"      # OTLP gRPC
      - "4318:4318"      # OTLP HTTP

  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro

  loki:
    image: grafana/loki:latest
    ports:
      - "3100:3100"

  tempo:
    image: grafana/tempo:latest
    command: ["-config.file=/etc/tempo.yaml"]
    volumes:
      - ./tempo.yaml:/etc/tempo.yaml:ro

  grafana:
    image: grafana/grafana:latest
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin
    ports:
      - "3000:3000"
    volumes:
      - ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/ds.yaml:ro

5. Load the alert rules

Copy chio-recording-rules.yml and chio-alert-rules.yml from deploy/prometheus/ and list them in prometheus.yml. The recording rules precompute the error ratios and p95 series; the alert rules layer two-window burn-rate and missing-data alerts on top.

prometheus.ymlyaml
rule_files:
  - /etc/prometheus/chio-recording-rules.yml
  - /etc/prometheus/chio-alert-rules.yml
deploy/prometheus/chio-alert-rules.yml4-17yaml
- alert: ChioReceiptWriteErrorBudgetBurn1h
  expr: |
    chio:receipt_write_error_ratio_1h > (14.4 * 0.001)
    and
    chio:receipt_write_error_ratio_6h > (6 * 0.001)
  for: 2m
  labels:
    severity: p1
    notification_route: pagerduty
    opsgenie: "true"
    slo: receipt-write
  annotations:
    summary: Chio receipt writes are burning error budget
    runbook: docs/operator-runbook/slo.md

You should see fourteen Chio alert rules in one group on the Prometheus alerts page. Receipt writes hold a 0.1% error budget; the short window catches a fast burn and the long window suppresses flapping.

Receipts settle disputes; metrics and traces find incidents

Ask each signal the question it answers.

SignalQuestion it answersBackendSigning and retention
ReceiptsWhat did Chio decide for this request?Receipt store and SIEMBy the kernel key; kept per your archive policy
TracesWhere did this request spend its time, and which guards ran?Tempo or JaegerUnsigned; kept per your log retention
MetricsIs the deny rate spiking? Are guards exhausting fuel?Prometheus or OTel metricsUnsigned and aggregated; kept per your TSDB retention

For dispute resolution and compliance evidence, query receipts. For the health of the node right now, read metrics. To explain why request X failed at 03:14, read its trace and join it to its receipt. If you find yourself grepping log lines to learn whether a call was denied yesterday, use ReceiptQuery instead; SIEM Export carries receipts to long-term retention with a sequence cursor, and the Receipt Dashboard gives operators a receipt-first view beside Grafana.

Read more in Receipts & Audit.

Four queries answer most questions

Paste these into Grafana Explore or the Prometheus console. The rule pack turns the same signals into paging alerts.

  • Deny rate per guard. rate(chio_guard_deny_total[5m]) grouped by guard_id, reason_class. A jump above a guard's baseline is the early warning for both attacks and policy regressions.
  • Fuel exhaustion tail. histogram_quantile(0.99, ...) on chio_guard_eval_duration_seconds with verdict="deny", joined against chio_guard_deny_total{reason_class="fuel"}. It shows WASM modules approaching their fuel ceiling.
  • External-guard call failures. The circuit breaker logs no transitions of its own; every failed provider call logs tracing::warn!(guard, error, "external guard failed") and pushes the breaker toward open. A sustained rate per guard means a provider is degraded and the breaker is shedding load; see External Guard Adapters.
  • Reload outcomes. chio_guard_reload_total grouped by outcome. A rising canary_failed or rolled_back count means hot reloads are not landing cleanly.

Every Prometheus label has a finite domain

The exporter strips a fixed set of four attributes from anything bound for Prometheus.

crates/observability/chio-otel-receipt-exporter/src/denylist.rs8-13rust
pub const PROMETHEUS_DENIED_ATTRIBUTES: [&str; 4] = [
    ATTR_GEN_AI_TOOL_CALL_ID,
    ATTR_CHIO_RECEIPT_ID,
    ATTR_CHIO_REPLAY_RUN_ID,
    ATTR_CHIO_TENANT_ID,
];

chio.tenant.id is on that list even though the dashboards need it on spans and log lines. The sink decides: tenant id is a filter dimension in Loki and Tempo and an unbounded label domain in Prometheus. Guard labels stay finite too: the registry caps unique guard ids at 1,024, and reason_class folds every unrecognized deny reason into other, so reason_class="other" on a dashboard means the classifier chose the fallback. Hitting the cap drops the guard from metrics only; the kernel still runs it and still records receipts, so alert on E_GUARD_METRIC_CARDINALITY_EXCEEDED in the logs.

Read more in Fail-Closed Semantics.

Log lines name the guard; receipts carry the decision

Every Chio crate logs through tracing, and guard spans use the chio.guard target, because EnvFilter directives match targets. Guards that log share one convention, a guard field carrying the kebab-case Guard::name(), the same string the receipt records as GuardEvidence.guard_name. To correlate one decision, use a request, trace, or receipt id; the log levels and representative call sites are in the reference below.

Each failure surfaces as a signal you can page on

A dashboard panel renders empty

A span or log line lacks a field the dashboard filters on. Compare your telemetry against the span and log fields listed in the reference; chio.receipt.id and chio.tenant.id are the usual gaps.

A MetricsMissing alert fires

Prometheus has seen no sample of the series for 10 minutes. Check that the target is up and that the scraper still presents a valid control token, then check the process that emits the series.

An OTel drop counter rises

chio_otel_ingress_drop_total counts trace batches the bounded ingress queue dropped; chio_otel_sink_drop_total counts batches whose receipt append failed. Size the queue for the span rate, then check the receipt store's writer.

ChioReceiptCheckpointStale pages

No receipt checkpoint has been written for a day, even if writes still commit. Run chio receipt checkpoint status and restore checkpointing before the uncheckpointed range grows further.

Metric families, rules, and dashboards

What /metrics carries

The sidecar composes the body from three renderers. Knowing which one owns a family tells you where to look when a series goes missing.

crates/products/chio-api-protect/src/proxy/router.rs115-135rust
/// Compose the Prometheus scrape body from the kernel guard families, the
/// http-core mediation-edge families, and the alert-pack families.
async fn handle_metrics() -> impl axum::response::IntoResponse {
    let alert_pack = || {
        let mut out = String::new();
        chio_metrics_spec::runtime::render_alert_pack_families(&mut out);
        out
    };
    let body = chio_metrics_spec::runtime::compose_metrics_body(&[
        &chio_kernel::render_guard_metrics_prometheus,
        &chio_http_core::metrics::render_http_core_metrics_prometheus,
        &alert_pack,
    ]);
    (
        [(
            axum::http::header::CONTENT_TYPE,
            "text/plain; version=0.0.4",
        )],
        body,
    )
}
RendererFamilies it writes
chio_kernel::render_guard_metrics_prometheus
The seven guard families, the two OTel drop counters, chio_signing_queue_block_total, chio_settlement_unresolved_total, chio_ambiguous_dispatch_retained_hold_total, and the two receipt watchdog gauges.
chio_http_core::metrics::render_http_core_metrics_prometheus
chio_guard_evaluations_total over guard="http_authority" and its three outcomes, and chio_kernel_decision_latency_seconds.
chio_metrics_spec::runtime::render_alert_pack_families
The eight families the alert rules burn against: fail-open, dispatch failure, revocation lag, DLQ depth, SOC export count and lag, alert dispatch count and latency.

The receipt watchdog gauges are chio_receipt_uncheckpointed_seq_range and chio_receipt_seconds_since_last_checkpoint. The second measures checkpoint progress, deliberately: in a store where writes keep committing while checkpointing stalls, a commit-based gauge would read near zero and the staleness alert would never fire.

The three guard descriptor tables

chio_wasm_guards::metrics declares three static descriptor tables, and the table a registry serves is a constructor argument. GuardMetricRegistry::with_families(families, max_guards) is the one constructor; new() and with_max_guards(n) wrap it with GUARD_METRIC_FAMILIES. That first table holds seven of the thirteen declared families and none of the tenant-labelled ones.

TableServed byOn the sidecar scrape
GUARD_METRIC_FAMILIES
GuardMetricRegistry::new(), with_max_guards(n), register_guard_metric_families()
Yes, through the kernel renderer
GUARD_POOL_METRIC_FAMILIES
register_guard_pool_metric_families() and GuardPoolMetrics, held by the wasmtime guard pool
No; read per tenant through GuardPoolMetricsSnapshot
RUNTIME_METRIC_FAMILIES
A registry built with with_families
Each of the three, through the kernel renderer

The seven guard families

GUARD_METRIC_FAMILIES, each labelled with guard_id. A guard can register metrics of its own; see Custom Guards.

MetricKindLabelsUnit
chio_guard_eval_duration_seconds
Histogram
guard_id, verdict
seconds
chio_guard_fuel_consumed_total
Counter
guard_id
fuel units
chio_guard_verdict_total
Counter
guard_id, verdict
count
chio_guard_deny_total
Counter
guard_id, reason_class
count
chio_guard_reload_total
Counter
guard_id, outcome
count
chio_guard_host_call_duration_seconds
Histogram
guard_id, host_fn
seconds
chio_guard_module_bytes
Gauge
guard_id, epoch
bytes

The three pool families

GUARD_POOL_METRIC_FAMILIES, each carrying guard_id and tenant_id. A blank tenant folds to unknown and an over-cap tenant to __overflow__, so the label domain stays finite whatever a caller passes.

MetricKindUnit
chio_guard_pool_checkout_total
Counter
count
chio_guard_pool_warm_size
Gauge
instances
chio_guard_pool_evict_total
Counter
count

The three runtime families

RUNTIME_METRIC_FAMILIES: the signing-queue counter and the two OTel drop counters. The grafana/chio-perf.json dashboard plots all three.

MetricKindLabelsUnit
chio_signing_queue_block_total
Counter
reason
count
chio_otel_ingress_drop_total
Counter
none
count
chio_otel_sink_drop_total
Counter
none
count

Histogram buckets

Both histogram families use one fixed bucket set for every guard, so cross-guard quantiles compare without per-guard configuration.

crates/guards/chio-wasm-guards/src/metrics.rsrust
pub const EVAL_DURATION_BUCKETS_SECONDS: &[f64] = &[
    0.0001, 0.0005, 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0,
];

pub const HOST_CALL_DURATION_BUCKETS_SECONDS: &[f64] = &[
    0.00001, 0.00005, 0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1,
];

Label vocabulary

LabelConstantValues
verdict
VERDICT_LABEL_VALUES
allow, deny, rewrite, error
reason_class
REASON_CLASS_LABEL_VALUES
policy, pii, secret, prompt_injection, oversize, fuel, trap, malformed, other
host_fn
HOST_FN_LABEL_VALUES
log, get_config, get_time_unix_secs, fetch_blob
outcome
RELOAD_OUTCOME_LABEL_VALUES
applied, canary_failed, rolled_back

classify_deny_reason_class keeps chio_guard_deny_total{reason_class} finite. It substring-matches a guard's free-form deny reason and returns only a listed value, folding anything unrecognized or absent into other. Which denials carry the label at all is set out in Fail-Closed Semantics.

Cardinality limits

The registry caps unique guard ids at MAX_GUARD_METRIC_CARDINALITY = 1024. Beyond that, registration fails closed with GuardMetricRegistrationError code E_GUARD_METRIC_CARDINALITY_EXCEEDED, carrying the guard id, the attempted count, and the limit, and the guard is left out of the metric family. The stable 12-character guard id derives from the guard digest.

crates/guards/chio-wasm-guards/src/metrics.rs409-416rust
pub fn guard_id_label_from_digest(digest: &str) -> String {
    digest
        .strip_prefix("sha256:")
        .unwrap_or(digest)
        .chars()
        .take(12)
        .collect()
}

Two guards sharing their first 12 hex digits would collide, which stays negligible at 1,024 active guards. A fleet near the cap raises the limit with GuardMetricRegistry::with_max_guards(n) and keeps the full id length. The pool registry takes the same limit as a tenant ceiling through GuardPoolMetrics::with_max_tenants(n).

Stripping helpers

Two entries of the denylist are re-exported from chio_kernel::otel; ATTR_CHIO_REPLAY_RUN_ID and ATTR_CHIO_TENANT_ID are declared beside the array. A caller that pushes spans to a Prometheus-shaped sink of its own calls strip_denied_span_attributes before the push.

crates/observability/chio-otel-receipt-exporter/src/lib.rs25-28rust
pub use denylist::{
    denied_attribute_keys, is_denied_attribute, strip_denied_attributes,
    strip_denied_batch_attributes, strip_denied_span_attributes, PROMETHEUS_DENIED_ATTRIBUTES,
};

The receipt exporter crate

chio-otel-receipt-exporter turns OTLP trace ingress into signed receipts. The kernel host's serve mode runs its OTLP gRPC ingress listener when one is configured, and a Rust caller can embed the same two pieces:

  • OtlpGrpcIngress. Accepts OTLP/gRPC trace exports in a narrow Rust representation. BoundedOtlpGrpcIngress adds a queue with the drop counters the scrape reports.
  • ReceiptStoreSink. Builds one ChioReceipt per OTLP span, signs it with ReceiptStoreSinkConfig.signing_keypair, and appends it to the configured Arc<dyn ReceiptStore>.

The sink's public API in crates/observability/chio-otel-receipt-exporter/src/sink.rs is three methods: new(store, config), export_traces(&OtlpGrpcTraceExport) returning a ReceiptStoreSinkSummary, and receipt_for_span(&OtlpSpan) returning one ChioReceipt. Both fallible methods return OTelReceiptExportError.

src/main.rs · in mainrust
use chio_otel_receipt_exporter::{ReceiptStoreSink, ReceiptStoreSinkConfig};

let config = ReceiptStoreSinkConfig {
    signing_keypair,                       // Ed25519 keypair
    policy_hash: "otel-receipt-exporter".into(),
    default_capability_id: "otel-ingress".into(),
    default_tool_server: "otel-collector".into(),
    default_tool_name: "gen_ai.tool.call".into(),
    tenant_id: Some("prod".into()),
};

let sink = ReceiptStoreSink::new(receipt_store, config);
let summary = sink.export_traces(&otlp_export)?;
println!("appended {} receipts", summary.appended_receipts);

The five string defaults are the ones ReceiptStoreSinkConfig::new fills in, with tenant_id defaulting to None. Each receipt carries the sanitized attributes in a ToolCallAction and goes through the same ReceiptStore trait the kernel uses. The sink validates trace_id and span_id and calls strip_denied_attributes on every span before signing, so the canonicalized form is stable across re-export. A retried partial batch re-appends only its unappended receipts, with the same ids.

Span and log fields the dashboards expect

deploy/dashboards/README.md lists the fields every shipped dashboard expects on spans and log lines: chio.receipt.id, chio.tenant.id, chio.policy.ref, chio.verdict, chio.tee.mode, chio.deny.reason, chio.guard.outcome, provenance.otel.trace_id, provenance.otel.span_id, redaction_pass_id, and redaction_elapsed_micros.

Log levels and fields

  • tracing::trace!. Per-step evaluation state. Off in production.
  • tracing::debug!. Per-call detail for local development. Off in production except while investigating.
  • tracing::info!. Lifecycle: kernel started, policy reloaded, checkpoint written, revocation propagated.
  • tracing::warn!. Recoverable degradation and fail-closed fallbacks: cache-miss escalation, a guard rejecting invalid config and constructing a deny-all fallback, external-guard call failures, advisory verdicts.
  • tracing::error!. Fail-closed events that produce Verdict::Deny through the fallback path: caught panics, poisoned mutexes, signing failures, an unreachable store.

Guards share no fixed field set beyond guard. Each attaches the fields that describe its decision, and many deterministic guards, forbidden-path among them, emit no tracing at all; their evidence lives in the receipt. Two representative call sites:

crates/guards/chio-guards/src/code_execution.rsrust
tracing::warn!(
    guard = "code-execution",
    module = %name,
    "denying code execution: dangerous module detected"
);
crates/guards/chio-guards/src/content_review.rsrust
tracing::warn!(
    guard = "content-review",
    service = %service,
    endpoint = %endpoint,
    detected_categories = ?categories,
    "content-review denied outbound message"
);

The guard value identifies the component (code-execution, content-review, forbidden-path), never an individual call. Every other field (module, service, endpoint, code_len) is guard-specific, so expect no decision, path, or reason field on an arbitrary guard's events.

The shipped Grafana dashboards

deploy/dashboards/README.md documents five dashboards. All five expect the fields above and use the datasource variables DS_LOKI, DS_TEMPO, and DS_JAEGER, so import works across stacks. The import loop globs every *.json under deploy/dashboards/, so it also installs grafana/chio-perf.json (signing-queue blocking and OTel drop counters) and chio-pheromone-relay-observability.json (relay retry queue, dead letters, stale leases, bounded rejections).

loki/chio-tee.json

"Chio TEE Loki receipt view", on Loki. Four panels: TEE records by verdict, a TEE mode and verdict density heatmap, the raw log stream, and a receipt-to-trace lookup table (top 50 by receipt id, with policy ref, verdict, and deny reason). Filters by receipt id, tenant id, trace id, verdict, and TEE mode. See Confidential Node for the TEE signals.

loki/verdict-drift.json

"Chio replay verdict drift", on Loki. Filters chio-replay logs to event="verdict_drift" and renders a heatmap of from_verdict × to_verdict, a stacked time series of reason_delta, a top-100 drift records table, and a raw drift log stream.

jaeger/receipt-span-lookup.json

"Chio Jaeger receipt span lookup", on Jaeger. Pivots from receipt id, trace id, span id, tenant id, and tool name to traces. Four panels: trace search by tags, a table of receipt and span lookups, traces by verdict over time, and trace duration over time.

tempo/span-timeline.json

"Chio Tempo span timeline", on Tempo. The primary panel runs the TraceQL query from step 3 to fetch every trace that touched a receipt. Further panels: a span rows table, a GenAI tool-call rate time series, and span duration by verdict.

tempo/redaction-latency.json

"Chio Tempo redaction latency", on Tempo. Tracks redaction-pass latency by redaction_pass_id and chio.guard.outcome. Four panels: p95 latency, a latency distribution histogram, a redaction pass spans table, and span rate.

Recording rules

Burn-rate alerts fire on ratios, so the ratios are recorded first, in one group at a 30-second interval. clamp_min(..., 1) in the denominator keeps a quiet window from dividing by zero and reporting a false 100% error ratio.

deploy/prometheus/chio-recording-rules.yml1-16yaml
groups:
  - name: chio-sre-recording
    interval: 30s
    rules:
      - record: chio:decision_latency:histogram_quantile_p95_5m
        expr: |
          histogram_quantile(
            0.95,
            sum by (surface, outcome, le) (rate(chio_kernel_decision_latency_seconds_bucket[5m]))
          )

      - record: chio:receipt_write_error_ratio_5m
        expr: |
          sum(rate(chio_receipt_write_total{outcome="error"}[5m]))
          /
          clamp_min(sum(rate(chio_receipt_write_total[5m])), 1)
recordOver
chio:decision_latency:histogram_quantile_p95_5m
chio_kernel_decision_latency_seconds_bucket, by surface and outcome
chio:receipt_write_error_ratio_5m / _1h / _6h
chio_receipt_write_total
chio:sidecar_error_ratio_1h / _6h
chio_sidecar_requests_total
chio:alert_dispatch_error_ratio_1h / _6h
chio_alert_dispatch_total
chio:soc_export_error_ratio_1h / _6h
chio_soc_export_total
chio:anchor_round_latency:histogram_quantile_p95_5m
chio_anchor_round_latency_seconds_bucket, by witness and outcome
chio:federation_hop_latency:histogram_quantile_p95_5m
chio_federation_hop_latency_seconds_bucket, by result
chio:guard_eval_latency:histogram_quantile_p95_5m
chio_guard_eval_duration_seconds_bucket, by guard and verdict
chio:alert_dispatch_latency:histogram_quantile_p95_5m
chio_alert_dispatch_latency_seconds_bucket, by route and outcome
chio:receipt_uncheckpointed_seq_range:max_5m
max_over_time(chio_receipt_uncheckpointed_seq_range[5m])

The one-hour and six-hour pairs exist because each burn-rate alert requires both windows over budget at once. The five-minute receipt-write ratio and the two _5m gauge and latency rollups feed dashboards and SOC export rather than a paging rule.

Burn-rate SLO alerts

Each SLO uses two windows: the short one catches a fast burn, the long one suppresses flapping. The receipt-write rule in step 5 sets the shape. ChioSidecarErrorBudgetBurn1h uses it with a 0.5% budget at p2, and ChioAlertDispatchErrorBudgetBurn1h and ChioSocExportErrorBudgetBurn1h with 1% each. The 14.4 and 6 multipliers are the standard fast and slow burn factors, applied to whichever budget the SLO holds.

Missing-data and zero-tolerance alerts

A silent exporter looks identical to a healthy one on a ratio, so each burn-rate alert has an absent_over_time watchdog. Some conditions carry no budget at all and page on the first occurrence.

deploy/prometheus/chio-alert-rules.yml112-122yaml
- alert: ChioFailOpenSuspected
  expr: increase(chio_fail_open_suspected_total[5m]) > 0
  for: 0m
  labels:
    severity: p0
    notification_route: pagerduty
    opsgenie: "true"
    slo: fail-closed
  annotations:
    summary: Chio observed a suspected fail-open path
    runbook: docs/operator-runbook/slo.md

The whole pack, with its labels

Fourteen rules in one group. The labels are the handoff to the chio-siem and chio-wall PagerDuty and OpsGenie dispatch path: severity, notification_route, and slo are on every rule, and three rules carry no opsgenie label, so a dispatcher that keys on opsgenie alone would drop those three.

AlertFires onseveritynotification_routeopsgenie
ChioReceiptWriteErrorBudgetBurn1h
1h and 6h ratios both over 0.1% budget, for 2m
p1
pagerduty
yes
ChioReceiptWriteMetricsMissing
absent_over_time(chio_receipt_write_total[10m])
p1
pagerduty
yes
ChioSidecarErrorBudgetBurn1h
both windows over 0.5% budget, for 2m
p2
pagerduty
yes
ChioSidecarMetricsMissing
absent_over_time(chio_sidecar_requests_total[10m])
p2
pagerduty
yes
ChioAlertDispatchErrorBudgetBurn1h
both windows over 1% budget, for 2m
p1
pagerduty
yes
ChioAlertDispatchMetricsMissing
absent_over_time(chio_alert_dispatch_total[10m])
p1
pagerduty
yes
ChioSocExportErrorBudgetBurn1h
both windows over 1% budget, for 5m
p2
opsgenie
yes
ChioSocExportMetricsMissing
absent_over_time(chio_soc_export_total[10m])
p2
opsgenie
yes
ChioFailOpenSuspected
any increase in chio_fail_open_suspected_total, for 0m
p0
pagerduty
yes
ChioFailOpenMetricsMissing
absent_over_time(chio_fail_open_suspected_total[10m])
p1
pagerduty
yes
ChioDispatchFailure
any increase in chio_dispatch_failure_total, for 2m
p1
pagerduty
yes
ChioDispatchFailureMetricsMissing
absent_over_time(chio_dispatch_failure_total[10m])
p2
pagerduty
no
ChioRevocationLagHigh
p95 revocation lag above 30 seconds, for 10m
p1
pagerduty
yes
ChioRevocationLagMetricsMissing
absent_over_time(chio_capability_revocation_lag_seconds_count[10m])
p2
pagerduty
no
ChioReceiptCheckpointStale
chio_receipt_seconds_since_last_checkpoint > 86400, for 15m
p2
pagerduty
no

Seven *MetricsMissing rules cover the four burn-rate SLOs and three of the four zero-tolerance rules; ChioReceiptCheckpointStale pages on its own threshold. Every rule carries a summary and a runbook annotation pointing at docs/operator-runbook/slo.md.

A third file, deploy/prometheus/chio-pheromone-relay-observability-rules.yml, defines the signal relay's operator alerts with the same severity, notification_route, and opsgenie labels: ChioPheromoneRelayDeadLetters, ChioPheromoneRelayRetryPressure, ChioPheromoneRelayStaleLeases, and ChioPheromoneRelayReplayStorm.

Source files

The facts above come from crates/products/chio-api-protect/src/proxy/router.rs, crates/guards/chio-wasm-guards/src/metrics.rs, crates/observability/chio-metrics-spec/src/runtime.rs, crates/observability/chio-otel-receipt-exporter/src/, deploy/dashboards/README.md, and deploy/prometheus/.

Node Observability · Chio Docs