- Replace unused observer_result.h with nostd/shared_ptr.h and nostd/unique_ptr.h in MetricsRegistry.h (misc-include-cleaner) - Use auto for cast initializations in MetricsRegistry.cpp and ValidationTracker test (modernize-use-auto) - Run rename scripts: rippled -> xrpld in comments and docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
27 KiB
Phase 9: Internal Metric Instrumentation Gap Fill — Task List
Status: Future Enhancement
Goal: Instrument xrpld to emit ~50+ metrics that exist in
get_counts/server_info/TxQ/PerfLog but currently lack time-series export via the OTel or beast::insight pipelines.Scope: Hybrid approach — extend
beast::insightfor metrics near existing registrations, use OTel Metrics SDKObservableGaugecallbacks for new categories (TxQ, PerfLog, CountedObjects).Branch:
pratik/otel-phase9-metric-gap-fill(frompratik/otel-phase8-log-correlation)Depends on: Phase 7 (native OTel metrics pipeline) and Phase 8 (log-trace correlation)
Related Plan Documents
| Document | Relevance |
|---|---|
| 06-implementation-phases.md | Phase 9 plan: motivation, architecture, exit criteria (§6.8.2) |
| 09-data-collection-reference.md | Current metric inventory + future metrics section |
| Phase7_taskList.md | Prerequisite — OTel Metrics SDK and OTelCollector class |
| Phase8_taskList.md | Prerequisite — log-trace correlation |
Third-Party Consumer Context
These metrics serve multiple external consumer categories identified during research:
| Consumer Category | Key Metrics They Need |
|---|---|
| Exchanges | Fee escalation levels, TxQ depth, settlement latency |
| Payment Processors | Load factors, io_latency, transaction throughput |
| Analytics Providers | NodeStore I/O, cache hit rates, counted objects |
| Validators/Operators | Per-job execution times, PerfLog RPC counters, consensus timing |
| Academic Researchers | Consensus performance time-series, fee market dynamics |
| Institutional Custody | Server health scores, reserve calculations, node availability |
Task 9.1: NodeStore I/O Metrics
Objective: Export node store read/write performance as time-series metrics.
What to do:
-
In
src/libxrpl/nodestore/Database.cpp, extend existingbeast::insightregistrations to add:- Gauge:
node_reads_total(cumulative read operations) - Gauge:
node_reads_hit(cache-served reads) - Gauge:
node_writes(cumulative write operations) - Gauge:
node_written_bytes(cumulative bytes written) - Gauge:
node_read_bytes(cumulative bytes read) - Gauge:
node_reads_duration_us(cumulative read time in microseconds) - Gauge:
write_load(current write load score) - Gauge:
read_queue(items in read queue)
- Gauge:
-
These values are already computed in
Database::getCountsJson()(line ~236). Wire the same counters tobeast::insighthooks.
Key modified files:
src/libxrpl/nodestore/Database.cppsrc/libxrpl/nodestore/Database.h(add insight members)
Derived Prometheus metrics: xrpld_nodestore_reads_total, xrpld_nodestore_reads_hit, xrpld_nodestore_write_load, etc.
Grafana dashboard: Add "NodeStore I/O" panel group to Node Health dashboard.
Task 9.2: Cache Hit Rate Metrics
Objective: Export SHAMap and ledger cache performance as time-series gauges.
What to do:
-
Register OTel
ObservableGaugecallbacks (via Phase 7'sOTelCollector) for:SLE_hit_rate— SLE cache hit rate (0.0–1.0)ledger_hit_rate— Ledger object cache hit rateAL_hit_rate— AcceptedLedger cache hit ratetreenode_cache_size— SHAMap TreeNode cache size (entries)treenode_track_size— Tracked tree nodesfullbelow_size— FullBelow cache size
-
The callback should read from the same sources as
GetCounts.cpphandler (line ~43). -
Create a centralized
MetricsRegistryclass that holds all OTel async gauge registrations, polled at 10-second intervals by thePeriodicMetricReader.
Key modified files:
- New:
src/xrpld/telemetry/MetricsRegistry.h/.cpp src/xrpld/rpc/handlers/GetCounts.cpp(extract shared access methods)src/xrpld/app/main/Application.cpp(register MetricsRegistry at startup)
Derived Prometheus metrics: xrpld_cache_SLE_hit_rate, xrpld_cache_ledger_hit_rate, xrpld_cache_treenode_size, etc.
Task 9.3: Transaction Queue (TxQ) Metrics
Objective: Export TxQ depth, capacity, and fee escalation levels as time-series.
What to do:
-
Register OTel
ObservableGaugecallbacks for TxQ state (fromTxQ.hline ~143):txq_count— Current transactions in queuetxq_max_size— Maximum queue capacitytxq_in_ledger— Transactions in current open ledgertxq_per_ledger— Expected transactions per ledgertxq_reference_fee_level— Reference fee leveltxq_min_processing_fee_level— Minimum fee to get processedtxq_med_fee_level— Median fee level in queuetxq_open_ledger_fee_level— Open ledger fee escalation level
-
Add to the
MetricsRegistry(Task 9.2).
Key modified files:
src/xrpld/telemetry/MetricsRegistry.cpp(add TxQ callbacks)src/xrpld/app/tx/detail/TxQ.h(expose metrics accessor if needed)
Derived Prometheus metrics: xrpld_txq_count, xrpld_txq_max_size, xrpld_txq_open_ledger_fee_level, etc.
Grafana dashboard: New Fee Market & TxQ dashboard (fee-market).
Task 9.4: PerfLog Per-RPC Method Metrics
Objective: Export per-RPC-method call counts and latency as OTel metrics.
What to do:
-
Register OTel instruments for PerfLog RPC counters (from
PerfLogImp.cppline ~63):- Counter:
rpc_method_started_total{method="<name>"}— calls started - Counter:
rpc_method_finished_total{method="<name>"}— calls completed - Counter:
rpc_method_errored_total{method="<name>"}— calls errored - Histogram:
rpc_method_duration_us{method="<name>"}— execution time distribution
- Counter:
-
Use OTel
Counter<int64_t>andHistogram<double>instruments withmethodattribute label. -
Hook into the existing PerfLog callback mechanism rather than adding new instrumentation points.
Key modified files:
src/xrpld/perflog/detail/PerfLogImp.cpp(add OTel instrument updates alongside existing JSON counters)src/xrpld/telemetry/MetricsRegistry.cpp(register instruments)
Derived Prometheus metrics: xrpld_rpc_method_started_total{method="server_info"}, xrpld_rpc_method_duration_us_bucket{method="ledger"}, etc.
Grafana dashboard: Add "Per-Method RPC Breakdown" panel group to RPC Performance dashboard.
Task 9.5: PerfLog Per-Job-Type Metrics
Objective: Export per-job-type queue and execution metrics.
What to do:
-
Register OTel instruments for PerfLog job counters:
- Counter:
job_queued_total{job_type="<name>"}— jobs queued - Counter:
job_started_total{job_type="<name>"}— jobs started - Counter:
job_finished_total{job_type="<name>"}— jobs completed - Histogram:
job_queued_duration_us{job_type="<name>"}— time spent waiting in queue - Histogram:
job_running_duration_us{job_type="<name>"}— execution time distribution
- Counter:
-
Hook into PerfLog's existing job tracking alongside Task 9.4.
Key modified files:
src/xrpld/perflog/detail/PerfLogImp.cppsrc/xrpld/telemetry/MetricsRegistry.cpp
Derived Prometheus metrics: xrpld_job_queued_total{job_type="ledgerData"}, xrpld_job_running_duration_us_bucket{job_type="transaction"}, etc.
Grafana dashboard: New Job Queue Analysis dashboard (job-queue).
Task 9.6: Counted Object Instance Metrics
Objective: Export live instance counts for key internal object types.
What to do:
-
Register OTel
ObservableGaugecallbacks forCountedObject<T>instance counts:object_count{type="Transaction"}— live Transaction objectsobject_count{type="Ledger"}— live Ledger objectsobject_count{type="NodeObject"}— live NodeObject instancesobject_count{type="STTx"}— serialized transaction objectsobject_count{type="STLedgerEntry"}— serialized ledger entriesobject_count{type="InboundLedger"}— ledgers being fetchedobject_count{type="Pathfinder"}— active pathfinding computationsobject_count{type="PathRequest"}— active path requestsobject_count{type="HashRouterEntry"}— hash router entries
-
The
CountedObjecttemplate already tracks these via atomic counters. The callback just reads the current counts.
Key modified files:
src/xrpld/telemetry/MetricsRegistry.cpp(add counted object callbacks)include/xrpl/basics/CountedObject.h(may need static accessor for iteration)
Derived Prometheus metrics: xrpld_object_count{type="Transaction"}, xrpld_object_count{type="NodeObject"}, etc.
Grafana dashboard: Add "Object Instance Counts" panel to Node Health dashboard.
Task 9.7: Fee Escalation & Load Factor Metrics
Objective: Export the full load factor breakdown as time-series.
What to do:
-
Register OTel
ObservableGaugecallbacks for load factors (fromNetworkOPs.cppline ~2694):load_factor— combined transaction cost multiplierload_factor_server— server + cluster + network contributionload_factor_local— local server load onlyload_factor_net— network-wide load estimateload_factor_cluster— cluster peer loadload_factor_fee_escalation— open ledger fee escalationload_factor_fee_queue— queue entry fee level
-
These overlap with some existing StatsD metrics but provide finer granularity (individual factor breakdown vs. combined value).
Key modified files:
src/xrpld/telemetry/MetricsRegistry.cppsrc/xrpld/app/misc/NetworkOPs.cpp(expose load factor accessors if needed)
Derived Prometheus metrics: xrpld_load_factor, xrpld_load_factor_fee_escalation, etc.
Grafana dashboard: Add "Load Factor Breakdown" panel to Fee Market & TxQ dashboard.
Task 9.7a: push_metrics.py Parity — Missing Observable Gauges
Objective: Fill the remaining metric gaps between the external push_metrics.py script (in ripplex-ansible) and the internal OTel MetricsRegistry observable gauges. After this task, all metrics collected by push_metrics.py that CAN be collected internally are covered.
What was done:
- Extended existing
cacheHitRateGauge_callback withAL_size(AcceptedLedger cache size) - Extended existing
nodeStoreGauge_callback with 4 new metrics fromgetCountsJson():node_reads_duration_us(JSON string — usesstd::stoll(asString()))read_request_bundle(native JSON int)read_threads_running(native JSON int)read_threads_total(native JSON int)
- Added new
xrpld_server_infoInt64ObservableGauge with 8 metrics:server_state— operating mode as int (0=DISCONNECTED .. 4=FULL)uptime— seconds since server startpeers— total peer countvalidated_ledger_seq— validated ledger sequence (atomic read)ledger_current_index— current open ledger sequencepeer_disconnects_resources— cumulative resource-related disconnectslast_close_proposers— fromgetConsensusInfo()["previous_proposers"]last_close_converge_time_ms— fromgetConsensusInfo()["previous_mseconds"]
- Added new
xrpld_build_infoInt64ObservableGauge (info-style, value=1 withversionlabel) - Added new
xrpld_complete_ledgersInt64ObservableGauge parsing comma-separated ranges into{bound, index}pairs - Added new
xrpld_db_metricsInt64ObservableGauge with 4 metrics:db_kb_total,db_kb_ledger,db_kb_transaction(SQLite stat queries)historical_perminute(historical ledger fetch rate)
Key modified files:
src/xrpld/telemetry/MetricsRegistry.h(4 new gauge members, updated ASCII diagram)src/xrpld/telemetry/MetricsRegistry.cpp(4 new callback registrations, 2 callback extensions)
Not implementable inside xrpld:
connection_count_51233/51234— OS-level port connection counts from external shell script (get_connection.sh)
Derived Prometheus metrics: xrpld_server_info{metric="server_state"}, xrpld_build_info{version="2.4.0"}, xrpld_complete_ledgers{bound="start",index="0"}, xrpld_db_metrics{metric="db_kb_total"}, etc.
Grafana dashboard: New panels added to Node Health dashboard (node-health.json).
Task 9.8: New Grafana Dashboards
Objective: Create Grafana dashboards for the new metric categories.
What to do:
-
Create 2 new dashboards:
- Fee Market & TxQ (
fee-market) — TxQ depth/capacity, fee levels, load factor breakdown, fee escalation timeline - Job Queue Analysis (
job-queue) — Per-job-type rates, queue wait times, execution times, job queue depth
- Fee Market & TxQ (
-
Update 2 existing dashboards:
- Node Health (
xrpld-statsd-node-health) — Add NodeStore I/O panels, cache hit rate panels, object instance counts - RPC Performance (
rpc-performance) — Add per-method RPC breakdown panels
- Node Health (
Key modified files:
- New:
docker/telemetry/grafana/dashboards/rippled-fee-market.json - New:
docker/telemetry/grafana/dashboards/rippled-job-queue.json docker/telemetry/grafana/dashboards/rippled-statsd-node-health.jsondocker/telemetry/grafana/dashboards/rippled-rpc-perf.json
Task 9.9: Update Documentation
Objective: Update telemetry reference docs with all new metrics.
What to do:
-
Update
OpenTelemetryPlan/09-data-collection-reference.md:- Add new section for OTel SDK-exported metrics (NodeStore, cache, TxQ, PerfLog, CountedObjects, load factors)
- Update Grafana dashboard reference table (add 2 new dashboards)
- Add Prometheus query examples for new metrics
-
Update
docs/telemetry-runbook.md:- Add an Alerting section covering the provisioned rules and how to wire a receiver
- Add troubleshooting entries for new metric categories
-
Provision Grafana alert rules (
docker/telemetry/grafana/provisioning/alerting/):- 6 rules in 3 groups — consensus/ledger (
LedgerHistoryMismatch,LedgerCloseStalled), validator (ValidationsMissed,ValidationsNotChecked), job queue (JobQueueTxOverflow,JobQueueLatencyHigh) xrpld-defaultwebhook contact point + flat notification policy; auto-loaded via the existingprovisioning/mount (no docker-compose change)- Alerting operator docs (per-alert meaning, tuning, receiver wiring) now live in the Alerting section of
docs/telemetry-runbook.md
- 6 rules in 3 groups — consensus/ledger (
Key modified files:
OpenTelemetryPlan/09-data-collection-reference.mddocs/telemetry-runbook.mddocker/telemetry/grafana/provisioning/alerting/{rules,contactpoints,policies}.yaml(new)docs/telemetry-runbook.md(Alerting section added)
Task 9.10: Integration Tests
Objective: Verify all new metrics appear in Prometheus after a test workload.
What to do:
-
Extend the existing telemetry integration test:
- Start xrpld with
[telemetry] enabled=1and[insight] server=otel - Submit a batch of RPC calls and transactions
- Query Prometheus for each new metric family
- Assert non-zero values for: NodeStore reads, cache hit rates, TxQ count, PerfLog RPC counters, object counts, load factors
- Start xrpld with
-
Add unit tests for the
MetricsRegistryclass:- Verify callback registration and deregistration
- Verify metric values match
get_countsJSON output - Verify graceful behavior when telemetry is disabled
Key modified files:
src/test/telemetry/MetricsRegistry_test.cpp(new)- Existing integration test script (extend assertions)
Task 9.11: Validator Health Dashboard (External Dashboard Parity)
Source: External Dashboard Parity — dashboards for Phase 7 metrics inspired by the community xrpl-validator-dashboard.
Upstream: Phase 7 Tasks 7.9-7.16 (metrics must be emitting). Downstream: Phase 10 (dashboard load checks), Phase 11 (alert rules reference these panels).
Objective: Create a Grafana dashboard for validation agreement, amendment/UNL health, and state tracking.
Dashboard: validator-health.json
| Panel | Type | PromQL |
|---|---|---|
| Agreement % (1h) | stat | xrpld_validation_agreement{metric="agreement_pct_1h"} |
| Agreement % (24h) | stat | xrpld_validation_agreement{metric="agreement_pct_24h"} |
| Agreements vs Missed (1h) | bargauge | agreements_1h and missed_1h side by side |
| Agreements vs Missed (24h) | bargauge | agreements_24h and missed_24h side by side |
| Validation Rate | stat | rate(xrpld_validations_sent_total[5m]) * 60 |
| Validations Checked Rate | stat | rate(xrpld_validations_checked_total[5m]) * 60 |
| Amendment Blocked | stat | xrpld_validator_health{metric="amendment_blocked"} |
| UNL Expiry (days) | stat | xrpld_validator_health{metric="unl_expiry_days"} |
| Validation Quorum | stat | xrpld_validator_health{metric="validation_quorum"} |
| State Value Timeline | timeseries | xrpld_state_tracking{metric="state_value"} |
| Time in Current State | stat | xrpld_state_tracking{metric="time_in_current_state_seconds"} |
| State Changes Rate | stat | rate(xrpld_state_changes_total[1h]) |
| Ledgers Closed Rate | stat | rate(xrpld_ledgers_closed_total[5m]) * 60 |
Dashboard conventions: $node template variable for service_instance_id filtering, dark theme, matching existing panel sizes and color schemes.
Key new files: docker/telemetry/grafana/dashboards/rippled-validator-health.json
Exit Criteria:
- All 13 panels render with non-zero data during normal operation
$nodefilter works correctly for multi-node deployments- Amendment blocked and UNL expiry panels use color thresholds (red=blocked/expiring)
Task 9.12: Peer Quality Dashboard (External Dashboard Parity)
Source: External Dashboard Parity
Objective: Create a Grafana dashboard for peer health aggregates.
Dashboard: peer-quality.json
| Panel | Type | PromQL |
|---|---|---|
| P90 Peer Latency | timeseries | xrpld_peer_quality{metric="peer_latency_p90_ms"} |
| Insane/Diverged Peers | stat | xrpld_peer_quality{metric="peers_insane_count"} |
| Higher Version Peers % | stat | xrpld_peer_quality{metric="peers_higher_version_pct"} |
| Upgrade Recommended | stat | xrpld_peer_quality{metric="upgrade_recommended"} |
| Resource Disconnects | timeseries | xrpld_Overlay_Peer_Disconnects_Charges |
| Inbound vs Outbound | bargauge | xrpld_Peer_Finder_Active_Inbound_Peers, ..._Outbound_Peers |
Key new files: docker/telemetry/grafana/dashboards/rippled-peer-quality.json
Exit Criteria:
- All 6 panels render correctly
- P90 latency panel shows trend over time
- Upgrade recommended panel uses color threshold (red=1, green=0)
Task 9.13: Ledger Economy Dashboard Panels (External Dashboard Parity)
Source: External Dashboard Parity
Objective: Add "Ledger Economy" row to the existing node-health.json dashboard.
| Panel | Type | PromQL |
|---|---|---|
| Base Fee (drops) | stat | xrpld_ledger_economy{metric="base_fee_xrp"} |
| Reserve Base (drops) | stat | xrpld_ledger_economy{metric="reserve_base_xrp"} |
| Reserve Inc (drops) | stat | xrpld_ledger_economy{metric="reserve_inc_xrp"} |
| Ledger Age | stat | xrpld_ledger_economy{metric="ledger_age_seconds"} |
| Transaction Rate | timeseries | xrpld_ledger_economy{metric="transaction_rate"} |
Key modified files: docker/telemetry/grafana/dashboards/node-health.json
Exit Criteria:
- 5 new panels render correctly in existing dashboard
- Fee values match
server_infoRPC output - Transaction rate shows smooth trend (not spiky)
Exit Criteria
- All ~50 new metrics visible in Prometheus via OTLP pipeline
MetricsRegistryclass registers/deregisters cleanly with OTel SDK- Async gauge callbacks execute at 10s intervals without performance impact
- 2 new Grafana dashboards operational (Fee Market, Job Queue)
- 2 existing dashboards updated with new panel groups
- Integration test validates all new metric families are non-zero
- No performance regression (< 0.5% CPU overhead from new callbacks)
- Documentation updated with full new metric inventory
- Validator Health dashboard renders all 13 panels
- Peer Quality dashboard renders all 6 panels
- Ledger Economy panels added to node-health dashboard
Appendix: Alerting Design
Design for the provisioned Grafana alert rules (Task 9.9a). Previously a standalone spec; merged here so the phase plan is self-contained.
Date: 2026-07-06
Branch: pratik/otel-phase9-metric-gap-fill (PR #6513, Jira RIPD-5187)
Status: Approved
Purpose
Phase 9 exports ~68 internal xrpld metrics and ships Grafana dashboards for them. This adds the missing operator-facing piece: provisioned Grafana alert rules that fire on the health-critical metrics phase 9 introduces. The phase-9 task list (line 311) and Jira story RIPD-5187 both already list "alerting rules" as a phase-9 deliverable, so this closes that gap.
Scope is deliberately narrow — the three subsystems whose failure is node-fatal: consensus/ledger health, validator health, job queue. RPC/API health is explicitly out of scope.
Why phase 9 (not phase 11)
Every metric these alerts fire on is born in phase 9
(xrpld_ledger_history_mismatch_total, xrpld_ledgers_closed_total,
xrpld_validation_missed_total, xrpld_validations_checked_total,
xrpld_jq_trans_overflow_total, xrpld_job_queued_duration_us_bucket). Alerts
belong with the metrics they watch, and this is where the dependency lives.
Delivery
Provisioned YAML, version-controlled — matching the existing datasource /
dashboard provisioning pattern. No docker-compose change: the Grafana service
already mounts ./grafana/provisioning:/etc/grafana/provisioning:ro, and
Grafana auto-loads provisioning/alerting/*.yaml.
New files under docker/telemetry/grafana/provisioning/alerting/:
| File | Purpose |
|---|---|
contactpoints.yaml |
One contact point xrpld-default (webhook to a documented placeholder; comments show how to swap for Slack/email). |
policies.yaml |
Default notification policy: route all alerts → xrpld-default, grouped by alertname + service_instance_id. |
rules.yaml |
6 alert rules across 3 groups (below). |
Plus the Alerting section of docs/telemetry-runbook.md — operator runbook:
what each alert means, likely causes, and how to point the contact point at a
real receiver.
Alert rules
All rules target Prometheus datasource uid: prometheus. Each rule uses the
Grafana rule shape: query (A) → reduce (B, last value) → threshold (C). All
rate()/histogram_quantile() expressions aggregate with
sum by (service_instance_id) (or + le) so each node alerts independently.
Alert rules run headless, so they cannot use the dashboards' $node template
variables — they match all series and group by service_instance_id instead.
| Group | Alert | Expression (5m window) | Fires | for |
severity |
|---|---|---|---|---|---|
| Consensus | LedgerHistoryMismatch | sum by (service_instance_id)(rate(xrpld_ledger_history_mismatch_total[5m])) |
> 0 |
5m | critical |
| Consensus | LedgerCloseStalled | sum by (service_instance_id)(rate(xrpld_ledgers_closed_total[5m])) |
< 0.001 (≈0) |
3m | critical |
| Validator | ValidationsMissed | sum by (service_instance_id)(rate(xrpld_validation_missed_total[5m])) |
> 0 |
5m | warning |
| Validator | ValidationsNotChecked | sum by (service_instance_id)(rate(xrpld_validations_checked_total[5m])) |
< 0.001 (≈0) |
5m | warning |
| Job queue | JobQueueTxOverflow | sum by (service_instance_id)(rate(xrpld_jq_trans_overflow_total[5m])) |
> 0 |
5m | warning |
| Job queue | JobQueueLatencyHigh | histogram_quantile(0.99, sum by (le, service_instance_id)(rate(xrpld_job_queued_duration_us_bucket[5m]))) |
> 1000000 (µs = 1s) |
5m | warning |
Each rule carries labels severity and category (consensus/validator/jobqueue)
and annotations summary + description (with {{ $labels.service_instance_id }}
and {{ $values.B.Value }} interpolation).
Threshold rationale
- LedgerCloseStalled
< 0.001for 3m: healthy nodes close a ledger every ~3-5s; a 5m rate decaying to ~0 means the node is stuck. The epsilon (not exact0) avoids float rate-noise suppressing the alert. - JobQueueLatencyHigh 1s p99: a default starting point, easy to tune — jobs queued >1s at p99 indicate the node is saturated.
- Others are
> 0on error/miss counters: any sustained nonzero rate is actionable.
Non-goals / YAGNI
- No per-alert silencing schedules, no mute timings.
- No RPC/API, overlay, or fee-market alerts (dashboards cover those visually).
- Single contact point — multi-receiver routing is left to the operator.
Verification
yamllint(orpython -c yaml.safe_load) on all three YAML files.docker compose -f docker/telemetry/docker-compose.yml config -qstill parses.- Optional live check: start stack,
GET /api/v1/provisioning/alert-rulesreturns the 6 rules; Grafana logs show no provisioning errors. - Code-review pass (subagent) against phase conventions before commit.