Files
rippled/include/xrpl/consensus/ConsensusSpanNames.h
Pratik Mankawde 70ae3ff922 Merge branch 'pratik/otel-phase10-workload-validation' into pratik/otel-sync-diagnostics
Phase-10 brought in the upstream nodestore/peerfinder/consensus reorganisation
along with its own write-path telemetry, which collided with the sync-diagnostic
signals on this branch. Twelve files conflicted; every resolution keeps both
intents rather than picking a side.

The nodestore write timing was implemented twice, independently. Both sides
added getStoreDurationUs()/getFetchDurationUs() to Database and both timed the
backend call in each concrete store(). Keeping both would have added twice to
storeDurationUs_ per store while storeStats() still counted one, so the mean
write latency would have read double on every dashboard -- silently, since no
test on either side asserts an exact microsecond figure. Resolved to one
accumulator API: recordStoreDuration(), which takes a duration, clamps a
sub-microsecond sample to zero and uses a relaxed atomic add. Phase-10's
storeDurationStats() is gone and its two call sites now use the survivor, so
all three store paths -- both store() overrides and importInternal() -- add
exactly once.

SlotCensus and its pure virtual moved from src/xrpld/peerfinder/ to
include/xrpl/peerfinder/PeerfinderManager.h, following the Manager interface
upstream relocated. The xrpld header is now phase-10's makeConfig shim, and
Overlay.h, MetricMacros.cpp and the getSlotCensus() override chain point at the
new location. ConsensusSpanNames.h and peerfinder Slot.h/Config.h include paths
followed their headers into libxrpl the same way.

InboundLedger gained phase-10's AcquireStats counters next to this branch's
span activations in both the destructor abort path and done(); neither
displaces the other. nodestore_state keeps the constant-based name this branch
requires of it and phase-10's fuller description.

Upstream #7292 deleted src/test/nodestore/Database_test.cpp, which held this
branch's testDurationAccessors. Phase-10 restored the per-store half of that
coverage in DatabaseConfig_test, but nothing covered importInternal -- it writes
through storeBatch() and never through store(), so it is a third store path that
has to time itself. That half is ported to a GTest in
src/tests/libxrpl/nodestore/Database.cpp, keeping the exact zero-before and
accumulate-after assertions and the per-instance negative check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 11:17:53 +01:00

408 lines
18 KiB
C++

#pragma once
/**
* Compile-time span name constants for consensus tracing.
*
* Used by RCLConsensus (app), Consensus.h (template), and PeerImp
* (overlay) for consensus lifecycle spans.
* Built on StaticStr/join() from SpanNames.h.
*
* ## Span Hierarchy
*
* Root span created in Adaptor::startRoundTracing(). In "deterministic"
* strategy the trace-id is derived from the previous ledger hash so all
* nodes tracing the same round share a trace.
*
* consensus.round [main thread, root]
* | Created: Adaptor::startRoundTracing()
* | Attrs: consensus_ledger_id, ledger_seq, consensus_mode,
* | trace_strategy, consensus_round_id
* |
* +-- consensus.phase.open [main thread, child]
* | Created: Consensus::startRoundInternal()
* | Ended: Consensus::closeLedger()
* |
* +-- consensus.proposal.send [main thread]
* | Created: Adaptor::propose()
* | Attrs: consensus_round (proposeSeq)
* |
* +-- consensus.ledger_close [main thread]
* | Created: Adaptor::onClose()
* | Attrs: ledger_seq, consensus_mode
* |
* +-- consensus.establish [main thread, child]
* | Created: Consensus::startEstablishTracing()
* | Ended: Consensus::phaseEstablish() on accept
* | Attrs: converge_percent, establish_count, proposers
* |
* +-- consensus.update_positions [main thread]
* | Created: Consensus::updateOurPositions()
* | Attrs: converge_percent, proposers, disputes_count
* | Events: per-dispute vote details (tx_id, our_vote, yays, nays)
* |
* +-- consensus.check [main thread]
* | Created: Consensus::haveConsensus()
* | Attrs: agree/disagree counts, threshold_percent, result
* |
* +-- consensus.accept [main thread, child of round]
* | Created: Adaptor::makeAcceptSpan(), shared_ptr kept alive
* | until doAccept() completes on jtACCEPT thread
* | Attrs: proposers, round_time_ms, quorum
* | |
* | +-- consensus.accept.apply [jtACCEPT thread, child of accept]
* | Created: Adaptor::doAccept()
* | Attrs: ledger_seq, close_time, close_time_correct,
* | close_resolution_ms, consensus_state, proposing, round_time_ms,
* | parent_close_time, close_time_self, close_time_vote_bins,
* | resolution_direction, tx_count
* | Events: tx.included (per tx, attrs: tx_id)
* |
* +~~~ consensus.validation.send [jtACCEPT thread, linked]
* | Created: Adaptor::createValidationSpan() (follows-from link)
* | Attrs: ledger_seq, proposing
* |
* +-- consensus.mode_change [main thread]
* Created: Adaptor::onModeChange()
* Attrs: mode_old, mode_new
*
* Standalone spans (no parent, created per-message in overlay):
*
* consensus.proposal.receive [PeerImp I/O thread]
* Created: PeerImp::onMessage(TMProposeSet)
*
* consensus.validation.receive [PeerImp I/O thread]
* Created: PeerImp::onMessage(TMValidation)
*
* ## Per-ledger trace (separate from the per-round trace above)
*
* consensus.validation.accept sits in a DIFFERENT trace from the spans above.
* The round trace is keyed on the PREVIOUS ledger hash (the round's seed);
* this span is keyed on the hash of the ledger the arriving validation is FOR,
* the same key LedgerMaster uses for ledger.validate and ledger.store, so a
* reader following one slow ledger sees the validation that triggered its
* acceptance beside the acceptance itself:
*
* trace_id = validatedLedgerHash[0:16]
* +-- consensus.validation.accept [JtValidationT worker / consensus thread]
* +-- ledger.validate [LedgerMaster::checkAccept]
* +-- ledger.store [LedgerMaster::storeLedger]
*
* Legend:
* +-- child-of relationship (same trace)
* +~~~ follows-from link (separate sub-tree, causal link)
*/
#include <xrpl/telemetry/SpanNames.h>
#include <string_view>
namespace xrpl::telemetry::consensus::span {
// ===== Span name segments ====================================================
namespace part {
inline constexpr auto proposal = makeStr("proposal");
inline constexpr auto validation = makeStr("validation");
inline constexpr auto accept = makeStr("accept");
inline constexpr auto phase = makeStr("phase");
} // namespace part
namespace op {
inline constexpr auto round = makeStr("round");
inline constexpr auto proposalSend = join(part::proposal, makeStr("send"));
inline constexpr auto ledgerClose = makeStr("ledger_close");
inline constexpr auto establish = makeStr("establish");
inline constexpr auto updatePositions = makeStr("update_positions");
inline constexpr auto check = makeStr("check");
inline constexpr auto accept = makeStr("accept");
inline constexpr auto acceptApply = join(part::accept, makeStr("apply"));
inline constexpr auto validationSend = join(part::validation, makeStr("send"));
inline constexpr auto modeChange = makeStr("mode_change");
inline constexpr auto proposalReceive = join(part::proposal, makeStr("receive"));
inline constexpr auto validationReceive = join(part::validation, makeStr("receive"));
inline constexpr auto phaseOpen = join(part::phase, makeStr("open"));
/**
* "validation.accept" — an arriving trusted validation being handed to the
* acceptance gate. Distinct from `validationReceive`, which is the overlay
* decoding a validation message: this is the later, per-ledger step where the
* validation is counted toward accepting a specific ledger.
*/
inline constexpr auto validationAccept = join(part::validation, makeStr("accept"));
} // namespace op
// ===== Full span names (prefix.op) ===========================================
inline constexpr auto round = join(seg::consensus, op::round);
inline constexpr auto proposalSend = join(seg::consensus, op::proposalSend);
inline constexpr auto ledgerClose = join(seg::consensus, op::ledgerClose);
inline constexpr auto establish = join(seg::consensus, op::establish);
inline constexpr auto updatePositions = join(seg::consensus, op::updatePositions);
inline constexpr auto check = join(seg::consensus, op::check);
inline constexpr auto accept = join(seg::consensus, op::accept);
inline constexpr auto acceptApply = join(seg::consensus, op::acceptApply);
inline constexpr auto validationSend = join(seg::consensus, op::validationSend);
inline constexpr auto modeChange = join(seg::consensus, op::modeChange);
inline constexpr auto proposalReceive = join(seg::consensus, op::proposalReceive);
inline constexpr auto validationReceive = join(seg::consensus, op::validationReceive);
inline constexpr auto phaseOpen = join(seg::consensus, op::phaseOpen);
/**
* "consensus.validation.accept" — full name, passed to `SpanGuard::hashSpan()`
* (which takes one complete span name) so the span adopts the trace id derived
* from the VALIDATED ledger's hash and joins that ledger's trace.
*/
inline constexpr auto validationAccept = join(seg::consensus, op::validationAccept);
// ===== Attribute keys ========================================================
namespace attr {
/**
* Canonical shared constants (defined in SpanNames.h). `ledgerHash` and
* `fullValidation` are shared with the peer.validation.receive span — same
* concept, same key, distinguished by span name (not an emitter prefix).
*/
using ::xrpl::telemetry::attr::closeResolutionMs;
using ::xrpl::telemetry::attr::closeTime;
using ::xrpl::telemetry::attr::closeTimeCorrect;
using ::xrpl::telemetry::attr::fullValidation;
using ::xrpl::telemetry::attr::ledgerHash;
using ::xrpl::telemetry::attr::ledgerSeq;
/**
* Domain-qualified attrs (rule 5 — bare name ambiguous across domains).
* Use `<domain>_<field>` underscore form for TraceQL ergonomics.
*/
inline constexpr auto ledgerId = makeStr("consensus_ledger_id");
inline constexpr auto mode = makeStr("consensus_mode");
inline constexpr auto round = makeStr("consensus_round");
inline constexpr auto roundId = makeStr("consensus_round_id");
/**
* Current phase name attached to consensus.round; updated on each
* phase transition event (open/establish/accepted).
*/
inline constexpr auto consensusPhase = makeStr("consensus_phase");
/**
* Boolean flag set on consensus.check when checkConsensus reports stalled.
*/
inline constexpr auto consensusStalled = makeStr("consensus_stalled");
/**
* Domain-owned bare attrs.
*/
inline constexpr auto proposers = makeStr("proposers");
inline constexpr auto roundTimeMs = makeStr("round_time_ms");
inline constexpr auto proposing = makeStr("proposing");
/**
* Round continuity / context attrs (set on consensus.round at round start).
*/
inline constexpr auto previousProposers = makeStr("previous_proposers");
inline constexpr auto previousRoundTimeMs = makeStr("previous_round_time_ms");
inline constexpr auto previousLedgerSeq = makeStr("previous_ledger_seq");
inline constexpr auto closeTimeResolutionMs = makeStr("close_time_resolution_ms");
/**
* Open-phase end metadata (set on consensus.phase.open before reset).
*/
inline constexpr auto openDurationMs = makeStr("open_duration_ms");
inline constexpr auto peerPositionsAtClose = makeStr("peer_positions_at_close");
/**
* Ledger-close inputs.
*/
inline constexpr auto txCountOpen = makeStr("tx_count_open");
/**
* Establish/check additional state.
*/
inline constexpr auto proposersFinished = makeStr("proposers_finished");
/**
* Accept/apply enrichment.
*/
inline constexpr auto disputesResolvedCount = makeStr("disputes_resolved_count");
/**
* Validation send/receive enrichment. (`full_validation` is shared — see the
* `using` re-export above.)
*/
inline constexpr auto validationSignTime = makeStr("validation_sign_time");
/**
* "validation_status" — what the validation store did with an arriving
* validation, set on consensus.validation.accept. One of the bounded
* `ValStatus` names (see `val::status*`), so it is safe to aggregate: it is the
* difference between "validations are arriving and counting" and "they arrive
* and are all rejected", which on a stuck node look identical from the outside.
*/
inline constexpr auto validationStatus = makeStr("validation_status");
/**
* "accept_gated" — true when this validation did NOT reach the acceptance gate
* because another thread was already accepting the same ledger (the
* bypass-accept path). Two values, so it is aggregatable; without it the
* absence of a following ledger.validate span in the trace is unexplained.
*/
inline constexpr auto acceptGated = makeStr("accept_gated");
/**
* Receive-side hash prefixes for cross-peer correlation.
*/
inline constexpr auto prevLedgerPrefix = makeStr("prev_ledger_prefix");
inline constexpr auto positionHashPrefix = makeStr("position_hash_prefix");
/**
* "consensus_state" — domain-qualified (collides with other domains' state).
*/
inline constexpr auto consensusState = makeStr("consensus_state");
inline constexpr auto parentCloseTime = makeStr("parent_close_time");
inline constexpr auto closeTimeSelf = makeStr("close_time_self");
inline constexpr auto closeTimeVoteBins = makeStr("close_time_vote_bins");
inline constexpr auto resolutionDirection = makeStr("resolution_direction");
inline constexpr auto convergePercent = makeStr("converge_percent");
inline constexpr auto establishCount = makeStr("establish_count");
inline constexpr auto avalancheThreshold = makeStr("avalanche_threshold");
inline constexpr auto closeTimeThreshold = makeStr("close_time_threshold");
inline constexpr auto haveCloseTimeConsensus = makeStr("have_close_time_consensus");
inline constexpr auto agreeCount = makeStr("agree_count");
inline constexpr auto disagreeCount = makeStr("disagree_count");
inline constexpr auto thresholdPercent = makeStr("threshold_percent");
/**
* "consensus_result" — domain-qualified (collides with generic result).
*/
inline constexpr auto consensusResult = makeStr("consensus_result");
inline constexpr auto quorum = makeStr("quorum");
inline constexpr auto traceStrategy = makeStr("trace_strategy");
inline constexpr auto modeOld = makeStr("mode_old");
inline constexpr auto modeNew = makeStr("mode_new");
/**
* "is_bow_out" — whether this proposal is a bow-out (resigning from round).
*/
inline constexpr auto isBowOut = makeStr("is_bow_out");
/**
* Transaction/dispute attrs used in consensus accept spans.
*/
inline constexpr auto txId = makeStr("tx_id");
inline constexpr auto disputeOurVote = makeStr("dispute_our_vote");
inline constexpr auto disputeYays = makeStr("dispute_yays");
inline constexpr auto disputeNays = makeStr("dispute_nays");
inline constexpr auto txCount = makeStr("tx_count");
inline constexpr auto disputesCount = makeStr("disputes_count");
/**
* Trust flag (is the message origin a trusted UNL validator). Qualified by
* message type, shared with the peer.{proposal,validation}.receive spans:
* consensus.proposal.receive uses `proposal_trusted`, consensus.validation.
* receive uses `validation_trusted`. Same concept on both emitters → same key.
*/
inline constexpr auto proposalTrusted = makeStr("proposal_trusted");
inline constexpr auto validationTrusted = makeStr("validation_trusted");
} // namespace attr
// ===== Event names ===========================================================
namespace event {
/**
* "dispute.resolve"
*/
inline constexpr auto disputeResolve = join(makeStr("dispute"), makeStr("resolve"));
/**
* "tx.included"
*/
inline constexpr auto txIncluded = join(makeStr("tx"), makeStr("included"));
/**
* Phase transition events — fired on consensus.round at each transition
* so the round-level span carries a complete timeline of phase changes,
* including the handleWrongLedger recovery edge that re-enters Open.
*/
inline constexpr auto phaseOpen = join(makeStr("phase"), makeStr("open"));
inline constexpr auto phaseEstablish = join(makeStr("phase"), makeStr("establish"));
inline constexpr auto phaseAccepted = join(makeStr("phase"), makeStr("accepted"));
inline constexpr auto phaseRecovery = join(makeStr("phase"), makeStr("recovery"));
/**
* Outcome events — fired on consensus.round at the establish→accepted
* transition so the path that drove acceptance is queryable.
*/
inline constexpr auto outcomeYes = join(makeStr("outcome"), makeStr("yes"));
inline constexpr auto outcomeMovedOn = join(makeStr("outcome"), makeStr("moved_on"));
inline constexpr auto outcomeExpired = join(makeStr("outcome"), makeStr("expired"));
} // namespace event
// ===== Attribute values ======================================================
namespace val {
inline constexpr auto finished = makeStr("finished");
inline constexpr auto movedOn = makeStr("moved_on");
inline constexpr auto yes = makeStr("yes");
inline constexpr auto no = makeStr("no");
inline constexpr auto expired = makeStr("expired");
inline constexpr auto increased = makeStr("increased");
inline constexpr auto decreased = makeStr("decreased");
inline constexpr auto unchanged = makeStr("unchanged");
// consensus_phase attribute values (the phase the round is entering).
inline constexpr auto phaseOpen = makeStr("open");
inline constexpr auto phaseEstablish = makeStr("establish");
inline constexpr auto phaseAccepted = makeStr("accepted");
/**
* validation_status values — the five `ValStatus` outcomes of adding an
* arriving validation to the validation store, plus a sentinel.
*
* Only `current` continues to the acceptance gate; every other value means the
* validation was counted for nothing, which is exactly the state a node stuck
* below quorum is in. Spelled here rather than reusing `to_string(ValStatus)`
* because that function returns camelCase ("badSeq"), and attribute values on
* an aggregated dimension must stay lower_snake_case.
*/
inline constexpr auto statusCurrent = makeStr("current");
inline constexpr auto statusStale = makeStr("stale");
inline constexpr auto statusBadSeq = makeStr("bad_seq");
inline constexpr auto statusMultiple = makeStr("multiple");
inline constexpr auto statusConflicting = makeStr("conflicting");
inline constexpr auto statusUnknown = makeStr("unknown");
} // namespace val
// ===== Value rules ===========================================================
/**
* Map a `ValStatus` to its `validation_status` attribute value.
*
* Takes a plain int rather than the enum so this header stays free of
* `Validations.h` (which pulls in the whole validation-store template) and so
* the rule can be asserted directly from the lib-only test binary, which cannot
* link xrpld. The caller passes `static_cast<int>(status)`; the enumerator order
* is fixed by `ValStatus` and asserted by the paired unit test, which is what
* keeps this mapping honest if a value is ever inserted.
*
* @param valStatus `ValStatus` as an int: 0 Current, 1 Stale, 2 BadSeq,
* 3 Multiple, 4 Conflicting.
* @return The matching `val::status*` value, or `unknown` for anything else.
*
* Example — the two readings that matter on a stuck node:
* @code
* validationStatusValue(0); // "current" -- counted, gate will be tried
* validationStatusValue(2); // "bad_seq" -- rejected, counted for nothing
* @endcode
*
* Example — edge case: an out-of-range value still yields a usable label rather
* than an empty attribute, so the dimension never gains a blank series:
* @code
* validationStatusValue(99); // "unknown"
* @endcode
*
* @note Pure and side-effect free; safe to call from any thread.
*/
[[nodiscard]] constexpr std::string_view
validationStatusValue(int valStatus) noexcept
{
switch (valStatus)
{
case 0:
return val::statusCurrent;
case 1:
return val::statusStale;
case 2:
return val::statusBadSeq;
case 3:
return val::statusMultiple;
case 4:
return val::statusConflicting;
default:
return val::statusUnknown;
}
}
} // namespace xrpl::telemetry::consensus::span