mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-31 11:00:23 +00:00
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:
32
src/libxrpl/telemetry/Redaction.cpp
Normal file
32
src/libxrpl/telemetry/Redaction.cpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
44
src/tests/libxrpl/telemetry/Redaction.cpp
Normal file
44
src/tests/libxrpl/telemetry/Redaction.cpp
Normal 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"));
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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()));
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user