Merge branch 'pratik/otel-phase3-tx-tracing' into pratik/otel-phase4-consensus-tracing

# Conflicts:
#	include/xrpl/telemetry/SpanNames.h
This commit is contained in:
Pratik Mankawde
2026-07-24 16:07:03 +01:00
11 changed files with 136 additions and 6 deletions

View File

@@ -146,6 +146,19 @@ inline constexpr auto closeResolutionMs = makeStr("close_resolution_ms");
*/
inline constexpr auto ledgerHash = makeStr("ledger_hash");
inline constexpr auto fullValidation = makeStr("full_validation");
/**
* Shared "ledger being worked on" attrs — the open/tentative or in-flight
* consensus-build ledger a transaction is applied into, NOT an established or
* validated ledger (that is `ledgerSeq`, set on ledger.build / consensus.round).
* Named after the RPC field `ledger_current_index` and the `currentLedgerSeq`
* log usage. Reused by the tx lifecycle, apply-pipeline, and TxQ spans so a
* transaction's work can be correlated to the ledger it targeted.
* `currentLedgerHash` is the current view's parent-ledger hash, which equals the
* consensus.round deterministic trace-id seed on the consensus-build path.
*/
inline constexpr auto currentLedgerSeq = makeStr("current_ledger_seq");
inline constexpr auto currentLedgerHash = makeStr("current_ledger_hash");
} // namespace attr
// ===== Shared attribute values =============================================

View File

@@ -101,6 +101,15 @@ inline constexpr auto transactor = join(seg::tx, op::transactor);
// ===== Attribute keys ======================================================
namespace attr {
/**
* Shared "ledger being worked on" attrs (defined in SpanNames.h). Set on
* tx.preclaim and tx.transactor (both run against a view whose seq() is the
* ledger being applied into). tx.preflight is stateless (no view) and is the
* documented exception — it carries neither.
*/
using ::xrpl::telemetry::attr::currentLedgerHash;
using ::xrpl::telemetry::attr::currentLedgerSeq;
/**
* "stage" — which apply-pipeline stage this span represents. Drives the
* collector spanmetrics `stage` dimension for per-stage RED metrics.

View File

@@ -1607,6 +1607,13 @@ Transactor::operator()()
span.setAttribute(telemetry::tx_apply_span::attr::stage, telemetry::tx_apply_span::val::apply);
if (auto const* fmt = TxFormats::getInstance().findByType(ctx_.tx.getTxnType()))
span.setAttribute(telemetry::tx_apply_span::attr::txType, fmt->getName().c_str());
// The ledger being worked on (seq + parent hash) — correlates this apply
// stage to the ledger/consensus trace it is building into.
span.setAttribute(
telemetry::tx_apply_span::attr::currentLedgerSeq, static_cast<std::int64_t>(view().seq()));
span.setAttribute(
telemetry::tx_apply_span::attr::currentLedgerHash,
to_string(view().header().parentHash).c_str());
JLOG(j_.trace()) << "apply: " << ctx_.tx.getTransactionID();

View File

@@ -78,9 +78,20 @@ txTypeName(TxType txnType)
* @param name Full span name (tx_apply_span::preflight / ::preclaim).
* @param stage Stage attribute value (tx_apply_span::val::*).
* @param tx The transaction supplying the id and type.
* @param curLedgerSeq Seq of the ledger being worked on, set as the
* current_ledger_seq attribute. Passed by the
* view-bearing stages (preclaim); nullopt for the
* stateless preflight, which then omits it.
* @param curLedgerParentHash Parent hash of that ledger, set as
* current_ledger_hash. nullptr to omit.
*/
[[nodiscard]] telemetry::SpanGuard
makeStageSpan(std::string_view name, std::string_view stage, STTx const& tx)
makeStageSpan(
std::string_view name,
std::string_view stage,
STTx const& tx,
std::optional<LedgerIndex> curLedgerSeq = std::nullopt,
uint256 const* curLedgerParentHash = nullptr)
{
auto const txID = tx.getTransactionID();
auto span = telemetry::SpanGuard::hashSpan(
@@ -92,6 +103,17 @@ makeStageSpan(std::string_view name, std::string_view stage, STTx const& tx)
span.setAttribute(telemetry::tx_apply_span::attr::stage, stage);
if (char const* typeName = txTypeName(tx.getTxnType()))
span.setAttribute(telemetry::tx_apply_span::attr::txType, typeName);
// The ledger being worked on — set only by the view-bearing stages
// (preclaim/transactor). Preflight is stateless and passes nullopt, so
// it carries no ledger attribute (documented exception).
if (curLedgerSeq)
span.setAttribute(
telemetry::tx_apply_span::attr::currentLedgerSeq,
static_cast<std::int64_t>(*curLedgerSeq));
if (curLedgerParentHash)
span.setAttribute(
telemetry::tx_apply_span::attr::currentLedgerHash,
to_string(*curLedgerParentHash).c_str());
}
return span;
}
@@ -224,8 +246,14 @@ static TER
invokePreclaim(PreclaimContext const& ctx)
{
// Trace the preclaim stage under the transaction's deterministic trace_id.
// Preclaim has a ledger view, so tag the span with the ledger being worked
// on (seq + parent hash) to correlate it to the ledger/consensus trace.
auto span = makeStageSpan(
telemetry::tx_apply_span::preclaim, telemetry::tx_apply_span::val::preclaim, ctx.tx);
telemetry::tx_apply_span::preclaim,
telemetry::tx_apply_span::val::preclaim,
ctx.tx,
ctx.view.seq(),
&ctx.view.header().parentHash);
try
{
// use name hiding to accomplish compile-time polymorphism of static

View File

@@ -51,3 +51,12 @@ TEST(TxApplySpanNames, stage_values_are_the_three_pipeline_stages)
EXPECT_EQ(std::string_view(tx_apply_span::val::preclaim), "preclaim");
EXPECT_EQ(std::string_view(tx_apply_span::val::apply), "apply");
}
TEST(TxApplySpanNames, current_ledger_keys_are_shared_constants)
{
// "ledger being worked on" keys, re-exported from the base SpanNames.h so
// every emitter (tx.*, txq.*, apply pipeline) shares one string. Set on
// preclaim/transactor (view-bearing); preflight omits them (stateless).
EXPECT_EQ(std::string_view(tx_apply_span::attr::currentLedgerSeq), "current_ledger_seq");
EXPECT_EQ(std::string_view(tx_apply_span::attr::currentLedgerHash), "current_ledger_hash");
}

View File

@@ -1390,6 +1390,12 @@ NetworkOPsImp::processTransaction(
auto span = std::make_shared<SpanGuard>(txProcessSpan(transaction->getID()));
span->setAttribute(tx_span::attr::txHash, to_string(transaction->getID()).c_str());
span->setAttribute(tx_span::attr::local, bLocal);
// The current (open) ledger index at submission/relay time — the ledger
// being worked on. Correlates this tx.process to the ledger trace; the tx
// has not yet been applied to a specific ledger here, so there is no hash.
span->setAttribute(
tx_span::attr::currentLedgerSeq,
static_cast<std::int64_t>(ledgerMaster_.getCurrentLedgerIndex()));
if (auto const& stx = transaction->getSTransaction())
{
if (auto const* fmt = TxFormats::getInstance().findByType(stx->getTxnType()))
@@ -1600,8 +1606,19 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
if (e.failType == FailHard::Yes)
flags |= TapFailHard;
// Parent the txq.enqueue span to this tx's tx.process span
// via an explicit captured context (the parent is explicit,
// not ambient-inherited). Null on the open-ledger rebuild
// path, where no tx.process span exists.
auto const txProcessCtx =
(e.span && *e.span) ? e.span->spanContext() : telemetry::SpanContext{};
auto const result = registry_.get().getTxQ().apply(
registry_.get().getApp(), view, e.transaction->getSTransaction(), flags, j);
registry_.get().getApp(),
view,
e.transaction->getSTransaction(),
flags,
j,
txProcessCtx.isValid() ? &txProcessCtx : nullptr);
e.result = result.ter;
e.applied = result.applied;
changed = changed || result.applied;

View File

@@ -16,6 +16,7 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/Units.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/telemetry/SpanGuard.h>
#include <xrpl/tx/applySteps.h>
#include <boost/circular_buffer.hpp>
@@ -334,7 +335,8 @@ public:
OpenView& view,
std::shared_ptr<STTx const> const& tx,
ApplyFlags flags,
beast::Journal j);
beast::Journal j,
telemetry::SpanContext const* parentCtx = nullptr);
/**
* Fill the new open ledger with transactions from the queue.

View File

@@ -749,13 +749,35 @@ TxQ::apply(
OpenView& view,
std::shared_ptr<STTx const> const& tx,
ApplyFlags flags,
beast::Journal j)
beast::Journal j,
telemetry::SpanContext const* parentCtx)
{
using namespace telemetry;
ScopedSpanGuard span(TraceCategory::Transactions, txq_span::prefix::txq, txq_span::op::enqueue);
// Parent the enqueue span to the caller's tx.process span when a context is
// supplied (submission path), using the explicit-context factory: the parent
// comes from the captured ctx rather than being inherited from whatever span
// is ambient, and this ScopedSpanGuard's scope is RAII-bounded to this fully
// synchronous call (no coroutine yield), so no unrelated parent leaks in and
// its scope cannot leak out onto a reused worker.
// On the open-ledger rebuild path parentCtx is null and this is a root; the
// current_ledger_seq attribute below correlates it to the ledger instead.
// A lambda (not a ternary) picks the factory: ScopedSpanGuard's move ctor is
// deleted, so guaranteed copy elision on each return is the only way to
// construct it conditionally.
auto span = [&]() -> ScopedSpanGuard {
if (parentCtx && parentCtx->isValid())
return ScopedSpanGuard::childSpan(txq_span::enqueue, *parentCtx);
return ScopedSpanGuard(
TraceCategory::Transactions, txq_span::prefix::txq, txq_span::op::enqueue);
}();
span.setAttribute(txq_span::attr::txHash, to_string(tx->getTransactionID()).c_str());
if (auto const* fmt = TxFormats::getInstance().findByType(tx->getTxnType()))
span.setAttribute(txq_span::attr::txType, fmt->getName().c_str());
// The ledger being worked on (open/tentative apply or in-flight consensus
// build) — correlates this enqueue to the ledger trace in every context.
span.setAttribute(txq_span::attr::currentLedgerSeq, static_cast<std::int64_t>(view.seq()));
span.setAttribute(
txq_span::attr::currentLedgerHash, to_string(view.header().parentHash).c_str());
// Default outcome; overridden below on the direct-apply and queued paths.
// Every other early return leaves the tx rejected from the queue.
span.setAttribute(txq_span::attr::txqStatus, txq_span::val::rejected);

View File

@@ -73,12 +73,22 @@ inline constexpr auto acceptTx = makeStr("accept_tx");
inline constexpr auto cleanup = makeStr("cleanup");
} // namespace op
// ===== Full span names (prefix.op) ===========================================
//
// Joined "txq.<op>" names for the explicit-context factories
// (SpanGuard::childSpan(name, ctx)) that take one full span name rather than a
// prefix/suffix pair.
inline constexpr auto enqueue = join(prefix::txq, op::enqueue);
// ===== Attribute keys ======================================================
namespace attr {
/**
* Canonical shared constants (defined in SpanNames.h).
*/
using ::xrpl::telemetry::attr::currentLedgerHash;
using ::xrpl::telemetry::attr::currentLedgerSeq;
using ::xrpl::telemetry::attr::ledgerSeq;
using ::xrpl::telemetry::attr::txHash;

View File

@@ -1318,6 +1318,12 @@ PeerImp::handleTransaction(
auto span = std::make_shared<SpanGuard>(txReceiveSpan(txID, *m));
span->setAttribute(tx_span::attr::txHash, to_string(txID).c_str());
span->setAttribute(tx_span::attr::peerId, static_cast<int64_t>(id_));
// The current (open) ledger index when the relayed tx was received —
// the ledger being worked on. Correlates this tx.receive to the ledger
// trace; not yet applied to a specific ledger here, so no hash.
span->setAttribute(
tx_span::attr::currentLedgerSeq,
static_cast<std::int64_t>(app_.getLedgerMaster().getCurrentLedgerIndex()));
if (auto const* fmt = TxFormats::getInstance().findByType(stx->getTxnType()))
span->setAttribute(tx_span::attr::txType, fmt->getName().c_str());
if (auto const version = getVersion(); !version.empty())

View File

@@ -50,6 +50,13 @@ namespace attr {
using ::xrpl::telemetry::attr::peerId;
using ::xrpl::telemetry::attr::txHash;
/**
* "current_ledger_seq" — the open/current ledger index at the time the tx is
* received or submitted (the ledger being worked on), NOT an established ledger.
* No parent-ledger hash here: these entry points have no ledger view.
*/
using ::xrpl::telemetry::attr::currentLedgerSeq;
/**
* "local" — whether tx originated locally.
*/