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

Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com>
This commit is contained in:
Pratik Mankawde
2026-07-06 20:41:14 +01:00
23 changed files with 265 additions and 768 deletions

View File

@@ -0,0 +1,32 @@
#include <xrpl/telemetry/Redaction.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/digest.h>
#include <algorithm>
#include <cctype>
#include <string>
#include <string_view>
namespace xrpl::telemetry {
std::string
redactAccount(std::string_view addr)
{
// Empty in, empty out: nothing to hash and no token to emit.
if (addr.empty())
return {};
// sha512Half yields a uint256; to_string renders it as uppercase hex.
// Keep the first 16 chars (64 bits) — enough to correlate spans while
// staying non-reversible — and lowercase them for a stable token.
auto const digest = sha512Half(Slice(addr.data(), addr.size()));
std::string token = to_string(digest).substr(0, 16);
std::transform(token.begin(), token.end(), token.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return token;
}
} // namespace xrpl::telemetry

View File

@@ -235,6 +235,7 @@ SpanGuard::linkedSpan(std::string_view name) const
return SpanGuard(
std::make_unique<Impl>(tracer->StartSpan(
std::string(name), {}, {{spanCtx, {{kLinkTypeKey, kLinkTypeFollowsFrom}}}}, opts)));
// LCOV_EXCL_STOP
}
SpanGuard

View File

@@ -0,0 +1,44 @@
#include <xrpl/telemetry/Redaction.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <cctype>
#include <string>
using namespace xrpl;
using namespace xrpl::telemetry;
// Empty input must produce empty output (edge / negative path).
TEST(Redaction, empty_input_returns_empty)
{
EXPECT_EQ(redactAccount(""), "");
}
// Success path: assert the EXACT expected token. The value is the first
// 16 chars of sha512Half(addr) rendered as lowercase hex, computed once
// and hard-coded here so any change to the hashing recipe is caught.
TEST(Redaction, known_account_exact_hash)
{
EXPECT_EQ(redactAccount("rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"), "a513895f49311f54");
}
// Structural invariants: length, lowercase-hex alphabet, and stability.
TEST(Redaction, token_is_16_lowercase_hex_and_stable)
{
auto const token = redactAccount("rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh");
EXPECT_EQ(token.size(), 16u);
EXPECT_TRUE(std::all_of(token.begin(), token.end(), [](unsigned char c) {
return std::isdigit(c) || (c >= 'a' && c <= 'f');
}));
// Deterministic: hashing the same input twice yields the same token.
EXPECT_EQ(token, redactAccount("rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"));
}
// Distinct inputs must produce distinct tokens (no accidental constant).
TEST(Redaction, distinct_inputs_distinct_tokens)
{
EXPECT_NE(
redactAccount("rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"),
redactAccount("rDifferentAccount1234567890abcdefgh"));
}

View File

@@ -84,8 +84,6 @@ inline constexpr auto numPaths = makeStr("pathfind_num_paths");
inline constexpr auto numRequests = makeStr("pathfind_num_requests");
/// "pathfind_ledger_index" — pathfind target ledger index.
inline constexpr auto ledgerIndex = makeStr("pathfind_ledger_index");
/// "pathfind_dest_amount" — requested destination amount as string.
inline constexpr auto destAmount = makeStr("pathfind_dest_amount");
/// "pathfind_dest_currency" — destination currency code.
inline constexpr auto destCurrency = makeStr("pathfind_dest_currency");
/// "pathfind_num_source_assets" — candidate source assets count.

View File

@@ -742,7 +742,6 @@ PathRequest::doUpdate(
auto span = SpanGuard::span(
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::compute);
span.setAttribute(pathfind_span::attr::fast, fast);
span.setAttribute(pathfind_span::attr::destAmount, saDstAmount_.getFullText().c_str());
span.setAttribute(pathfind_span::attr::destCurrency, to_string(saDstAmount_.asset()).c_str());
JLOG(journal_.debug()) << iIdentifier_ << " update " << (fast ? "fast" : "normal");

View File

@@ -73,15 +73,16 @@ PathRequestManager::updateAll(std::shared_ptr<ReadView const> const& inLedger)
cache = getAssetCache(inLedger, true);
}
// updateAll runs on every ledger close; skip span emission entirely when
// there are no active path subscriptions to avoid a steady stream of empty
// spans at mainnet close cadence.
if (requests.empty())
return;
using namespace telemetry;
auto span = SpanGuard::span(
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::updateAll);
// 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. A null guard is used in that case; all other work
// still runs unchanged (notably the isNewPathRequest() flag reset below),
// so behaviour matches the pre-span code path.
auto span = requests.empty()
? SpanGuard{}
: SpanGuard::span(
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::updateAll);
span.setAttribute(pathfind_span::attr::ledgerIndex, static_cast<int64_t>(inLedger->seq()));
span.setAttribute(pathfind_span::attr::numRequests, static_cast<int64_t>(requests.size()));

View File

@@ -221,6 +221,31 @@ callMethod(JsonContext& context, Method method, std::string const& name, Object&
}
}
// 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
// both fields or names an unregistered command. The raw request value is
// deliberately NOT used: the command attribute is promoted to a Prometheus
// label by the spanmetrics connector, so an attacker-controlled string would
// let arbitrary request input drive unbounded span-name / label cardinality.
// Resolving against the registry keeps per-command error attribution for real
// commands (e.g. a submit rejected with rpcTOO_BUSY stays rpc.command.submit)
// while collapsing garbage input to a single series.
std::string_view
resolveCommandSpanName(JsonContext const& context)
{
if (!context.params.isMember(jss::command) && !context.params.isMember(jss::method))
return rpc_span::val::unknownCommand;
std::string const cmd = context.params.isMember(jss::command)
? context.params[jss::command].asString()
: context.params[jss::method].asString();
auto const* handler = getHandler(context.apiVersion, context.app.config().betaRpcApi, cmd);
return (handler != nullptr) ? std::string_view{handler->name}
: std::string_view{rpc_span::val::unknownCommand};
}
} // namespace
Status
@@ -229,25 +254,12 @@ doCommand(RPC::JsonContext& context, json::Value& result)
Handler const* handler = nullptr;
if (auto error = fillHandler(context, handler))
{
std::string cmdName;
if (context.params.isMember(jss::command))
{
cmdName = context.params[jss::command].asString();
}
else if (context.params.isMember(jss::method))
{
cmdName = context.params[jss::method].asString();
}
else
{
cmdName = "unknown"; // LCOV_EXCL_LINE
}
// Use the resolved command name as the span suffix so dashboards
// can break out per-command error rates (e.g. rpc.command.submit
// for a submit that hit rpcTOO_BUSY). Falling back to a single
// "unknown" name only when the request truly omits both fields.
// 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.
auto const cmdName = resolveCommandSpanName(context);
auto span = SpanGuard::span(TraceCategory::Rpc, rpc_span::prefix::command, cmdName);
span.setAttribute(rpc_span::attr::command, cmdName.c_str());
span.setAttribute(rpc_span::attr::command, cmdName);
span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error);
span.setError(getErrorInfo(error).token.cStr());

View File

@@ -10,6 +10,7 @@
#include <xrpl/protocol/jss.h>
#include <xrpl/resource/Fees.h>
#include <xrpl/server/InfoSub.h>
#include <xrpl/telemetry/Redaction.h>
#include <xrpl/telemetry/SpanGuard.h>
namespace xrpl {
@@ -20,10 +21,11 @@ doPathFind(RPC::JsonContext& context)
using namespace telemetry;
auto span = SpanGuard::span(
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::request);
// Addresses are hashed before emission for privacy.
if (auto const& src = context.params[jss::source_account]; src.isString())
span.setAttribute(pathfind_span::attr::sourceAccount, src.asString());
span.setAttribute(pathfind_span::attr::sourceAccount, redactAccount(src.asString()));
if (auto const& dst = context.params[jss::destination_account]; dst.isString())
span.setAttribute(pathfind_span::attr::destAccount, dst.asString());
span.setAttribute(pathfind_span::attr::destAccount, redactAccount(dst.asString()));
if (context.app.config().pathSearchMax == 0)
return rpcError(RpcNotSupported);

View File

@@ -14,6 +14,7 @@
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/resource/Fees.h>
#include <xrpl/telemetry/Redaction.h>
#include <xrpl/telemetry/SpanGuard.h>
#include <memory>
@@ -28,10 +29,11 @@ doRipplePathFind(RPC::JsonContext& context)
using namespace telemetry;
auto span = SpanGuard::span(
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::request);
// Addresses are hashed before emission for privacy.
if (auto const& src = context.params[jss::source_account]; src.isString())
span.setAttribute(pathfind_span::attr::sourceAccount, src.asString());
span.setAttribute(pathfind_span::attr::sourceAccount, redactAccount(src.asString()));
if (auto const& dst = context.params[jss::destination_account]; dst.isString())
span.setAttribute(pathfind_span::attr::destAccount, dst.asString());
span.setAttribute(pathfind_span::attr::destAccount, redactAccount(dst.asString()));
if (context.app.config().pathSearchMax == 0)
return rpcError(RpcNotSupported);