Merge branch 'pratik/otel-phase2-rpc-tracing' into pratik/otel-phase3-tx-tracing

This commit is contained in:
Pratik Mankawde
2026-08-27 12:14:00 +01:00
9 changed files with 189 additions and 49 deletions

View File

@@ -13,6 +13,12 @@
#include <xrpl/telemetry/Telemetry.h>
#ifndef XRPL_ENABLE_TELEMETRY
// beast::Journal is named only by the compiled-out makeTelemetry() below, so
// this include belongs to that configuration and would be unused in the other.
#include <xrpl/beast/utility/Journal.h>
#endif
#include <memory>
#include <utility>

View File

@@ -602,7 +602,14 @@ PathRequest::findPaths(
span.setAttribute(
pathfind_span::attr::numSourceAssets, static_cast<int64_t>(sourceAssets.size()));
#ifdef XRPL_ENABLE_TELEMETRY
// Only the numPaths attribute at the end of this function reads this, so it
// is not maintained at all when telemetry is compiled out. An #ifdef rather
// than `if (span)`, because the attribute cannot read a variable that does
// not exist, and a counter kept up to date but never read is an unused
// variable, which fails the build.
std::int64_t totalPaths = 0;
#endif
for (auto const& asset : sourceAssets)
{
if (continueCallback && !continueCallback())
@@ -622,7 +629,9 @@ PathRequest::findPaths(
auto ps = pathfinder->getBestPaths(
kMaxPaths, fullLiquidityPath, context_[asset], asset.getIssuer(), continueCallback);
context_[asset] = ps;
#ifdef XRPL_ENABLE_TELEMETRY
totalPaths += static_cast<std::int64_t>(ps.size());
#endif
auto const& sourceAccount = [&] {
if (!isXRP(asset.getIssuer()))
@@ -725,7 +734,9 @@ PathRequest::findPaths(
}
}
#ifdef XRPL_ENABLE_TELEMETRY
span.setAttribute(pathfind_span::attr::numPaths, totalPaths);
#endif
/* The resource fee is based on the number of source currencies used.
The minimum cost is 50 and the maximum is 400. The cost increases
@@ -748,21 +759,34 @@ PathRequest::doUpdate(
// nests under it. doUpdate does not yield, so scoping is safe.
auto span = ScopedSpanGuard(
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::compute);
span.setAttribute(pathfind_span::attr::fast, fast);
// to_string(Issue) renders a non-XRP asset as "<issuer>/<currency>" with the
// issuer as a plaintext Base58 address, so it cannot be emitted as-is: every
// account reaching a span is hashed first. Redact just the issuer and keep
// the currency, which is what this attribute is for. An MPT asset renders as
// its issuance ID and carries no address, so it needs no redaction.
span.setAttribute(
pathfind_span::attr::destCurrency,
saDstAmount_.asset().visit(
[](Issue const& issue) {
return isXRP(issue.account)
? to_string(issue.currency)
: redactAccount(toBase58(issue.account)) + "/" + to_string(issue.currency);
},
[](MPTIssue const& mpt) { return to_string(mpt.getMptID()); }));
// Guarded on the span being live because setAttribute's arguments are
// evaluated whatever the build, and doUpdate is hot: PathRequestManager
// calls it once per active path_find subscription on every ledger close, so
// a node with N subscriptions pays this N times a close. The destCurrency
// value costs a base58check encode of the issuer (two SHA-256 rounds), a
// SHA-512Half over the result and three string allocations. The compiled-out
// guard's operator bool() is a literal false, so the block disappears
// entirely in that build; with telemetry compiled in it is skipped when
// telemetry is disabled at runtime or the pathfind category is off.
if (span)
{
span.setAttribute(pathfind_span::attr::fast, fast);
// to_string(Issue) renders a non-XRP asset as "<issuer>/<currency>" with
// the issuer as a plaintext Base58 address, so it cannot be emitted
// as-is: every account reaching a span is hashed first. Redact just the
// issuer and keep the currency, which is what this attribute is for. An
// MPT asset renders as its issuance ID and carries no address, so it
// needs no redaction.
span.setAttribute(
pathfind_span::attr::destCurrency,
saDstAmount_.asset().visit(
[](Issue const& issue) {
return isXRP(issue.account)
? to_string(issue.currency)
: redactAccount(toBase58(issue.account)) + "/" + to_string(issue.currency);
},
[](MPTIssue const& mpt) { return to_string(mpt.getMptID()); }));
}
JLOG(journal_.debug()) << iIdentifier_ << " update " << (fast ? "fast" : "normal");

View File

@@ -3,7 +3,6 @@
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/rpc/detail/AssetCache.h>
#include <xrpld/rpc/detail/PathFindSpanNames.h>
#include <xrpld/rpc/detail/PathRequest.h>
#include <xrpl/basics/Log.h>
@@ -16,17 +15,26 @@
#include <xrpl/protocol/jss.h>
#include <xrpl/resource/Consumer.h>
#include <xrpl/server/InfoSub.h>
#include <xrpl/telemetry/SpanGuard.h>
#include <algorithm>
#include <cstdint>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <utility>
#include <vector>
// Needed only by the update_all span in updateAll(), which is compiled out when
// telemetry is off. Without the same guard here they would be unused includes in
// that build, which clang-tidy's misc-include-cleaner rejects.
#ifdef XRPL_ENABLE_TELEMETRY
#include <xrpld/rpc/detail/PathFindSpanNames.h>
#include <xrpl/telemetry/SpanGuard.h>
#include <optional>
#endif // XRPL_ENABLE_TELEMETRY
namespace xrpl {
/**
@@ -75,12 +83,16 @@ PathRequestManager::updateAll(std::shared_ptr<ReadView const> const& inLedger)
cache = getAssetCache(inLedger, true);
}
#ifdef XRPL_ENABLE_TELEMETRY
using namespace telemetry;
// updateAll runs on every ledger close. Skip span emission when there are
// no active path subscriptions, to avoid a steady stream of empty spans at
// mainnet close cadence. All other work still runs unchanged (notably the
// isNewPathRequest() flag reset below), so behaviour matches the pre-span
// code path.
// Nothing outside telemetry reads this block, and updateAll runs on every
// ledger close, so it is compiled out entirely when telemetry is off rather
// than left to construct a stub guard and discard two attributes per close.
// No span object exists to test here, so the guard has to be an #ifdef.
//
// Skip span emission when there are no active path subscriptions, to avoid
// a steady stream of empty spans at mainnet close cadence. All other work
// still runs unchanged (notably the isNewPathRequest() flag reset below).
//
// Scoped, so the pathfind.compute spans that doUpdate() creates below on
// this thread nest under it. std::optional because ScopedSpanGuard is
@@ -93,6 +105,7 @@ PathRequestManager::updateAll(std::shared_ptr<ReadView const> const& inLedger)
span->setAttribute(pathfind_span::attr::ledgerIndex, static_cast<int64_t>(inLedger->seq()));
span->setAttribute(pathfind_span::attr::numRequests, static_cast<int64_t>(requests.size()));
}
#endif // XRPL_ENABLE_TELEMETRY
bool newRequests = app_.getLedgerMaster().isNewPathRequest();
bool mustBreak = false;

View File

@@ -223,6 +223,14 @@ callMethod(JsonContext& context, Method method, std::string const& name, Object&
}
}
// Telemetry-only helper, so it is compiled out with telemetry off. Left
// running it would cost several json lookups, up to two string copies and a
// handler-table lookup on every failed request, for a name nobody records.
// Its result IS the span name, so `if (span)` cannot guard it: at that point
// no span exists to test. The single call site is gated the same way, which
// also keeps this file-static function referenced in both configurations.
#ifdef XRPL_ENABLE_TELEMETRY
// Resolve the span suffix / command attribute for a request that failed in
// fillHandler. Returns the canonical handler name for a recognized command
// (a finite, bounded set) or the literal "unknown" for a request that omits
@@ -256,6 +264,8 @@ resolveCommandSpanName(JsonContext const& context)
: std::string_view{rpc_span::val::unknownCommand};
}
#endif // XRPL_ENABLE_TELEMETRY
} // namespace
Status
@@ -264,6 +274,11 @@ doCommand(rpc::JsonContext& context, json::Value& result)
Handler const* handler = nullptr;
if (auto error = fillHandler(context, handler))
{
// Every statement below only feeds the error span, and the span name
// itself comes from resolveCommandSpanName(), so there is no span
// object to test with `if (span)`. With telemetry off the whole block
// is compiled out and a storm of malformed requests pays nothing.
#ifdef XRPL_ENABLE_TELEMETRY
// Bound the span name and command attribute to the finite set of
// registered handler names (plus "unknown") — see the helper for why
// raw request input must not reach the telemetry pipeline.
@@ -279,6 +294,7 @@ doCommand(rpc::JsonContext& context, json::Value& result)
: std::string_view(rpc_span::val::user));
span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error);
span.setError(getErrorInfo(error).token.cStr());
#endif // XRPL_ENABLE_TELEMETRY
injectError(error, result);
return error;

View File

@@ -478,7 +478,15 @@ ServerHandler::processSession(
// else collapses to "unknown". Emitting the raw string would let request
// input drive unbounded label cardinality. Mirrors the HTTP path's
// resolveCommandSpanName().
span.setAttribute(rpc_span::attr::command, resolveWsCommandSpanName(jv, app_.config()));
//
// The guard is required because the resolver is a call argument: it runs
// even when setAttribute itself is an empty no-op. Without it, every
// WebSocket message pays for the JSON member lookups, a string copy and a
// handler-registry lookup that nothing reads.
if (span)
{
span.setAttribute(rpc_span::attr::command, resolveWsCommandSpanName(jv, app_.config()));
}
auto is = std::static_pointer_cast<WSInfoSub>(session->appDefined);
if (is->getConsumer().disconnect(journal_))

View File

@@ -25,16 +25,27 @@ doPathFind(rpc::JsonContext& context)
// thread) nest under it. doPathFind does not yield, so scoping is safe.
auto span = ScopedSpanGuard(
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::request);
// Addresses are hashed before emission for privacy. Read through a const
// reference: the non-const json::Value::operator[] inserts a null for a
// missing key, which would make PathRequest::parseJson's isMember() checks
// see an absent field as present and return Malformed instead of Missing.
// Reading for telemetry must not alter what the request looks like.
auto const& params = std::as_const(context.params);
if (auto const& src = params[jss::source_account]; src.isString())
span.setAttribute(pathfind_span::attr::sourceAccount, redactAccount(src.asString()));
if (auto const& dst = params[jss::destination_account]; dst.isString())
span.setAttribute(pathfind_span::attr::destAccount, redactAccount(dst.asString()));
// Guarded on the span being live because setAttribute's arguments are
// evaluated whatever the build, and neither is free: asString() copies the
// address out of the JSON and redactAccount() takes a SHA-512Half over it.
// That is two copies and two hashes on every path_find call. The
// compiled-out guard's operator bool() is a literal false, so the block
// disappears entirely in that build; with telemetry compiled in it is
// skipped when telemetry is disabled at runtime or the category is off.
if (span)
{
// Addresses are hashed before emission for privacy. Read through a
// const reference: the non-const json::Value::operator[] inserts a null
// for a missing key, which would make PathRequest::parseJson's
// isMember() checks see an absent field as present and return Malformed
// instead of Missing. Reading for telemetry must not alter what the
// request looks like.
auto const& params = std::as_const(context.params);
if (auto const& src = params[jss::source_account]; src.isString())
span.setAttribute(pathfind_span::attr::sourceAccount, redactAccount(src.asString()));
if (auto const& dst = params[jss::destination_account]; dst.isString())
span.setAttribute(pathfind_span::attr::destAccount, redactAccount(dst.asString()));
}
if (context.app.config().pathSearchMax == 0)
return rpcError(RpcNotSupported);

View File

@@ -34,16 +34,27 @@ doRipplePathFind(rpc::JsonContext& context)
// span's log lines stay trace-correlated.
auto span = ScopedSpanGuard(
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::request);
// Addresses are hashed before emission for privacy. Read through a const
// reference: the non-const json::Value::operator[] inserts a null for a
// missing key, which would make PathRequest::parseJson's isMember() checks
// see an absent field as present and return Malformed instead of Missing.
// Reading for telemetry must not alter what the request looks like.
auto const& params = std::as_const(context.params);
if (auto const& src = params[jss::source_account]; src.isString())
span.setAttribute(pathfind_span::attr::sourceAccount, redactAccount(src.asString()));
if (auto const& dst = params[jss::destination_account]; dst.isString())
span.setAttribute(pathfind_span::attr::destAccount, redactAccount(dst.asString()));
// Guarded on the span being live because setAttribute's arguments are
// evaluated whatever the build, and neither is free: asString() copies the
// address out of the JSON and redactAccount() takes a SHA-512Half over it.
// That is two copies and two hashes on every ripple_path_find call. The
// compiled-out guard's operator bool() is a literal false, so the block
// disappears entirely in that build; with telemetry compiled in it is
// skipped when telemetry is disabled at runtime or the category is off.
if (span)
{
// Addresses are hashed before emission for privacy. Read through a
// const reference: the non-const json::Value::operator[] inserts a null
// for a missing key, which would make PathRequest::parseJson's
// isMember() checks see an absent field as present and return Malformed
// instead of Missing. Reading for telemetry must not alter what the
// request looks like.
auto const& params = std::as_const(context.params);
if (auto const& src = params[jss::source_account]; src.isString())
span.setAttribute(pathfind_span::attr::sourceAccount, redactAccount(src.asString()));
if (auto const& dst = params[jss::destination_account]; dst.isString())
span.setAttribute(pathfind_span::attr::destAccount, redactAccount(dst.asString()));
}
if (context.app.config().pathSearchMax == 0)
return rpcError(RpcNotSupported);