Files
rippled/.github/scripts/otel-naming
Pratik Mankawde 3153f3ef56 docs(telemetry): align runbook and plan docs with the shipped phase-9/10 code
The reference docs had drifted from the code in ways that break the reader
rather than merely misinform: PromQL examples that return no data, a rollback
flag that is a no-op, a sampling knob that does not exist, and two span parents
that moved. Code is treated as the truth throughout; where the code is the
defective side, the doc now records it as a known issue instead of describing
the bug as intent.

Renames the docs missed: histogram names gain the exporter's unit suffix
(ios_latency_milliseconds_bucket and four siblings), ledger_history_mismatch
gains _total, the StatsD-era quantile label gives way to le buckets,
rpc.request becomes rpc.http_request, traces_spanmetrics_calls_total becomes
span_calls_total, and the nine dotted xrpl.* span attributes are recorded as
renamed rather than left as live keys.

Re-parenting: consensus.update_positions and consensus.check are children of
consensus.establish, not of consensus.round.

Units and labels: state_accounting_*_duration is microseconds, not seconds;
cache_metrics label values are case-sensitive; object_count carries demangled
C++ type names. Nodestore read and write latency stays microseconds -- the
nanosecond accumulator change did not move the exported unit.

Adds what shipped but was undocumented: the ledger.acquire span, seven
consensus.round events, twelve span attributes, node_writes_duration_us, the
7-day validation-agreement window, the TxQ admission and reduce-relay metric
families, metrics_endpoint, and the phase-10 validation workflow.

Corrects claims that never held: 10% head sampling (it is fixed at 100%),
configurable redaction (it is unconditional), -DXRPL_ENABLE_TELEMETRY=OFF
(the flag is -Dtelemetry=OFF, default ON), FindOpenTelemetry.cmake and the
xrpl_telemetry target (neither exists), Promtail and a StatsD exporter in the
pipeline (neither exists), and Loki stream selection on job= (only
service_name is a stream label).

Phase 9 is marked complete, its provisioned alerting is attributed to the
branch that shipped it, and Phase 11 stays at zero except the one prerequisite
its code closes. Counts are reconciled repo-wide: 41 emitted span families,
15 dashboards on disk with 14 asserted, 13 alert rules in 5 groups.

Hardens the gate that let this drift through: Rule E of the naming check now
covers the reference docs, its allow-dotted marker is key-scoped and warns on
stale or empty use, a missing checked file is reported instead of silently
skipped, the test suite runs in CI, and doc paths trigger the check.

C++ and CMake changes are comment-only: three MetricsRegistry instrument names,
eight OTelCollector claims of a metric-name prefix that formatName never adds,
and the telemetry option's inverted default.
2026-08-13 18:55:32 +01:00
..

OTel naming-consistency check

check_otel_naming.py enforces the OpenTelemetry span-attribute naming convention documented in CONTRIBUTING.md across every layer of the telemetry pipeline. The *SpanNames.h constants are the single source of truth (L1); every other layer must agree with them.

Running locally

python .github/scripts/otel-naming/check_otel_naming.py

It takes no arguments, can be run from any directory inside the repo, and uses only the Python standard library (no pip install, matching the levelization check). A non-zero exit code means a violation was found; the output lists each violation as RULE | location | token | expected.

What it checks

The valid key set is derived dynamically from the OTel code — there is no hardcoded allowlist:

  • L1 keys come from the namespace attr { ... } blocks of every *SpanNames.h, resolving the makeStr("x") / join(seg::a, seg::b) DSL (cross-file, so join(seg::rpc, ...) resolves seg::rpc from the base SpanNames.h). Each constant is resolved against its own header, so two headers that define a same-named constant (e.g. a base attr::ledgerHash and a domain attr::ledgerHash) each contribute their real wire key — a later header cannot clobber an earlier one's value in a flat table.
  • Legitimate dotted keys = ONLY the keys the code actually sets as resource attributes, i.e. the entries inside Telemetry.cpp's Resource::Create({...}) call: the semconv::service::* keys (service.*) plus any attr::<name> constants passed there (xrpl.network.*). A dotted key that is declared in a header but never set as a resource attr is a span attribute in resource clothing — a Rule-A violation, even if it lives in the base SpanNames.h.

Rules (each fails the build, when its inputs are present)

Rule Check
A No stray dotted span-attribute key (only the derived resource keys may be dotted).
G Attribute keys are lower_snake_case (^[a-z][a-z0-9_]*$ per dot-segment) — no camelCase, UPPERCASE, or spaces.
F No string literals as attribute keys or span-name arguments in setAttribute/addEvent/span/rootSpan/childSpan (rootSpan shares span's (cat, prefix, name) signature). Attribute values are exempt (runtime data); *SpanNames.h definitions and test files are exempt.
B Every collector spanmetrics.dimensions name exists in the L1 key set.
C Every Tempo span-filter tag exists in the L1 key set.
D Every dashboard label resolves to an L1 span attribute, a native-metric label (L6, emitted by MetricsRegistry), or a Prometheus/Grafana builtin. TraceQL scope prefixes (span./resource./…) are stripped before the L1 lookup.
E No dotted xrpl.<domain>.<field> attribute key in the runbook (only the L1 resource attrs xrpl.network.* may be dotted). Span names, filenames, OTel-standard keys, and metric labels are not flagged.

Rule F runs unconditionally (it is a purely syntactic check on the call-sites and needs no *SpanNames.h), so a code path that calls SpanGuard::span/setAttribute directly without ever defining a header is still caught.

Warnings (printed, never fail the build)

Rule Check
H A namespace-qualified constant (e.g. foo::bar::myKey) used at a telemetry call-site is not defined in any *SpanNames.h. The constant should live in the proper header; defining it in-place bypasses rules A/G/F. Warns rather than fails — the argument may be a legitimately dynamic value, and the header may live on a later branch. Bare locals and std:: names are not warned.

Presence-gated

Every rule runs only when the source files it needs are present in the tree and is otherwise skipped (printed as SKIP: <rule> — <reason>), never failed. This keeps the check correct no matter how telemetry work is split across PRs — a stacked chain, one large PR, or independent per-stage PRs where (for example) the collector config lands before the dashboards. The collector/Tempo/dashboard/ runbook layers are introduced in later phases; on a branch without them, only the L1-intrinsic rules (A, G, F) run.