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.
47 KiB
Configuration Reference
Parent Document: OpenTelemetryPlan.md Related: Implementation Phases
5.1 xrpld Configuration
OTLP = OpenTelemetry Protocol | TxQ = Transaction Queue
5.1.1 Configuration File Section
The authoritative [telemetry] example lives in cfg/xrpld-example.cfg. Telemetry is disabled by default (enabled=0); enabling it turns on distributed tracing for transaction flow, consensus, and RPC calls, with traces exported to an OpenTelemetry Collector over OTLP. Head sampling is intentionally fixed at 1.0 (sample everything) and is not configurable — per-node head-sampling would produce broken/partial distributed traces, so volume reduction is delegated to the collector's tail sampling (see Section 7.4.2). Transaction trace IDs are always deterministic (trace_id = txHash[0:16]); there is no strategy switch for the transaction path. The full option reference follows.
service_instance_idis effectively required forbeast::insightmetrics — and only for those. Three producers resolve the instance id independently, and exactly one of them lacks a node-key fallback:
Producer Resource built by Unset service_instance_idyieldsTraces (and therefore all span_*metrics)Telemetry::start()Base58 node public key Native XRPL_METRIC_*(MetricsRegistry)MetricsRegistry::initExporterAndProvider()Base58 node public key beast::insight([insight] server=otel)TelemetryImplconstructorservice.instance.idabsent — no fallback
- Traces: the tracer resource is built in
Telemetry::start()(Telemetry.cpp:380-387), which runs afterApplicationImp::setup()has calledsetServiceInstanceId()(Application.cpp:1323) with the Base58 node public key. An unset key therefore still yields the node key. Thespanmetricsconnector derivesspan_calls_total/span_duration_milliseconds_*from those spans, so span metrics inherit the correct id too.- Native
XRPL_METRIC_*metrics build their own MeterProvider resource inMetricsRegistry::initExporterAndProvider()(MetricsRegistry.cpp:280,:296-304, provider created at:339), andApplicationImp::startTelemetry()supplies the id with an explicit node-key fallback (Application.cpp:1674-1679: read the config key, andif (instanceId.empty() && nodeIdentity_)substitutetoBase58(TokenType::NodePublic, …)). By thensetup()has resolvednodeIdentity_(Application.cpp:1315), so these metrics carry the node key even with the config key unset.beast::insightmetrics are the exception. They use the global MeterProvider, whose resource is built in theTelemetryImplconstructor (Telemetry.cpp:321-338,initMetrics()at:447), because insight instruments are created eagerly in subsystem constructors and would otherwise bind to the noop provider forever. At that pointserviceInstanceIdis still""(Application.cpp:348passes an empty node key), and the code comment atTelemetry.cpp:333-336states plainly that the later setter "cannot change this immutable resource". Worse,initMetrics()sets the attribute unconditionally (Telemetry.cpp:488), so the resource carriesservice.instance.id=""rather than omitting it — whereasMetricsRegistryguards the same write withif (!instanceId.empty())(MetricsRegistry.cpp:302-303).Result: with
service_instance_idunset,beast::insightmetrics — and only those — export with an emptyservice.instance.id. Every shipped Grafana dashboard filters onservice_instance_id=~"$node", so insight-backed panels lose their per-node dimension; span-metric andXRPL_METRIC_*panels are unaffected. Set the key explicitly on any node whose insight metrics are dashboarded.Known issue. The asymmetry is a defect, not a design:
MetricsRegistryalready demonstrates the node-key fallback that the global provider needs. A fix would have to resolve the node identity beforeTelemetryImplis constructed, or make the insight metrics use a late-built provider.
5.1.2 Configuration Options Summary
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Enable/disable telemetry |
endpoint |
string | http://localhost:4318/v1/traces |
OTLP/HTTP collector endpoint for traces |
metrics_endpoint |
string | http://localhost:4318/v1/metrics |
OTLP/HTTP collector endpoint for the native metrics pipeline (MetricsRegistry). Read in Application.cpp:1670 |
use_tls |
bool | false |
Enable TLS for exporter connection |
tls_ca_cert |
string | "" |
Path to CA certificate file |
tls_client_cert |
string | "" |
Path to node's client certificate (PEM) for mutual TLS; requires use_tls=1; empty = one-way TLS |
tls_client_key |
string | "" |
Path to private key (PEM) for tls_client_cert; requires use_tls=1; required when the cert is set |
batch_size |
uint | 512 |
Spans per export batch |
batch_delay_ms |
uint | 5000 |
Max delay before sending batch (ms) |
max_queue_size |
uint | 2048 |
Maximum queued spans |
trace_transactions |
bool | true |
Enable transaction tracing |
trace_consensus |
bool | true |
Enable consensus tracing |
trace_rpc |
bool | true |
Enable RPC tracing |
trace_peer |
bool | true |
Enable peer message tracing (high volume) |
trace_ledger |
bool | true |
Enable ledger tracing |
consensus_trace_strategy |
string | "deterministic" |
Consensus trace ID strategy: "deterministic" (trace_id = prevLedgerHash[0:16]) or "attribute" (random). Parsed at TelemetryConfig.cpp:155-156, consumed at RCLConsensus.cpp:1291,1296. Not validated — see the note below |
service_name |
string | "xrpld" |
Service name (service.name) for traces and metrics |
service_instance_id |
string | node public key (base58) | Instance identifier (service.instance.id). Traces, span metrics and native XRPL_METRIC_* metrics all fall back to the node key; beast::insight metrics do not — see the note in §5.1.1 |
consensus_trace_strategy is not validated. TelemetryConfig.cpp:155-156
copies the raw string into Setup::consensusTraceStrategy without checking it
against an allowed set, and the only comparison in the code is
strategy == "attribute" (RCLConsensus.cpp:1296). Any unrecognised value —
including a typo — silently takes the deterministic branch with no log warning.
The two accepted values are documented at include/xrpl/telemetry/Telemetry.h:287-292.
Not a config key — deterministic transaction trace IDs are unconditional.
Earlier drafts of this document listed a tx_trace_strategy option
("deterministic" | "attribute"). No such key exists: TelemetryConfig.cpp
parses no transaction-strategy key, and the transaction trace ID is always
derived from the transaction hash. Only the consensus path has a
switchable strategy.
Planned (not yet implemented): the following options appear in the design
documents but are not parsed by TelemetryConfig.cpp. They will be added as
the corresponding subsystems are instrumented:
| Option | Planned Phase | Purpose |
|---|---|---|
exporter |
Future | Select between OTLP/HTTP and OTLP/gRPC |
trace_pathfind |
Phase 2 | Path computation tracing toggle |
trace_txq |
Phase 3 | Transaction queue tracing toggle |
trace_validator |
Future | Validator list / manifest update tracing |
trace_amendment |
Future | Amendment voting tracing |
exporteris not read, so do not set it. Both shipped sample configs (docker/telemetry/xrpld-telemetry.cfg,docker/telemetry/xrpld-telemetry-mainnet.cfg) used to carryexporter=otlp_http; the line had no effect and has since been replaced with a comment saying so. OTLP/HTTP is the only transport that exists (§2.2.1), andendpoint/metrics_endpointare the only transport knobs, until the §2.2.2 gRPC work lands.
5.2 Configuration Parser
TxQ = Transaction Queue
The parser makeTelemetrySetup() in src/libxrpl/telemetry/TelemetryConfig.cpp reads the [telemetry] Section and populates a Telemetry::Setup struct, applying the defaults listed in Section 5.1.2 via section.valueOr(...). It takes serviceInstanceId from the nodePublicKey argument when the key is absent, applies one unconditional endpoint default (dflt::endpoint, TelemetryConfig.cpp:61, used at :108) — the parser has no notion of exporter type — and leaves the sampling ratio at its fixed 1.0 default (a static constexpr member, so there is nothing to parse; TelemetryConfig.cpp:139, Telemetry.h:234). It also rejects two contradictory mTLS configurations outright (tls_client_cert without tls_client_key, and either without use_tls=1) rather than failing open at handshake time.
metrics_endpoint is deliberately not handled here: it is read separately in ApplicationImp::startTelemetry() (Application.cpp:1670) and passed to MetricsRegistry::start(). Note the consequence — the two metric exporters resolve their URL differently:
| Metric source | Exporter built by | URL comes from |
|---|---|---|
beast::insight ([insight] server=otel) |
Telemetry::initMetrics() (global provider) |
endpoint with a trailing /v1/traces rewritten to /v1/metrics |
Native XRPL_METRIC_* (MetricsRegistry) |
MetricsRegistry::initExporterAndProvider() |
metrics_endpoint, defaulting to http://localhost:4318/v1/metrics |
Setting a non-default endpoint therefore moves the insight metrics with it, but leaves the native metrics on localhost unless metrics_endpoint is set too.
5.3 Application Integration
5.3.1 ApplicationImp Changes
Deferred identity: The node public key (
nodeIdentity_) is not available duringApplicationImp's member initializer list — it is resolved later insetup(). TheTelemetryobject is therefore constructed with an emptyserviceInstanceIdand patched viasetServiceInstanceId()oncesetup()has calledgetNodeIdentity(). This patch reaches traces only. The global MeterProvider resource — the onebeast::insightmetrics use — is already frozen by then (§5.1.1), so those metrics keep whateverservice_instance_idthe config supplied (""if it supplied none). NativeXRPL_METRIC_*metrics do not go through this patch at all:startTelemetry()re-reads the config key and applies its own node-key fallback when buildingMetricsRegistry's separate resource (Application.cpp:1674-1679).
ApplicationImp (in src/xrpld/app/main/Application.cpp) owns a std::unique_ptr<telemetry::Telemetry> telemetry_. It is built in the member initializer list via makeTelemetry(makeTelemetrySetup(...)) with an empty serviceInstanceId, then patched in setup() by calling setServiceInstanceId() with the Base58 node public key (unless the user supplied a custom service_instance_id). start() and run() forward to telemetry_->start() / telemetry_->stop(), and getTelemetry() returns the owned instance.
5.3.2 ServiceRegistry Interface Addition
include/xrpl/core/ServiceRegistry.h gains a pure-virtual telemetry::Telemetry& getTelemetry() (with a forward declaration of telemetry::Telemetry), giving every component a uniform accessor for the tracing subsystem.
Note:
ApplicationextendsServiceRegistry, sogetTelemetry()is available on both. Components that hold aServiceRegistry&(e.g.NetworkOPsImp) callregistry_.get().getTelemetry(). Components that still hold anApplication&(e.g.ServerHandler,PeerImp,RCLConsensusAdaptor) callapp_.getTelemetry()directly.
5.4 CMake Integration
OTLP = OpenTelemetry Protocol
5.4.1 Locating the OpenTelemetry SDK
Superseded design. Earlier drafts described a hand-written
cmake/FindOpenTelemetry.cmakemodule that aliasedOpenTelemetry::api,OpenTelemetry::sdkandOpenTelemetry::otlp_grpc_exporterwith apkg-configfallback. That module was never written — it exists in no commit — and the aliasing approach it described does not work with the package the build actually consumes.
The SDK is located by the Conan-generated CMake config package, nothing else:
CMakeLists.txt—find_package(opentelemetry-cpp CONFIG REQUIRED), guarded by thetelemetryoption (§5.4.2). The dependency itself is declared inconanfile.py:153(opentelemetry-cpp/1.28.0), also guarded —requirements()adds it onlyif self.options.telemetry(:152), so with the option off the package never enters the dependency graph.- Linking goes through the umbrella target
opentelemetry-cpp::opentelemetry-cpp, never the per-component targets.cmake/XrplCore.cmake:221-225and:83-91record why: the Conan package under-declares its inter-component dependencies, so naming::api/::sdkindividually produces the wrong static-link order and fails at executable link time. The umbrella target supplies both the trace and metrics components with the correct ordering.
5.4.2 CMakeLists.txt Changes
The build flag is telemetry:
option(telemetry "Enable OpenTelemetry tracing" ON) # top-level CMakeLists.txt
The declared value is ON temporarily, so that CI compiles the telemetry code paths while the feature branches are in review. OFF is the intended default once merged, and the flip is a separate change. Set the value explicitly rather than relying on the default:
| To … | Use (CMake) | Use (Conan) |
|---|---|---|
| Build telemetry in | -Dtelemetry=ON |
-o telemetry=True |
| Build it out (all no-ops) | -Dtelemetry=OFF |
-o telemetry=False |
When the option is ON, the guarded block below it runs
find_package(opentelemetry-cpp CONFIG REQUIRED) and adds the
compile definition XRPL_ENABLE_TELEMETRY.
XRPL_ENABLE_TELEMETRYis not a CMake option. It is only ever added as a compile definition byadd_compile_definitions(XRPL_ENABLE_TELEMETRY)in that same block. Passing-DXRPL_ENABLE_TELEMETRY=OFFon the CMake command line disables nothing — it defines an unused cache variable and telemetry stays compiled in. CMake does report it, at the end of configuration underManually-specified variables were not used by the project, so it is not literally silent — but that line is easy to scroll past. Any procedure that relies on it (including the rollback path in §3.9.6) must use-Dtelemetry=OFF.
The target is xrpl.libxrpl.telemetry, created by add_module(xrpl telemetry)
at cmake/XrplCore.cmake:231 from include/xrpl/telemetry/ +
src/libxrpl/telemetry/. There is no xrpl_telemetry target.
Selection between the real and the no-op implementation is an in-source
#ifdef, not a source swap: NullTelemetry.cpp is compiled into the target
unconditionally (see its header comment, NullTelemetry.cpp:1-12). It provides
the makeTelemetry() factory when XRPL_ENABLE_TELEMETRY is undefined; when
the macro is defined, Telemetry.cpp provides the factory instead and
NullTelemetry's virtuals only serve as noop tracer/span fallbacks. Call sites
compile unchanged either way.
5.5 OpenTelemetry Collector Configuration
OTLP = OpenTelemetry Protocol | APM = Application Performance Monitoring
Production hardening: The configurations in this section are starting points. For production deployments where xrpld ships telemetry across a network to a centrally-hosted collector, see Securing the OTel Pipeline for the required mTLS receiver config, NetworkPolicy, and peer trace-context validation.
The authoritative collector config lives in the repo at docker/telemetry/otel-collector-config.yaml (with Tempo backend config in docker/telemetry/tempo.yaml). The sections below summarize the development and production shapes of that pipeline.
5.5.1 Development / Base Configuration
docker/telemetry/otel-collector-config.yaml is the base config used by the
local stack and by CI. It carries three pipelines, not one:
| Pipeline | Receivers | Processors | Exporters |
|---|---|---|---|
traces |
otlp |
resource/tier, resource/stripsdk, attributes/hash, batch |
debug, otlp/tempo, spanmetrics |
metrics |
otlp, spanmetrics |
resource/tier, resource/stripsdk, batch |
prometheus |
logs |
filelog |
resource/logs, resource/tier, resource/stripsdk, batch |
otlphttp/loki |
Component detail:
- Receivers.
otlpon gRPC0.0.0.0:4317and HTTP0.0.0.0:4318(both traces and native metrics arrive on 4318).filelogtails/var/log/xrpld/*/debug.logand runs aregex_parserthat liftstimestamp,partition,severityand the optionaltrace_id/span_idemitted by the journal sink (§5.8.5). - Processors.
batch(1s timeout,send_batch_size: 100);resource/tier(action: upsertondeployment.environment,action: insertonxrpl.network.typeonly when absent);resource/stripsdk(drops thetelemetry.sdk.*attributes);resource/logs(action: upsertonservice.nameandjob— only the former becomes a Loki stream label, see the known issue in §5.8.5);attributes/hash(hashespathfind_source_accountandpathfind_dest_account). - Connector.
spanmetricswithnamespace: "span"(otel-collector-config.yaml:114) — this is why the derived RED metrics arespan_calls_total/span_duration_milliseconds_*. The connector's own default namespace is empty, so without this setting the names would be the barecalls_total/duration_milliseconds_*. Thetraces_spanmetrics_*family is not the connector's default and is not produced here at all — it comes from a different producer, Tempo'smetrics_generatorspan-metricsprocessor (tempo.yaml:75), whoseremote_writeis commented out in this repo (see §5.8.6). Histogramunit: mswith sub-millisecond buckets from0.01ms, plus explicit2s–30sboundaries for consensus andledger.acquire. ~25 low-cardinality dimensions are promoted to labels (command,rpc_status,tx_type,ter_result,stage,consensus_mode,outcome, …). - Exporters.
debug(console,verbosity: detailed),otlp/tempo(tempo:4317,tls.insecure: true),otlphttp/loki(http://loki:3100/otlp— Loki 3.x native OTLP; the oldlokiexporter was removed in collector-contrib v0.147.0), andprometheuson0.0.0.0:8889withresource_to_telemetry_conversion.enabled: trueso the tier and instance resource attributes become Prometheus labels. - Extensions.
health_checkon0.0.0.0:13133only. There is nozpagesextension.
Deliberately absent from the base config — do not document them as present:
no memory_limiter, no tail_sampling, no Elastic APM exporter, and no
tx_account attribute rule (the hashed keys are the two pathfind_*_account
ones).
5.5.2 Production Configuration
There is no separate "production" collector config in this repo. The one
overlay that exists is docker/telemetry/otel-collector-config.grafanacloud.yaml.
It is not the base config plus one processor — it restructures the service
graph. The full delta:
| Added by the overlay | Where | Purpose |
|---|---|---|
basicauth/grafanacloud |
:29 |
Extension; instance id / API token from the container environment |
tail_sampling |
:60 |
One probabilistic policy at 0.5%, decision_wait: 10s |
transform/cloudlabels |
:119 |
Copies three resource attrs onto datapoint labels for Cloud (OTLP) ingest |
otlphttp/grafanacloud |
:236 |
Single OTLP/HTTP exporter fanning all three signals to Grafana Cloud |
metrics_flush_interval |
:136 |
spanmetrics flushes every 15s instead of the 60s default |
| Removed by the overlay | Consequence |
|---|---|
attributes/hash |
Pathfinding account attributes are not hashed on this config — see below |
debug |
No console span dump; collector logs alone when diagnosing ingest |
Pipelines go from three (traces, metrics, logs) to five
(:253-280): traces/metrics, traces/store, metrics/local,
metrics/cloud, logs. tail_sampling is applied in traces/store
(:259-261) — the branch feeding Tempo and Grafana Cloud — not in a pipeline
named traces, which does not exist in the overlay. The traces/metrics
branch feeds spanmetrics unsampled, so the derived RED metrics stay exact
while stored traces are ~1/200 of ingested ones.
Known issue — the cloud path does not hash pathfinding accounts. The base config runs
attributes/hashon itstracespipeline (otel-collector-config.yaml:105-110), hashingpathfind_source_accountandpathfind_dest_accountas defense in depth behind the node-side hashing. The overlay declares no such processor and lists none on any of its five pipelines, so on the Grafana Cloud config those two attributes reach both Grafana Cloud and the local Tempo with whatever value the node sent. Any node that emits raw addresses loses its second line of defense. Addingattributes/hashtotraces/storeandtraces/metricswould close the gap.
Hardening a collector for a real deployment (TLS/mTLS on the receiver,
NetworkPolicy, peer trace-context validation) is covered in
Securing the OTel Pipeline — not by any config file in
docker/telemetry/.
5.6 Docker Compose Development Environment
OTLP = OpenTelemetry Protocol
The authoritative development stack lives in the repo at docker/telemetry/docker-compose.yml. It brings up six services on a shared xrpld-telemetry bridge network. All images are pinned to exact tags.
| Service | Image | Published ports | Role |
|---|---|---|---|
otel-collector |
otel/opentelemetry-collector-contrib:0.158.0 |
4317, 4318, 8889 |
OTLP ingest, spanmetrics, filelog tail, Prometheus scrape target |
tempo |
grafana/tempo:2.9.4 |
3200 |
Trace storage and TraceQL |
loki |
grafana/loki:3.7.6 |
3100 |
Log storage for log↔trace correlation |
prometheus |
prom/prometheus:v3.13.2 |
9090 |
Scrapes the collector's :8889 |
grafana |
grafana/grafana:13.1.2 |
3000 |
Dashboards + provisioned datasources/alerts, anonymous admin |
renderer |
grafana/grafana-image-renderer:v5.12.0 |
8081 |
Panel→PNG rendering for image export and alert screenshots |
Two corrections to earlier drafts:
prometheusis not optional.grafanalists it independs_on(along withtempo,lokiandrenderer), and 7 of the 15 dashboards queryspan_calls_totalfrom it. Removing it blanks most panels.- Port
13133is not published. The collector'shealth_checkextension listens on13133inside the container, but the base compose file publishes only4317,4318and8889. Health checks from the host must either add a port mapping or rundocker compose exec.
The collector also bind-mounts the xrpld log root read-only
(${XRPLD_LOG_DIR:-./data/logs} → /var/log/xrpld) for the filelog
receiver, and the grafana service reads Slack/email alert secrets from an
optional gitignored .env.alerting.
5.7 Configuration Architecture
OTLP = OpenTelemetry Protocol
flowchart TB
subgraph config["Configuration Sources"]
cfgFile["xrpld.cfg<br/>[telemetry] section"]
cmake["CMake option: telemetry<br/>ON today for CI, OFF once merged<br/>when ON, defines XRPL_ENABLE_TELEMETRY"]
end
subgraph init["Initialization"]
parse["makeTelemetrySetup()"]
factory["makeTelemetry()"]
end
subgraph runtime["Runtime Components"]
tracer["TracerProvider"]
exporter["OTLP Exporter"]
processor["BatchProcessor"]
end
subgraph collector["Collector Pipeline"]
recv["Receivers"]
proc["Processors"]
exp["Exporters"]
end
cfgFile --> parse
cmake -->|"compile flag"| parse
parse --> factory
factory --> tracer
tracer --> processor
processor --> exporter
exporter -->|"OTLP"| recv
recv --> proc
proc --> exp
style config fill:#e3f2fd,stroke:#1976d2
style runtime fill:#e8f5e9,stroke:#388e3c
style collector fill:#fff3e0,stroke:#ff9800
Reading the diagram:
- Configuration Sources:
xrpld.cfgprovides runtime settings (endpoint, per-component trace toggles) while the CMaketelemetryoption controls whether telemetry is compiled in at all. That option is declared ON today only so CI compiles the instrumented paths; OFF is the intended default once merged, so treat the build gate as something to pass explicitly, and the runtime gate is opt-in either way (enabled=0by default). Head sampling is fixed at 1.0 and is not a config option; volume reduction happens via tail sampling in the collector. - Initialization:
makeTelemetrySetup()parses config values, thenmakeTelemetry()constructs the provider, processor, and exporter objects. - Runtime Components: The
TracerProvidercreates spans, theBatchProcessorbuffers them, and theOTLP Exporterserializes and sends them over the wire. - OTLP arrow to Collector: Trace data leaves the xrpld process via OTLP/HTTP and enters the external Collector pipeline. (OTLP/gRPC is future work — see design decisions §2.2.2.)
- Collector Pipeline:
Receiversingest OTLP data,Processorsapply sampling/filtering/enrichment, andExportersforward traces to storage backends (Tempo, etc.).
5.8 Grafana Integration
APM = Application Performance Monitoring
Step-by-step instructions for integrating xrpld traces with Grafana.
5.8.1 Data Source Configuration
Three datasources are provisioned from docker/telemetry/grafana/provisioning/datasources/. There is no Elastic APM datasource — elastic-apm.yaml was described in an earlier draft but never existed. Elastic remains a possible backend (§7.2); nothing in this repo provisions it.
| File | Type | URL | uid | Notes |
|---|---|---|---|---|
tempo.yaml |
tempo |
http://tempo:3200 |
tempo |
nodeGraph, serviceMap/tracesToMetrics → prometheus, tracesToLogs → loki, plus ~30 Explore search filters |
prometheus.yaml |
prometheus |
http://prometheus:9090 |
prometheus |
Backs every span-metric and native-metric panel |
loki.yaml |
loki |
http://loki:3100 |
loki |
Backs log-derived-insights; derived fields jump back to Tempo |
The Tempo tracesToLogs block is configured as filterByTraceID: true,
filterBySpanID: false, tags: []. The empty tag list is deliberate: the
correlation is by trace ID alone, so no span attribute needs to exist on both
sides. Earlier drafts claimed trace_id + tx_hash tags — that is not what
ships, and adding a tag Tempo cannot resolve blanks the link.
The search-filter list is the practical index of queryable span attributes:
resource scope (service.name, service.instance.id, service.version,
xrpl.network.id, xrpl.network.type), intrinsics (name, status,
duration), and span scope (command, rpc_status, rpc_role, tx_hash,
tx_type, tx_status, local, path, suppressed, peer_version,
consensus_*, ledger_seq, ledger_hash, close_time_correct,
close_resolution_ms, proposers, mode_old, mode_new, txq_status,
ter_code).
5.8.2 Dashboard Provisioning
grafana/provisioning/dashboards/dashboards.yaml declares a single file
provider named xrpld-telemetry, orgId: 1, targeting Grafana folder xrpld
from path /var/lib/grafana/dashboards (no /rippled suffix), with
disableDeletion: false, editable: true, foldersFromFilesStructure: false.
It sets no poll interval — Grafana's updateIntervalSeconds default
applies; the "every 30s" figure in earlier drafts was invented.
docker-compose.yml mounts ./grafana/dashboards read-only at that path, so
the 15 JSON files in docker/telemetry/grafana/dashboards/ are what gets
provisioned.
5.8.3 Shipped Dashboards
The dashboards are Prometheus-first, not TraceQL-first, and their uids are
bare (no xrpld- prefix). The full inventory and per-panel query reference is
09-data-collection-reference.md; the uids
are:
consensus-health, fee-market, job-queue, ledger-data-sync,
ledger-operations, log-derived-insights, network-traffic, node-health,
overlay-traffic-detail, peer-network, peer-quality, rpc-pathfinding,
rpc-performance, transaction-overview, validator-health.
Panel-count convention used in these docs: counts are of data panels only —
type: "row"collapsible headers are excluded, because a row is a layout element with no query. A board's rawpanelsarray is therefore longer than its stated count (e.g.rpc-performancehas 19 array entries: 2 rows + 17 data panels).
Two examples described in earlier drafts do not exist and should not be looked
for: xrpld-rpc-performance (the real board is rpc-performance, 17 data
panels in 2 rows, mostly Prometheus span metrics) and xrpld-tx-tracing (the
transaction board is transaction-overview, 18 data panels in 3 rows; its
error panel filters span_calls_total{span_name="tx.process", ter_result!~"tesSUCCESS|"}, since no tx.validate span was ever built — see
02 §2.3.2).
Why
!~"tesSUCCESS|"and not!="tesSUCCESS". An absent Prometheus label compares equal to the empty string, andtx.processcan end without ater_resultattribute:processTransaction()returns early whenpreProcessTransaction()rejects the transaction (NetworkOPs.cpp:1437-1438) anddoTransactionAsync()returns early when the transaction is already applying (:1461-1462); the only setter runs later, at:1674. Those series carryter_result="", which!="tesSUCCESS"counts as an error. The regex form excludes the empty value explicitly (the trailing|alternative), which is the formdocs/telemetry-runbook.md:1198and two of the threetransaction-overview.jsonfailure panels already use.
Every dashboard exposes a $node template variable bound to
service_instance_id; see the §5.1.1 note on why service_instance_id must be
set for metric panels to split per node.
5.8.4 TraceQL Query Examples
Common queries for xrpld traces. Every span name and attribute below is one
that the code actually emits — check against the *SpanNames.h constants
before adding more.
# Find all traces for a specific transaction hash
{resource.service.name="xrpld" && span.tx_hash="ABC123..."}
# Find slow RPC commands (>100ms)
{resource.service.name="xrpld" && name=~"rpc.command.*"} | duration > 100ms
# Find consensus rounds taking >5 seconds
{resource.service.name="xrpld" && name="consensus.round"} | duration > 5s
# Find failed transaction processing
{resource.service.name="xrpld" && name="tx.process" && span.ter_result!="tesSUCCESS"}
# Find failed apply-pipeline stages (preflight / preclaim / transactor)
{resource.service.name="xrpld" && name=~"tx\\.(preflight|preclaim|transactor)" && status=error}
# Find transactions that arrived from a peer rather than a local client.
# The `local` attribute lives on tx.process, NOT on tx.receive (see the note
# below).
{resource.service.name="xrpld" && name="tx.process" && span.local=false}
# Compare latency across nodes
{resource.service.name="xrpld" && name="rpc.command.account_info"} | avg(duration) by (resource.service.instance.id)
Queries in earlier drafts used
tx.validate,tx.relayandspan.relay_count. None of the three exists: signature/format validation ships astx.preflight/tx.preclaim, and no relay span or relay-count attribute was ever built. See 02 §2.3.2.
TraceQL silently returns nothing for an absent attribute. Unlike PromQL, where a missing label compares equal to
"", a TraceQL attribute predicate matches only spans that actually carry the attribute — including negated forms such as!=and=~".*". So filtering on the wrong span name yields zero rows with no error.localhas exactly one set-site,NetworkOPs.cpp:1417, and it is ontx.process: an earlier draft paired it withname="tx.receive", which can never match. Check the attribute's owning span in 09 §1.2 before combining aname=and aspan.predicate.
5.8.5 Correlation with Logs
Log↔trace correlation is implemented (Phase 8) and needs no Promtail, Fluentd or PerfLog change. Two pieces:
- The node stamps the IDs. The journal sink
Logs::format()(src/libxrpl/basics/Log.cpp:304-338, guarded byXRPL_ENABLE_TELEMETRY) reads the thread-local OTel context and, when a valid span is active, prefixes the message withtrace_id=<32 hex> span_id=<16 hex>. It reads the context value directly rather than callingGetSpan()to avoid a heap allocation on the (common) no-span path. This is the ordinarydebug.logstream — PerfLog is not involved, and thesetTraceIdhook described in earlier drafts was never built. - The collector ingests them. The
filelogreceiver tails/var/log/xrpld/*/debug.logand itsregex_parserliftstrace_idandspan_idas optional capture groups (§5.5.1).resource/logsapplies anupsertofservice.name=xrpld, which Loki promotes to the stream labelservice_name, so the canonical selector is{service_name="xrpld"}. Logs land in Loki viaotlphttp/loki.
Known issue — the collector's
jobupsert is ineffective for stream selection.resource/logsalso applies anupsertof ajob=xrpldattribute (otel-collector-config.yaml:62-70) with the stated intent that operators could paste{job="xrpld"}. That does not work. On OTLP ingest Loki promotes only an allow-listed set of resource attributes to indexed stream labels (service.name,service.namespace,service.instance.id,deployment.environment, thek8s.*/cloud.*keys);jobis not on that list, and this repo ships no Loki config override —docker-compose.yml:75starts Loki with the image's built-in/etc/loki/local-config.yaml.jobtherefore lands in structured metadata, which cannot appear in a stream selector, so{job="xrpld"}returns an empty result rather than an error. Corroboration in-repo:docs/telemetry-runbook.md:2533states the same ("service_name="xrpld"(notjob="xrpld")"), and all 38 Loki queries in the shipped dashboards (35 panel targets + 3 Loki-backed template variables) select onservice_name— zero usejob. Either drop thejobupsert or addjobto Loki'sdistributor.otlp_config.resource_attributesallow-list via a mounted Loki config; until then, useservice_name.
Grafana then links the two directions: the Tempo datasource's tracesToLogs
(filterByTraceID: true, tags: []) jumps trace → logs, and loki.yaml's
derived fields jump log → trace.
5.8.6 Correlation with Insight/OTel System Metrics
To correlate traces with Beast Insight system metrics:
Step 1: Export Insight metrics to Prometheus
Beast Insight metrics are exported natively via OTLP to the OTel Collector,
which exposes them on its Prometheus endpoint (:8889) alongside spanmetrics.
Set server=otel in the [insight] section of xrpld.cfg; no separate StatsD
exporter or Prometheus scrape job is needed.
makeCollectorManager() (src/xrpld/app/main/CollectorManager.cpp) reads these
[insight] keys:
| Key | Read at | Effect when server=otel |
|---|---|---|
server |
:35 |
Live. statsd | otel | anything else. Selects the collector implementation. |
address |
:39 |
StatsD only — the UDP endpoint. |
prefix |
:41, :53 |
Inert. Stored on the OTel collector but formatName() prepends nothing (OTelCollector.cpp:855-866); only StatsD applies it. |
endpoint |
:50 |
Inert. Logged for diagnostics (OTelCollector.cpp:730), then unused. |
service_instance_id |
:58 |
Inert. (void)-discarded (OTelCollector.cpp:722). |
service_name |
:64 |
Inert. (void)-discarded (OTelCollector.cpp:723). |
Where the identity and endpoint actually come from.
OTelCollectordeliberately does not own a pipeline: it fetches the Meter from the global MeterProvider thatTelemetry::initMetrics()published (OTelCollector.cpp:726-745). So the resource attributes — includingservice.instance.id, which every dashboard filters on — and the exporter URL both come from the[telemetry]section, not[insight]. The four inert keys above are back-compat leftovers from the StatsD-era signature; setting them has no effect. Set[telemetry] service_instance_idinstead (§5.1.1).
server=otelis not the default.CollectorManager.cpp:72-75falls through toNullCollectorfor any unrecognised or absentservervalue, so a node with no[insight]section emits no metrics at all.
Step 2: Correlate metrics to traces
Today this is a time-range correlation, not a click-through one: note the
window from the metric panel, then search Tempo over the same window filtered
by service.instance.id.
Exemplars are NOT implemented. Earlier drafts of this section instructed operators to rely on automatic exemplars, set
exemplarTraceIdDestinationson the Prometheus datasource, and enableexemplar: trueon panels. None of that is wired up: the stringexemplarappears nowhere insrc/libxrpl/telemetry/,src/xrpld/telemetry/, ordocker/telemetry/. Concretely, three things are missing —
- the SDK's exemplar filter is left at its default and no reservoir is configured in
Telemetry::initMetrics()orMetricsRegistry;- the collector's
prometheusexporter has no exemplar settings;grafana/provisioning/datasources/prometheus.yamlhas noexemplarTraceIdDestinationsblock.Note also that the query used as an example,
rpc_duration_seconds_bucket, does not exist — RPC latency histograms arespan_duration_milliseconds_bucket(spanmetrics,unit: ms) andrpc_method_us(native). Wiring exemplars end to end is genuine open work; until it lands, do not document a click-through that operators cannot perform.
Step 3: Jump the other way instead
Trace → metrics is available now: the Tempo datasource sets
tracesToMetrics.datasourceUid: prometheus with a ±1h time shift, so the
span-metric queries it builds resolve against the span_* families the
collector's spanmetrics connector produces. Trace → logs and log → trace are
both live (§5.8.5).
Known gap — Service Map is configured but inactive. The Tempo datasource declares
serviceMap.datasourceUid: prometheus, andtempo.yaml:70-76enables theservice-graphsmetrics-generator processor, but the generator has nowhere to write: itsremote_writeblock is commented out (tempo.yaml:53-56), andprometheus.yml:6-9defines a single scrape job againstotel-collector:8889— it never scrapes or accepts writes from Tempo.traces_service_graph_request_totaland its siblings are therefore never stored, so the Service Map / Node Graph tab renders empty. The same gap means Tempo'sspan-metricsprocessor never landstraces_spanmetrics_*either (§5.5.1) — every span metric the dashboards use comes from the collector's connector instead. Closing it needs both halves: uncommentremote_writeintempo.yamland enable--web.enable-remote-write-receiveron the Prometheus service (or add a scrape job for Tempo).
Previous: Implementation Strategy | Next: Implementation Phases | Back to: Overview