Skip to content

feat: OTel integration in XRPLD - #7770

Draft
pratikmankawde wants to merge 2481 commits into
developfrom
pratik/otel-phase10-workload-validation
Draft

feat: OTel integration in XRPLD#7770
pratikmankawde wants to merge 2481 commits into
developfrom
pratik/otel-phase10-workload-validation

Conversation

@pratikmankawde

@pratikmankawde pratikmankawde commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

PR Chain: Phase-1aPhase-1bPhase-1cPhase-2Phase-3Phase-4Phase-5Phase-6Phase-7Phase-8Phase-9Phase-10
Base: develop
This PR is the consolidated roll-up of the twelve phase PRs above.

High Level Overview of Change

Full OpenTelemetry integration for xrpld: distributed traces, native OTel metrics, and log-trace correlation, with a companion observability stack (Collector, Tempo, Prometheus, Loki, Grafana) and a workload harness that proves the end-to-end signal path.

Grafana dashboards

No need to review tasklist files.

Context of Change

xrpld had StatsD counters and free-form logs — no traces, no structured attributes, and no way to tie a slow RPC to the consensus round it landed in.

  • Traces — 41 span types across RPC/gRPC, transaction receive → apply stages → queue, ledger build/validate/store/acquire, the full consensus round, peer ingress and path-finding. W3C trace context propagates across both RPC and peer boundaries via an additive TraceContext protobuf message; consensus rounds share a deterministic trace ID so all nodes in a round land in one trace.
  • MetricsOTelCollector implements beast::insight::Collector on the OTel Metrics SDK, replacing the StatsD hop; MetricsRegistry adds the values that previously existed only in get_counts / server_info / TxQ / PerfLog, reaching parity with the external push_metrics.py.
  • Logstrace_id / span_id on log lines, ingested into Loki 3.x via OTLP, with two-way Grafana links to Tempo.
  • Ops surface — 15 provisioned dashboards, 13 alert rules grouped by (service_instance_id), a Grafana Cloud export path, an operator runbook and a glossary.
  • Validation — a 5-node workload cluster plus scripted Tempo/Prometheus assertions and a timing regression gate, run in CI.

Delivered as ten reviewed phases (see the chain above); plan docs live under OpenTelemetryPlan/. Reviewing the phase PRs in order is far easier than reading this diff.

Enablement — the build option telemetry is currently ON so CI keeps compiling this code, but a stock node emits nothing: the runtime [telemetry] enabled key defaults to 0, and metrics need [insight] server=otel.

API Impact

  • Public API: New feature
  • Public API: Breaking change
  • libxrpl change — adds telemetry primitives (SpanGuard/ScopedSpanGuard, *SpanNames.h, Redaction, OTelCollector) and instrumentation-only changes to nodestore, JobQueue and PerfLog; no existing symbol changed.
  • Peer protocol change — additive and backward compatible: optional TraceContext message, field 1001 reserved on TMTransaction / TMProposeSet / TMValidation. Peers that never set it are unaffected.

No wire-format or JSON-RPC schema change.

Test Plan

  • GTest suites for the telemetry primitives, propagator, redaction, metric registry and nodestore stats.
  • docker/telemetry/workload/run-full-validation.sh brings up 5 validators plus the full stack, drives RPC/transaction load, and asserts every expected span, attribute and metric emits.
  • Timing regression gate diffs span and job-queue quantiles against a committed baseline.
  • Alert rules provision with no rule in error health.

@pratikmankawde pratikmankawde changed the title Pratik/otel phase10 workload validation feat: OTel integration in XRPLD Jul 9, 2026
@pratikmankawde pratikmankawde added DraftRunCI Normally CI does not run on draft PRs. This opts in. DistributedTracingAndObservability Distributed Tracing And Observability related changes labels Jul 9, 2026
@pratikmankawde

Copy link
Copy Markdown
Contributor Author

/ai-review

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues.

Review by Claude Sonnet 4.6 · Prompt: V15

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

Copy link
Copy Markdown

This PR has conflicts, please resolve them in order for the PR to be reviewed.

@github-actions

Copy link
Copy Markdown

All conflicts have been resolved. Assigned reviewers can now start or resume their review.

@github-actions

Copy link
Copy Markdown

This PR has conflicts, please resolve them in order for the PR to be reviewed.

@pratikmankawde

Copy link
Copy Markdown
Contributor Author

/ai-review

@pratikmankawde pratikmankawde left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full review of the OTel integration. Overall quality is high: span cardinality is mostly bounded by design, lock ordering and shutdown sequencing are reasoned about explicitly, and tests assert exact values on both positive and negative paths.

Findings are posted inline, grouped by severity:

  • High (1): raw WS command reaches a spanmetrics dimension (unbounded Prometheus cardinality) — ServerHandler.cpp
  • Medium (5): dual metric pipelines with divergent resource attributes; per-tx span events truncating at the SDK limit; stale CI path filters; telemetry default-ON vs. contradicting comment; internal planning docs in the PR
  • Low (6): doc contradictions, detachCallbacks() guarantee overstated, inconsistent span activation on peer receive paths, repairWindowEntry linear scan, unguarded findByType, inconsistent hash-size policy

Verified as sound (no action needed): JobQueue collect() snapshot pattern avoiding lock-order inversion; PerfLog signature change propagated to all implementations and tests; rpcEnd early-return fix; RPC method/job handler label bounding; Application shutdown ordering (detachCallbacks → services → registry stop → telemetry stop); secrets hygiene in docker/telemetry; GTest suites.

Recommended pre-merge priority: fix the WS command bounding first (blocking), then decide the telemetry build default, align the two metric pipelines' resources, fix the CI filters, and drop the planning docs.

Comment thread src/xrpld/rpc/detail/ServerHandler.cpp Outdated
Comment thread src/xrpld/telemetry/MetricsRegistry.cpp Outdated
Comment thread src/xrpld/app/consensus/RCLConsensus.cpp Outdated
Comment thread .github/workflows/telemetry-validation.yml Outdated
Comment thread CMakeLists.txt
Comment thread src/xrpld/telemetry/MetricsRegistry.h
Comment thread src/xrpld/overlay/detail/PeerImp.cpp Outdated
Comment thread src/xrpld/overlay/detail/PeerImp.cpp Outdated
Comment thread src/xrpld/telemetry/detail/ValidationTracker.cpp
Comment thread src/libxrpl/tx/Transactor.cpp Outdated

@pratikmankawde pratikmankawde left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up to my earlier review: an in-depth security and performance impact pass focused on the peer/client attack surface, data leakage, and hot-path costs. New findings are inline (2 security, 2 performance). Two items have no single code anchor, so they live here:

Benchmark evidence should gate the merge (Medium). All overhead claims in this PR are design-level: the disabled-at-runtime path is verified allocation-free (one atomic load + one virtual call per span site), and enabled-mode volume/queue sizing looks bounded on paper. But the PR ships docker/telemetry/workload/benchmark.sh precisely to prove this. Suggest attaching its output to the PR before merge: (a) develop vs. compiled-in-but-disabled (expect noise-level delta), and (b) enabled under mainnet-shaped load (expect low single-digit % CPU).

Runbook note on peer trace parentage (Low). trace_context from peers is advisory and unauthenticated by design (correctly outside the signed payload, strictly shape-validated, memory-safe). Since the overlay is permissionless, any peer can seed arbitrary trace IDs that telemetry-enabled nodes adopt for their receive spans — forgeable parentage and Tempo trace pollution. TLS doesn't change this (the authenticated peer is the author). Worth an explicit runbook warning that cross-node trace parentage is untrusted data, and possibly a future option to honor it only from cluster/trusted peers.

Verified safe during this pass (no action): full span-attribute inventory contains no RPC bodies/secrets/seeds; log correlation is one-directional (trace IDs into logs only); zero new listening sockets (telemetry is outbound push only); peer-supplied trace context parsing is memory-safe with double validation; span/metric queues are bounded with drop-on-overflow; exporter supports one-way and mutual TLS via config; opentelemetry-cpp/1.26.0 pinned via Conan; CI workflows use workflow_dispatch/same-repo push only (no pull_request_target on self-hosted runners); metric callbacks read atomics/cache snapshots without heavyweight locks; ValidationTracker worst-case memory ~8 MB.

Comment thread src/libxrpl/telemetry/Redaction.cpp
Comment thread src/xrpld/overlay/detail/PeerImp.cpp Outdated
Comment thread src/xrpld/overlay/detail/PeerImp.cpp Outdated
Comment thread src/libxrpl/telemetry/Telemetry.cpp
@github-actions

Copy link
Copy Markdown

All conflicts have been resolved. Assigned reviewers can now start or resume their review.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

This PR has conflicts, please resolve them in order for the PR to be reviewed.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

All conflicts have been resolved. Assigned reviewers can now start or resume their review.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

This PR has conflicts, please resolve them in order for the PR to be reviewed.

NetworkOPs.cpp include block: develop replaced DeliveredAmount.h,
MPTokenIssuanceID.h and NFTSyntheticSerializer.h with the single
rpc/detail/SyntheticFields.h. Kept that and this branch's two telemetry
includes.
Two conflicts, both additions at the same spot:
NetworkOPs.cpp include block kept develop's rpc/detail/SyntheticFields.h
alongside this branch's telemetry/MetricsRegistry.h; .cspell.config.yaml
kept both new words.
kMetricExportInterval and kMetricExportTimeout were declared but the reader
options set the same values as literals, so both constants were unused. constexpr
at namespace scope has internal linkage, so clang reports them under
-Wunused-const-variable, which -Dwerr=ON makes fatal: it failed the compile on
ubuntu-clang-release-amd64 and macos-arm64-release, and clang-tidy as well.

Using them removes two magic numbers and keeps the comment that explains why the
interval is 1 s.
… range tests

clang-tidy reported six bugprone-unchecked-optional-access errors: the dataflow
analysis does not treat ASSERT_TRUE(x.has_value()) as establishing the
precondition, because the assertion's early return is hidden inside a macro. Three
sites now guard with a plain if + FAIL(), the form already used elsewhere in this
file and one the analysis does model.

The fourth site compares the whole optional instead. That covers both "was it
parsed" and "are the bounds right" in one exact assertion, and because it is
EXPECT rather than ASSERT every row of the table is now checked -- previously one
bad row returned from the test and hid the other five.

parseLedgerRange uses std::errc, which comes from <system_error>; the header
included only <charconv>, which misc-include-cleaner flagged.
…ser names

The commented [telemetry] block in cfg/xrpld-example.cfg documented 8 of the 14
keys the parser accepts. Add the six that were missing - service_instance_id,
use_tls, tls_ca_cert, batch_size, batch_delay_ms and max_queue_size - each with
the unit and default read from the parser, and rename the documented endpoint
key to traces_endpoint so it matches what makeTelemetrySetup reads.

use_tls is documented for what it does rather than what its name suggests: it
gates whether tls_ca_cert reaches the exporter as a CA bundle, while the scheme
of traces_endpoint is what selects TLS. The path is not opened during parsing,
so an unreadable file surfaces as an export failure at runtime.

05-configuration-reference.md named three symbols that do not exist:
setup_Telemetry, make_Telemetry and Section::value_or. Correct them to
makeTelemetrySetup, makeTelemetry and Section::valueOr.
…ers on

The Consensus Health template-variable table documented $node as resolving via
exported_instance. That dashboard defines $node as
label_values(target_info, service_instance_id) and its panels filter on
service_instance_id; exported_instance appears in it zero times.

exported_instance is a real label, but it belongs to the StatsD boards shipped
alongside, where Prometheus renames a scraped instance label that collides with
the target's own. Documenting it against an OTel dashboard pointed readers at
the wrong pipeline's label.
…dence keys

Addresses review findings on the native-metrics work.

StatsDCollector::onTimer drained the send buffer inside the polling_ gate. That
gate holds back hook handlers until the application's services are built, but
sendBuffers() is socket I/O. StatsDEventImpl derives only from EventImpl, so it
never enters metrics_ and posts straight to the buffer; its |ms timings piled up
before onCollectionReady and were dropped after onCollectionStopping. The drain
now runs every tick, and outside metricsLock_, so onCollectionStopping no longer
waits on a UDP flush.

TelemetryImpl's constructor left meterProvider_ set when initMetrics() threw.
initMetrics publishes globally as its last step, so a throw left getMeter()
callers holding a provider nothing else could reach. Reset it in the catch.

~ApplicationImp caught only std::exception around telemetry shutdown while the
callees reach third-party SDK code, so a foreign exception would have terminated
the process. Added a logging catch-all.

ValidationTracker's hard trim evicted by unordered_map bucket order. It now
evicts oldest-first, so the entry dropped under pressure is the one least likely
to still reconcile.

The GetMeter test restored the global meter provider only on the success path,
and ASSERT_TRUE early-returns past it. Uses xrpl::ScopeExit instead.

The hook debounce window is a named constant rather than a bare 500 in a
comparison, and the metric export cadence becomes operator-configurable through
metric_export_interval_ms and metric_export_timeout_ms. Both are range-checked:
the SDK warns and silently substitutes its own 60s/30s defaults when the timeout
is not below the interval, so an unchecked value would slow export rather than
speed it up. Parsing uses a signed representation because lexical_cast<uint32_t>
accepts a leading minus and wraps it.

Naming corrections: CollectorManager documented exported_instance, which no OTel
dashboard uses; node-health queried job_count where the exported name is
jobq_job_count; network-traffic and overlay-traffic-detail referenced an
undeclared DS_PROMETHEUS variable; the counter table omitted the _total suffix
the Prometheus exporter appends; the plan docs and task list carried an xrpld_
prefix formatName never applies; and OTelCollector::New()'s contract promised its
instanceId, serviceName and networkType arguments were read, contradicting the
definition that marks them unused.
…icsRegistry

The native-metrics pipeline built its own OTLP/HTTP exporter and set only the
URL, so an operator who enabled TLS got mutual TLS on the trace exporter and a
plaintext-configured exporter for metrics. cfg/xrpld-example.cfg promises TLS for
"the OTLP exporter connection" with no carve-out, and two exporters exist. The
exporter now reads the same four [telemetry] TLS keys the trace exporter does.

Its resource was also thinner than the trace resource: service.name was
hardcoded, and service.version, xrpl.network.id and xrpl.network.type were
absent. Because the collector promotes resource attributes to labels, an operator
setting service_name split their fleet - spans carried the configured name while
every XRPL_METRIC_ series still said xrpld, blanking native-metric panels in
every dashboard that filters on it. All four now come from config, and
xrpl.network.type is derived inside from network_id through the shared
networkTypeFromId so a caller cannot supply a mismatched pair.

start() and initExporterAndProvider() take a StartOptions aggregate rather than
growing to eleven positional parameters, seven of them same-typed strings where a
swap would compile silently and stamp the wrong label - the defect class this
change exists to remove. It is constructed at one production site, so replacing
it with Telemetry::Setup once that is exposed stays a single-site edit.

service.version, service.instance.id and xrpl.node.id are stamped only when
non-empty, keeping this pipeline's existing behaviour of omitting an attribute
rather than writing it blank.

Also corrects three plan-doc references that cited line numbers rather than
symbols; the reader line moved and the numbers differ per branch.
…integration

Both sides documented the same six [telemetry] keys, so the automatic merge
duplicated all of them. Resolved by keeping this branch's structure - which
already covers all 14 keys and groups them under TLS and batch-processor
headings - and folding in the corrections from the upstream side:

- endpoint is renamed to traces_endpoint, which is what the parser reads, and
  described as used verbatim including its signal path.
- use_tls no longer claims to enable TLS. The exporter's URL scheme selects
  TLS; this key only decides whether tls_ca_cert reaches it as a CA bundle.
- tls_ca_cert records that the path is not opened while the config is parsed,
  so an unreadable file shows up as an export failure rather than at startup.
- service_instance_id explains that it is normally left unset and filled in
  from the node public key during startup.

Section::value_or in 05-configuration-reference.md becomes Section::valueOr;
that member does not exist under the other spelling.
phase-6 corrected the Consensus Health template-variable table to name
service_instance_id, the label that dashboard actually filters on. This branch
removes that table entirely - the section is restructured around a
Prometheus-label reference and a pointer to the runbook - so the corrected row
has nothing to land in. Resolved by keeping the restructured section; phase-6's
fix remains correct for phase-6, where the table still exists.

The [telemetry] cfg block merged without conflict: the composed 14-key block
from upstream and this branch's metrics_endpoint, metric_export_interval_ms and
metric_export_timeout_ms entries coexist, 17 keys with one entry each.
…orrelation

Nine files conflicted. Resolutions, and why:

Telemetry.cpp - upstream carried its own initMetrics(), makeResource() and
makeMetricExporter(); this branch already has an initMetrics() that builds the
exporter inline and uses makeMetricsResource(), which stamps xrpl.node.id only
when it is already known. Keeping both would have defined initMetrics() twice.
Kept this branch's, then pointed its reader at setup_.metricExportInterval and
setup_.metricExportTimeout: upstream turned those constants into [telemetry]
keys and removed the old ones, so the previous spelling no longer resolves.

OTelCollector.h - kept upstream's parameter docs. This branch's text promised
instanceId, serviceName and networkType become resource attributes; the
constructor marks all three [[maybe_unused]] and the .cpp already says they are
not read.

CollectorManager.cpp - kept upstream's comment for the same reason.

node-health.json - kept this branch's 60 panels. Upstream's only change to the
file was job_count to jobq_job_count, which this branch already had.

cfg/xrpld-example.cfg - composed. Kept this branch's warning that
service_instance_id must be set explicitly for the metrics pipeline, took
upstream's traces_endpoint rename, and removed a duplicate metrics_endpoint
entry along with the claim that metrics derive from the traces URL by rewriting
the signal path. Nothing derives it; both metric exporters read
metrics_endpoint. 17 keys, one entry each.

05-configuration-reference.md - both sides misdescribed the parser. Kept this
branch's fuller text, corrected the endpoint default to traces_endpoint, and
replaced the "resolve their URL differently" table with what the code does now.

09-data-collection-reference.md, 06-implementation-phases.md,
Phase7_taskList.md - kept this branch's versions, which drop a metric that was
never implemented, correct the state encoding to 0-6, and rename nudb_bytes to
stored_object_bytes. Re-applied upstream's rpc_requests_total fix, which taking
this side had reverted.
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

All conflicts have been resolved. Assigned reviewers can now start or resume their review.

…json

The close-time span attributes name their unit and epoch:
close_time_ripple_epoch_s, parent_close_time_ripple_epoch_s and
close_time_self_ripple_epoch_s. The harness inventory still required the
unsuffixed keys, so the attribute checks for consensus.accept.apply and
ledger.build failed on every validation run while the spans themselves
were correct.

Rename the four required_attributes entries to the keys the code emits.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DistributedTracingAndObservability Distributed Tracing And Observability related changes DraftRunCI Normally CI does not run on draft PRs. This opts in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants