Files
rippled/docker/telemetry/tempo.yaml
Pratik Mankawde 41b818b55b feat(telemetry): join a ledger's spans into one trace, add round histogram (WP-B3)
A slow fresh-sync ledger produced spans scattered across threads with no
way to relate them. They now share a trace id derived from the ledger's own
hash, the one value every participating site already holds, so nothing new
is plumbed across threads. This is the pattern the transaction pipeline
already uses for its tx id.

Joined: ledger.validate, ledger.store, and a new
consensus.validation.accept recorded when a trusted validation arrives. In
Tempo, searching one ledger hash returns them together, so an operator can
tell whether the ledger was slow to arrive, slow to be accepted, or slow to
be stored. They are siblings rather than a chain because the accept gate is
entered from three different threads, so no fixed parent order exists.

consensus.validation.accept also records why an arriving validation did or
did not advance the gate, which makes "validations arrive but are all
rejected" visible for the first time.

consensus_round_duration_ms turns the existing round-time span attribute
into a histogram, so a fleet trend needs a metric query rather than raw
trace inspection. An explicit bucket view is required, not optional: the
SDK default tops out at ten seconds while consensus abandons a round at two
minutes, so slow rounds would all fall in one bucket and every quantile
would read exactly ten seconds. Cost is one record per round.

Record layer: the histogram is native and needs no collector change. The
two new bounded attributes are added as span-metric dimensions to both
collector configs. The ledger hash stays out of them, since a per-ledger
dimension mints a series per ledger; it is indexed in Tempo as the join key.

The ledger.acquire span is not joined yet, because that file was being
changed concurrently. It is registered as an optional member of the join
group so nothing fails, and switching it is a one-line follow-up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 20:19:25 +01:00

143 lines
5.7 KiB
YAML

# Grafana Tempo configuration for xrpld telemetry stack.
#
# Runs in single-binary mode for local development.
# Receives traces via OTLP/gRPC from the OTel Collector and stores
# them locally. Queryable via Grafana Explore using the Tempo datasource.
#
# Search filters are configured on the Grafana datasource side
# (grafana/provisioning/datasources/tempo.yaml). Tempo auto-indexes
# all span attributes for search in single-binary mode.
#
# For production, replace local storage with S3/GCS backend and adjust
# retention via the compactor settings. See:
# https://grafana.com/docs/tempo/latest/configuration/
stream_over_http_enabled: true
server:
http_listen_port: 3200
# Raise the TraceQL metrics query range limit. The default
# query_frontend.metrics.max_duration is 3h, so a dashboard set to a longer
# window (e.g. 6h/12h) fails with "range exceeds 3h0m0s". 168h matches the
# search max_duration and gives dashboards generous headroom.
query_frontend:
metrics:
max_duration: 168h
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
ingester:
max_block_duration: 5m
compactor:
compaction:
block_retention: 1h
# Enable metrics generator for service graph, span metrics, and the
# local-blocks processor. Produces RED metrics (rate, errors, duration) per
# service/span for the service map, and keeps recent trace blocks queryable so
# TraceQL metrics queries (quantile_over_time, count_over_time, etc. via
# /api/metrics/query_range) work.
metrics_generator:
registry:
external_labels:
source: tempo
storage:
path: /var/tempo/generator/wal
# Uncomment and add a Prometheus service to docker-compose.yml
# to enable remote_write for service graph metrics:
# remote_write:
# - url: http://prometheus:9090/api/v1/write
# Separate WAL the local-blocks processor flushes traces to for metrics
# queries. Required when flush_to_storage is true.
traces_storage:
path: /var/tempo/generator/traces
processor:
local_blocks:
# xrpld consensus/transaction spans are SPAN_KIND_INTERNAL. By default
# local-blocks keeps only server spans for TraceQL metrics, so attribute
# aggregations over internal spans return nothing. Keep all spans.
filter_server_spans: false
# Flush recent blocks to traces_storage so query_range can read them.
flush_to_storage: true
overrides:
defaults:
metrics_generator:
processors:
- service-graphs
- span-metrics
- local-blocks
storage:
trace:
backend: local
wal:
path: /var/tempo/wal
local:
path: /var/tempo/blocks
block:
# Give the highest-value span attributes their own Parquet columns so
# TraceQL scopes a search to one ledger without scanning the generic
# attribute list. Tempo indexes every attribute for search regardless;
# a dedicated column only changes the cost of filtering on it.
#
# ledger_hash is here rather than in the collector's spanmetrics
# `dimensions:` on purpose: it is a per-ledger value, so as a metric
# dimension it would mint one series per ledger acquired (unbounded
# cardinality). As a trace column it gives the same per-ledger lookup at
# zero metric cost -- which is how a single stuck or abandoned
# ledger.acquire is found: {name="ledger.acquire" && span.outcome=
# "abandoned" && span.ledger_hash="<hash>"}.
#
# txset_hash and remote_endpoint are here for the same reason and by the
# same rule: a tx-set root hash is one value per consensus round and a
# peer endpoint is one value per address dialled, so either as a
# spanmetrics dimension would mint an unbounded number of metric series.
# As trace columns they answer the per-object questions the metrics
# cannot -- which proposed set stalled a round:
# {name="txset.acquire" && span.outcome="timeout" &&
# span.txset_hash="<hash>"}
# and which peer a dial keeps failing against:
# {name="peer.dial" && span.outcome="tcp_fail" &&
# span.remote_endpoint="10.0.0.5:51235"}
#
# missing_nodes, served_nodes and duration_ms are deliberately NOT given
# columns. They are unbounded integers, so they are useless as equality
# filters; Tempo indexes every attribute for search regardless, so they
# remain queryable by range (e.g. span.missing_nodes > 0) at no
# storage cost, which is how they are actually read.
#
# The ledger_hash column below carries a second job as of the per-ledger
# trace join (WP-B3), which is why it stays the highest-value column here.
# Every span for one ledger -- ledger.acquire, consensus.validation.accept,
# ledger.validate, ledger.store -- now derives its TRACE ID from that same
# ledger hash, so the whole story of one slow ledger is one trace:
# {span.ledger_hash="<hash>"}
# returns all of them across the three threads that produced them. The
# column is what makes that lookup cheap; it also stays the way a reader
# confirms two spans really are the same ledger rather than a trace-id
# coincidence, since the attribute holds the full 32 bytes while the trace
# id is only the leading 16.
#
# validation_status and accept_gated get no column: both are small closed
# value sets promoted to spanmetrics dimensions instead (see the collector
# configs), so the aggregate question is answered by a metric query and no
# per-value trace lookup is needed.
parquet_dedicated_columns:
- name: ledger_hash
type: string
scope: span
- name: txset_hash
type: string
scope: span
- name: remote_endpoint
type: string
scope: span