tx.receive no longer carries a suppressed attribute: the span is created only
once the node has decided to process the transaction, so there is no dropped
copy for the attribute to describe.
The validator fails a span that is missing a required attribute, so leaving it
listed turns the telemetry-validation leg red.
The workload README contradicted itself on --skip-loki: one bullet said CI always
passes it and so the two log-correlation checks are never exercised, another said
the workflow no longer passes it. The workflow mentions the flag nowhere, so the
first was the stale half.
Other claims checked against the tree and corrected:
- both the README and the plan doc described the push trigger as filtered on
branch names. The workflow has no branches filter, deliberately, because
GitHub ANDs branches with paths
- the plan doc printed 6 of the workflow's 12 paths globs, and claimed the
workflow was 367 lines against an actual 451. The glob block is now generated
from the workflow, and the line count dropped rather than restated
- rpcNOT_SUPPORTED does not exist anywhere in the tree. The symbol is
RpcNotSupported, and the refusal sites are RipplePathFind.cpp:59-60 and
PathFind.cpp:50-51, not :48-49 and :39
- RCLConsensus.cpp:666 and :663 are not log or event lines; the tx.included event
is at :720 and the per-transaction debug log at :715
- LedgerMaster.cpp:463 is fixIndex, not the ledger.store span, which is at :470
- ServerHandler.cpp:705 is inside makeJsonError; processRequest is at :718
- file counts: docker/telemetry/workload/ is 25 files, include/xrpl/telemetry/ 13
- the optional-span bullet named five causes covering 10 of 16 entries, omitting
the txq.* family and the WebSocket handshake
- the /api/v1/series choice was attributed to stale StatsD gauges; this harness
runs no StatsD
A line number in run-full-validation.sh was cited in five places and drifts on
every edit to that file, so those now name the file only. The keygen helper's
header records what production does instead -- validator-keys-tool create_keys
then create_token, keeping the master key off the node -- and why a disposable
cluster does not.
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.
Notes across the workload contract described earlier versions of themselves, or
cited commits that only exist inside this chain. A squash merge publishes none of
it, so each reference resolves nowhere.
Notes that described their own earlier text:
- expected_spans.json: 'this note previously concluded', 'this note previously
said', 'Un-skipped 2026-08-26', 'the reason had simply gone stale for two
weeks' and 'the claim this entry carried' are replaced by the standing reason
each entry holds. The wildcard pairs now say the validator globs the child via
_span_name_matches(), and state the literal-collapse failure as what a
different validator WOULD do rather than as history.
- regression-thresholds.json: 'an earlier version of this note wrongly claimed',
'the earlier version oversold it' and 'an earlier note called that' become the
cautions themselves -- do not reason from 'every ladder step is at least 2x',
do not oversell the backstop, do not read a false fire as a missing override.
- test_check_regression_bounds.py: the docstring gives the reason a literal is
wrong here, not the story of two tests that once hard-coded one.
Baseline-refresh history rewritten as measurement:
- README.md, baselines/README.md, telemetry-runbook.md and regression-metrics.json
no longer attribute threshold moves to 'the 2026-08-26 refresh'. The evidence
is kept as measurement -- span.tx.apply.p50 has read 0.7917 ms and 0.00597 ms
on the same workload, 132x apart; job.acceptLedger.running.p95 has measured a
5.74x floor on one baseline and 16.28x on another -- which is what supports the
claim that a single-run baseline cannot bound these keys.
Two chain-only commit ids removed, d059f21bf3 and 3860c93db2. Neither is
reachable from develop, so both cease to exist on merge; the second is chain
bookkeeping. The facts they were cited for (the validator globs wildcards; the
span ladder's floor is 0.01 ms) are stated directly instead.
Capture provenance is deliberately kept: baseline-timings.json 'captured_at',
the 2026-08-26 baseline heading, and the 2026-08-24 figures cited as data.
Documentation, JSON note strings and one docstring only, no behaviour change.
The harness contract described how path-finding load came to be absent rather
than why it is absent. The load exists on no branch before this one, so a squash
merge publishes no revision that ever issued it: the 2026-08-25 date resolves
nowhere, and 'removing it', 'used to satisfy' and 'has now cleared' compare
against a state a reader cannot reach.
- README: state that DEFAULT_WEIGHTS carries no ripple_path_find entry, and give
the error floor as what WOULD happen if it did, rather than what removing it
fixed. 'Putting it back' becomes 'Enabling it'.
- expected_spans.json: the pathfind.request and pathfind.compute notes, and both
hierarchy skip_reasons, now put the span's presence in the conditional -- the
parent would appear if the RPC were issued, because the ScopedSpanGuard at
RipplePathFind.cpp:35 sits above the rpcNOT_SUPPORTED guard at :48-49.
- expected_metrics.json: the rpc_method_errored_total, pathfind_fast and
pathfind_full notes drop the date and keep both independent reasons the
metrics stay absent.
- regression-thresholds.json: span.ledger.store 'is excluded from' the gated
surface rather than 'was removed from' it.
The reasoning is unchanged: pathfinding is off because Config.cpp:725-726 zeroes
pathSearchMax when [validation_seed] is present, a refused call still exports an
error span, and at a 3% weight that is a ~3% STATUS_CODE_ERROR floor.
Documentation and JSON note strings only, no behaviour change.
The check reported span.hierarchy.<parent>-><child> and a message reading
"Found <child> as child of <parent>" on the strength of both names appearing
somewhere in the same trace. A span parented by something unrelated passed, so
the one property the check exists to prove was never tested.
It now walks the child's parentSpanId chain looking for a span matching the
parent name. Ancestry rather than a direct edge, because all 21 declared
relationships are worded as the parent containing the child, so a scope
appearing in between is a refactor and not a broken relationship. Span ids are
compared as opaque strings: both fields come from the same Tempo response and
share its encoding, so nothing here depends on whether that is hex or base64.
Co-occurrence is still the search filter, which is what lets a conditional
child be found in an older trace instead of only the newest ones.
Verdicts are separated because they send the reader to different places: a
child that is present but not under the parent is a hierarchy bug, a chain
running into a span the trace lacks is one that never reached Tempo, and an
unusable parent span is neither. A definite negative outranks an indefinite
one, and one trace proving ancestry settles the relationship.
Tests cover each verdict plus the cross-trace and cyclic-chain cases, and each
one was checked against the specific defect it names. The runner now fails when
it collects no tests and reports SystemExit, both of which otherwise produce a
silent pass.
The pathfind.request skip_reason said only the child side handles globs. Both
sides do now; the blocker is the literal parent name in the Tempo query, so the
skip itself stands.
The hierarchy check searched the parent span and inspected the three newest
traces it returned. That is wrong whenever the child is conditional on a state
the workload only sometimes reaches: the parent fires constantly, so its newest
traces are the ones LEAST likely to carry a rare child. Three relationships had
been skipped as unassertable for exactly this, and in none of them was the child
missing -- each emitted traces of its own and simply was not in the three most
recent parent traces.
The check now issues a second query, a TraceQL trace-level conjunction of the
parent and child name predicates, and inspects those traces. Tempo searches its
whole retention for co-occurrence instead of leaving the answer to which traces
happen to be newest. The parent-only query is kept and still runs first, so "the
parent stopped being emitted" stays a distinct failure from "the parent is there
but the child never co-occurs" -- they mean different things to whoever reads the
report, and collapsing them would lose that.
The returned traces are still verified with _span_name_matches rather than the
query result being trusted on its own. Tempo has already guaranteed
co-occurrence, so this is redundant on the happy path; it is kept because it
keeps the glob semantics in one place and means a wrongly built query cannot
silently pass.
_traceql_name_predicate handles the wildcard contracts. TraceQL has no glob
operator, so `rpc.command.*` is sent as name=~"rpc\.command\..*" with the dots
escaped -- unescaped they would match any character in those positions, which is
the looseness _span_name_matches exists to avoid.
Two entries follow from the fix. txq.accept -> txq.accept_tx is asserted again:
its child is created inside the queued-transaction loop behind
`if (feeLevelPaid >= requiredFeeLevel)` (TxQ.cpp:1530) while the parent fires on
every close (:1499), which was the whole reason it failed. txq.enqueue ->
txq.batch_clear stays skipped but for ONE reason now instead of two -- its child
never fires at all under this workload, needing an account with a supersedable
batch, so it is purely a workload gap and needs nothing further from the
validator. The third, ledger.acquire -> ledger.acquire.txtree, lives on the
sync-diagnostics branch and is un-skipped there once this merges forward.
Written test-first, and the first test this module has had. The failing test
reproduces the exact CI message, "txq.accept_tx not found in txq.accept traces",
against a stubbed Tempo whose corpus holds the child only in a trace outside the
newest three. Three sibling tests guard the ways this could be "fixed" wrongly: an
absent child must still fail, a missing parent must still name the parent rather
than the child, and a wildcard child must be satisfied by any family member. The
stub records the queries issued, so the conjunction is asserted rather than
assumed. A stub rather than a live Tempo because the behaviour under test is which
traces the check ASKS FOR -- a passing query against real data proves the data
co-operated, not that the query was right.
The first run of those tests failed for the wrong reason: my stub's name-predicate
regex also matched the resource.service.name="xrpld" term every query carries and
so demanded a span literally named "xrpld". Fixed in the stub, with the lookbehind
commented as load-bearing, before touching production code.
Verification: 4/4 tests pass, and the failing one was watched failing first with
the production message; the issued queries were printed and confirmed to contain
the conjunction; validate_telemetry.py compiles; expected_spans.json parses;
21 relationships, 16 asserted and 5 skipped; counters still 41 span types;
otel-naming exits 0. Three unrelated files in this worktree are another party's
live work and were deliberately left unstaged.
Each span entry documents its parent, and a separate list holds the pairs the
validator actually checks. Three parentings were declared on the span entries and
absent from that list entirely, so they were neither asserted nor recorded as
unassertable -- silently missing rather than deliberately skipped. All three are
now listed, skipped, each with the reason that actually applies. Every span
declaring a parent now has an entry: the count went from 3 unaccounted to 0.
txq.enqueue -> txq.batch_clear is conditional and narrowly so. The child is
created in TxQ::tryClearAccountQueueUpThruTx (TxQ.cpp:550), which needs one
account holding several queued transactions AND an arriving transaction that
supersedes the batch. txq-burst produces queueing but arranges no such shape, and
it has never been observed on a run. It would also meet the sampling limit that
forced the txq.accept_tx skip, so fixing the sampling addresses both at once.
rpc.command.* -> pathfind.request is the one skip caused by a wildcard PARENT
rather than by a missing span, and the asymmetry is worth recording:
_validate_parent_child inserts the parent name literally into its Tempo query
(:801), so a wildcard parent matches nothing, while the CHILD side globs through
_span_name_matches (:826-828). That is exactly why rpc.ws_message ->
rpc.command.* can be asserted and this cannot.
pathfind.compute -> pathfind.discover has both ends absent, for the reason the
pathfind.compute entry already sets out at length: pathfinding is disabled on
every harness node because Config.cpp:725-726 zeroes pathSearchMax when a
[validation_seed] is present, and since 2026-08-25 no path-finding RPC is issued
either. Listed so the family is fully accounted for rather than partly silent.
No assertion is added or removed here -- this is accounting. The plan task that
prompted it also assumed the pathfind.compute skip reason was stale and needed
correcting; it is not, it already names both blockers and corrects an older
liquidity-based reason, so that half of the task was a defect in my plan rather
than in the file.
Verification: JSON parses; 21 relationships, 15 asserted and 6 skipped; no
duplicates; 0 spans declaring a parent without an entry, down from 3; counters
still 41 span types; otel-naming exits 0; pre-commit clean.
Both rpc.command.* relationships were skipped on the claim that
_validate_parent_child collapses a wildcard child to one literal name via
child_name.replace("*", "server_info"). That code does not exist. d059f21bf3
removed it on 2026-08-14 and replaced it with _span_name_matches(), which globs
through fnmatch.fnmatchcase; the check's own comment now reads "globs for
wildcard contracts". So any rpc.command.<anything> under the parent satisfies the
contract, and the command mix the sampled traces happen to carry no longer
matters -- which was the entire basis of the skip. The wildcard_probes map that
does still substitute a literal name belongs to the span-EXISTENCE check
(validate_telemetry.py:545, :554), not to the hierarchy check.
The WebSocket entry's reason went stale the day that code was deleted. The
rpc.process entry's is worse and is mine: c531ac569b rewrote that reason to fix a
different error in it -- it had claimed rpc.process cannot appear under a
WebSocket-only harness, when it appears on every run because
run-full-validation.sh polls each node over HTTP with curl -- and while fixing
that I copied the wildcard claim across from the stale WS entry without checking
it. Correcting one false statement in a note is not a licence to inherit the
next one.
Both are now asserted. Both parents emit on a normal run: rpc.ws_message is the
WebSocket root the load generator drives, and rpc.process reports 5 traces from
the curl readiness and validated-ledger polls, every one of which runs a command.
This also retires the plan's Task 5 without writing any validator code. The task
was scoped as "teach the validator to match a wildcard child"; it already does,
and had for two weeks. Checking the code before writing the feature turned a code
change into a data change.
Verification: JSON parses; 18 relationships, 15 asserted and 3 skipped, up from
13 asserted; the three remaining skips are txq.accept_tx (newest-N sampling of a
conditional child), rpc.ws_message -> rpc.process (genuinely not a code
relationship) and pathfind.compute (child never fires); counters still 41 span
types; churn 2/6; otel-naming exits 0; pre-commit clean. Whether these two hold in
a real trace is what the next run decides -- both ends emitting is necessary, not
sufficient.
c531ac569b asserted txq.accept -> txq.accept_tx. Run 32990348089 failed it:
"txq.accept_tx not found in txq.accept traces", the only failure in 278 checks.
Skipped rather than left red.
Not a missing span, and not an xrpld defect. Both ends emit on that same run, 5
traces each with all their attributes. The assertion was simply stronger than the
check can evaluate, and the reason is a conditional child meeting newest-N
sampling.
The parent is created once per accept pass, so every ledger close (TxQ.cpp:1499).
The child is created inside the loop over queued transactions and behind
`if (feeLevelPaid >= requiredFeeLevel)` (TxQ.cpp:1530), so it exists only for a
close where the queue actually held a transaction whose fee cleared the level.
_validate_parent_child searches the parent with limit=3
(validate_telemetry.py:803). Queue pressure comes from workload phase 5 of 7,
txq-burst, and mixed-peak (60s) then cooldown (30s) run after it -- so by the time
validation queries, the three newest txq.accept traces are quiet closes with an
empty queue and no child to find.
That is the same shape as the rpc.command.* skips already in this file: sampling
the newest traces of the parent is wrong whenever the child is conditional on load
that has since stopped. Recorded in the reason, with the two real fixes in
preference order -- prefer parent traces that contain the child via a TraceQL
child filter instead of newest-N, or move txq-burst to the final workload phase.
Raising the limit alone only shifts the odds, which would make the check flaky
rather than correct, so it is named and rejected there.
The other 13 assertions added in c531ac569b all PASS, including the three
consensus.round children, the two consensus.establish children,
rpc.http_request -> rpc.process and the three ledger.acquire phases. The
regression gate is clean at 0 regressions now that phase-10 recaptured the
baseline, and both reverse-coverage checks pass.
Verification: JSON parses; 18 relationships, 13 asserted and 5 skipped; counters
still 41 span types; churn 3/1, surgical; otel-naming exits 0; pre-commit clean.
Two problems, both about coverage this workflow claims to have and does not.
The push trigger gated on branch NAME as well as path, and GitHub ANDs the two.
Branch names are not something this repository controls, so a push to any branch
outside "pratik/otel-phase*", "feature/otel-*" or "feature/telemetry-*" was never
dispatched -- not queued, not skipped, no run to look at. That is not a
theoretical gap: two rounds of harness fixes on pratik/otel-sync-diagnostics
produced no signal at all before anyone noticed the workflow had never started.
The branches filter is removed; the paths already express the real question.
The path list was also incomplete in a way that matters more than it looks. The
span-name and metric-name headers are the wire contract this harness asserts
against by literal string, and the convention colocates each one with the class
it serves -- so eight of the ten *SpanNames.h headers live under consensus/,
overlay/, app/ledger/, app/main/, app/misc/, rpc/ and tx/, none of which was
matched. Renaming a span constant therefore compiled clean, emptied the
assertions and triggered nothing. Matched now by filename, "**/*SpanNames.h" and
"**/*MetricNames.h", so future headers are covered wherever they land. Added for
the same reason: include/xrpl/beast/insight (the interface headers decide what
the collector can publish, so they move the metric surface as surely as the
implementation), src/tests/libxrpl/telemetry (the GTests pinning those
constants), and the two checker directories that gate this surface in CI.
Second, the span hierarchy. Each span entry documents its parent, and a separate
list holds the pairs the validator actually checks in Tempo. Those had drifted
apart: 18 parentings were documented, 7 were checked. A span that stops nesting
under its parent -- which is what a detached guard does -- leaves every span and
every attribute intact, so no other check in this harness notices; the trace
simply stops being readable as one operation. Eleven pairs are added, each one
where both ends emitted on a real run: rpc.http_request -> rpc.process, the three
txq parentings, and seven consensus ones under consensus.round and
consensus.establish. Fourteen of eighteen are now asserted; the four still
skipped are the wildcard rpc.command.* families and pathfind.compute.
Three notes were also factually wrong, all repeating one mistake. They said
rpc.process and rpc.http_request cannot appear because that path is HTTP-only
while the load generator is WebSocket-only. The premise is right, the conclusion
is not: both appear on every run, five traces each, because
run-full-validation.sh polls each node's HTTP port with curl for readiness and
validated-ledger progress (:449, :502). Those polls take the HTTP path. A reader
acting on the old text would have gone looking for a way to make the harness
speak HTTP that it already speaks. The rpc.process -> rpc.command.* skip reason
inherited the same error and additionally claimed the WebSocket equivalent is
"asserted above instead", which it is not -- that one is skipped for the same
wildcard limitation. All three now state the real blocker, which is that
_validate_parent_child resolves a wildcard child to a single literal probe.
Both HTTP spans stay optional rather than being promoted: the curl polls are
harness scaffolding, not workload, and a future change to how the script waits
for a node could legitimately remove them.
Verification: JSON parses; 18 relationships, no duplicates, every non-wildcard
endpoint resolves to a declared span entry; counters still 41 span types and 62
unique attributes; workflow YAML parses, has no branches key, keeps
workflow_dispatch, and every new glob was checked against the tracked file list
with a matcher that reproduces GitHub's ** semantics; otel-naming exits 0;
pre-commit clean on both files. The eleven new assertions are proven only to the
extent that both ends emitted on run 32969481032 -- that a child is findable
INSIDE the parent's fetched trace is what CI will now decide.
Every node the harness starts is a validator, and validators disable
pathfinding: Config.cpp:725-726 zeroes pathSearchMax whenever a
[validation_seed] or [validator_token] section is present, and
run-full-validation.sh writes [validation_seed] into every generated node
cfg (:308) with no [path_search] section to put the default back. So
doRipplePathFind refused every call at RipplePathFind.cpp:48-49 and the
3% ripple_path_find weight bought no coverage at all.
It was not free either. The pathfind.request guard is constructed at
RipplePathFind.cpp:35, above that refusal, so each refused call still
exported a span, and the enclosing rpc.command.ripple_path_find span
carried rpc_status=error. That put a steady 3% error floor into
span_calls_total for STATUS_CODE_ERROR: any error-rate threshold derived
from harness data before this change was measuring the harness rather
than xrpld, and needs re-deriving.
Removing the load makes pathfind.request unreachable, so it moves from
required to optional in expected_spans.json; without that the span check
would fail on every run. Three notes in that file and three in
expected_metrics.json made claims that are now false, two of them citing
line numbers this commit deletes; all six are corrected. The runbook
required/optional count moves 26/15 to 25/16.
Two facts a future reader needs.
First, the weights previously summed to 103, not 100, so every percentage
the docstring stated was wrong: health checks were really 38.8%, not 40%.
Dropping the 3 makes the sum exactly 100 and every stated percentage
correct for the first time. expected_spans.json also carried live
arithmetic off the old total, "25/103 ... roughly 43%", now 25/100 and
42%.
Second, baselines/baseline-timings.json was captured WITH this load. Only
span.rpc.ws_message p50/p95/p99 of the 25 gated keys sees the RPC mix,
and their trip points sit 3.1x to 5.9x above baseline, so the gate will
not fire. But a timing baseline is workload-specific and its profile
field still reads full-validation, so nothing will flag the drift:
refresh it from the next CI run's timings artifact.
Pathfinding now has no coverage in this harness at all. The workload
README section "Pathfinding is not exercised" records that cost, the
manual verification route, and a four-step restore recipe in which steps
1 and 2 alone only reinstate the error floor.
The assert / do-not-assert decisions in expected_metrics.json and
expected_spans.json were all correct, but several recorded reasons were not.
Pathfinding is disabled outright on every harness node: Config.cpp:725-726
zeroes pathSearchMax whenever a [validation_seed] or [validator_token] section
is present, run-full-validation.sh writes [validation_seed] for every node and
has no [path_search] override, and both handlers return rpcNOT_SUPPORTED
before constructing a PathRequest.
- pathfind_full_milliseconds no longer claims a probabilistic path, nor
prescribes an explicit ledger index, which cannot help: the config gate
fires before the ledger parameter is read.
- pathfind_fast_milliseconds keeps its hasCompletion argument but now leads
with the config gate, which is the operative blocker.
- The pathfind.compute and pathfind.discover notes and the
pathfind.request to pathfind.compute skip reason no longer blame missing
liquidity. pathfind.update_all now records why its request list stays empty.
- statsd_gauges states the arming precondition: a beast gauge is only as safe
as an observable gauge when its object exists before Application.cpp:1570,
where onCollectionReady arms the registered gauges exactly once.
- Alert wiring claims softened: every rule in rules.yaml is paused.
- The per job type gauge group loses its bogus poll bandwidth reason, and its
regex claim is corrected: there is no running state regex, so 30 of those
gauges have no consumer at all.
- overlay_peer_disconnects has one query consumer, not two.
- Cloud dashboard copies dropped from consumer counts: that tree is ignored by
git and has no tracked files.
- rpc_method_errored_total explains that a refused RPC is a normal return, not
a throw, so the refusals above do not make it fire.
- 09-data-collection-reference.md no longer claims a Prometheus name query in
expected_metrics.json.
No behaviour change: the flattened check name list is byte identical.
The harness manifests asserted things the code cannot produce and missed most
of what it does. Two assertions were failing every run, and the metric set
covered 16 of the ~41 emitted names.
expected_spans.json: rpc.process was required with rpc.ws_message as its
parent, but it is created only in ServerHandler::processRequest() on the HTTP
path, so a WebSocket-only workload never produces it -- it is now optional and
parented to rpc.http_request, and the rpc.process -> rpc.command.* edge is
skipped with the real reason instead of a coroutine-context-loss diagnosis that
was never the cause. Adds the missing rpc.ws_upgrade span, corrects four
parents (consensus.mode_change, pathfind.request, and update_positions/check,
which are children of consensus.establish rather than consensus.round), and
demotes conditionally-set attributes out of required_attributes so a healthy
run stops failing. Counts recomputed from the file: 41 span types, 62 unique
required attributes.
expected_metrics.json: 16 -> 52 asserted entries across the job-queue, RPC
method, reduce-relay, overflow and validation families, plus the fifteenth
dashboard uid. Metrics the harness workload cannot exercise -- erroring RPC,
ledger-mismatch, TxQ overflow, and the lazily-created getobject_* instruments
-- are listed in a not_asserted group the validator skips, rather than as
assertions that would fail on a healthy node.
The workflow's push trigger listed two globs matching nothing
(include/xrpl/basics/Telemetry*.h, src/xrpld/app/misc/Telemetry*), so no C++
telemetry change ever triggered validation. Replaced with the paths the code
actually lives in, including src/libxrpl/beast/insight/** for the insight
export path the harness depends on. The four inert workflow_dispatch inputs are
now labelled UNUSED rather than looking like working knobs.
Docs: the workload README described a StatsD dirty-flag mechanism under a
member name that does not exist, on a code path the harness never uses -- it
sets [insight] server=otel, so gauges export through an observable-gauge
callback every cycle. Adds the missing txq-burst phase, reconciles three
different dashboard counts, and drops "posts summary to PR", which the workflow
has no permission to do. The runbook's phase-10 section loses the last
sampling_ratio reference (not a config key), gains a Regression Gate and CI
subsection covering the gate that can fail CI, and its compose-logs command now
names the workload compose file. cmake --preset default is left for a separate
change: no CMakePresets.json is tracked, so it is wrong everywhere it appears.
Also drops the dead exporter=otlp_http key the harness wrote into every node
config, and stops capture_timings.py defaulting --profile to a profile that
does not exist.
- peer.validation.receive now asserts the shared bare ledger_hash /
full_validation keys (was the dotted xrpl.ledger.hash and validation_full);
PARITY_SPAN_ATTRS checks both on the peer span too.
- Fix a span-name drift: the per-transaction accept span is txq.accept_tx
(op::acceptTx = "accept_tx"), not txq.accept.tx — the old assertion never
matched and was silently skipped as optional.
- Drop the "intentionally dotted" notes; there is no dotted span attribute.
The txq.enqueue span sets tx_hash, tx_type, and txq_status on every
code path (TxQ.cpp:746/748/751), but fee_level_paid and
required_fee_level are set only on the fee-evaluated path
(TxQ.cpp:895-898), which is reached after the rejected and
applied_direct early exits. They are therefore not guaranteed on every
txq.enqueue span, so requiring them caused the validation harness to
fail whenever a txq.enqueue span took an early-exit path.
Remove the two conditional attributes from required_attributes and
document why in the span note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The print-env CI fix let the Telemetry Stack Validation job build and run the
workload harness end-to-end for the first time. It reported 129/136 checks
passing; this commit fixes the 7 real failures plus a latent regression-gate bug.
Validation-suite fixes (verified against the CI run's actual emission + live node):
- expected_metrics.json: the beast::insight job-depth gauge is `xrpld_jobq_job_count`,
not `xrpld_job_count` (the latter is a Phase 9 OTel counter). Reverted the prior
rename. Removed the statsd_histograms block (`xrpld_rpc_time`/`xrpld_rpc_size`):
these RPC timers do not emit under the WS workload (0 series in CI).
- expected_spans.json: `tx_status` is only set on suppressed/known-bad receives, so
it is no longer a required attribute of every `tx.receive`. Marked `pathfind.compute`
and `pathfind.discover` optional and the `pathfind.request -> pathfind.compute`
hierarchy as skip — the self-to-self XRP probe returns before computing paths in a
fresh cluster with no liquidity, so only `pathfind.request` fires.
Regression-gate bug (telemetry-validation.yml "Print regression summary"):
- `jq -e` exits non-zero when its filter result is boolean false — the normal case
for a populated (non-placeholder) baseline — which was misreported as
"Failed to parse baseline JSON" and failed the job. Dropped `-e` (kept `-r`) so a
non-zero exit genuinely means malformed JSON.
The optional-span handling and regression comparison both worked correctly in the
CI run (txq.* / pathfind.update_all skipped-when-absent, 0 regressions detected).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Phase 10 validation harness had drifted from the code's recording surface
and the telemetry-validation CI job was failing before it could build.
CI fix (telemetry-validation.yml):
- Replace nonexistent local action ./.github/actions/print-env with the remote
XRPLF/actions/print-build-env (the build-xrpld job failed in 56s on this).
- Sync prepare-runner and upload-artifact action SHAs to the canonical workflow.
Recording-surface reconciliation (docker/telemetry/workload/):
- Migrate span attributes from dotted xrpl.<domain>.<field> to the bare/underscore
form introduced by the 2026-05-13 span-attr naming redesign (tx_hash, peer_id,
ledger_seq, consensus_mode, consensus_round, full_validation, quorum, ...).
Dotted xrpl.ledger.hash is retained only on peer.validation.receive (shared
constant), while consensus.validation.send uses bare ledger_hash.
- Fix attribute placement: tx.apply carries tx_count/tx_failed (not ledger_seq);
ledger.build carries ledger_seq/close_* (not tx_count/tx_failed).
- Replace the phantom rpc.request span with the real WS root rpc.ws_message; drop
the never-emitted duration_ms; rebuild the parent-child map accordingly.
- Add the new spans the code emits: apply-pipeline stage spans
(tx.preflight/preclaim/transactor with stage/tx_type/ter_result), txq.*,
consensus sub-spans (round/establish/update_positions/check/phase.open),
ledger.acquire, grpc.*, pathfind.*. Conditional spans are marked optional so
they are skipped (not failed) when the workload does not exercise them.
- validate_telemetry.py: service.name and Loki job label rippled -> xrpld; fix
PARITY_SPAN_ATTRS (rename the 4 real attrs, drop the 3 that are metrics not span
attrs); add optional-span handling that skips missing optional spans while still
validating attributes when present.
- expected_metrics.json: rippled_ -> xrpld_ on all beast::insight/overlay metrics,
xrpld_job_count, the 15 on-disk xrpld-* dashboard UIDs, and the real bare
spanmetrics dimension labels.
- regression-metrics.json + baseline-timings.json: rpc.request -> rpc.ws_message.
Metrics pipeline fix:
- Switch node [insight] config from server=statsd/prefix=rippled to server=otel +
/v1/metrics endpoint + prefix=xrpld across run-full-validation.sh,
xrpld-validator.cfg.template, benchmark.sh and the workload compose. The
collector has no StatsD receiver, so system metrics only reach Prometheus over
OTLP.
Synthetic load for new spans:
- Add ripple_path_find to the RPC load generator (drives pathfind.* spans).
- Add a high-TPS txq-burst workload phase to force fee escalation (drives txq.*).
All facts verified against the *SpanNames.h headers and a live xrpld node +
collector (Tempo service.name=xrpld, tx.preflight attrs [stage,ter_result,tx_type],
279 xrpld_ Prometheus metrics and zero rippled_).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Remove duplicate 'system-node-health' UID from expected_metrics.json
(already covered by 'rippled-system-node-health')
- Add parity span attributes to expected_spans.json: node health on
rpc.command.*, validation hash/full on consensus.validation.send,
quorum/proposers on consensus.accept, validation hash/full on
peer.validation.receive
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>