From 742aa0878bbccdce385db4c8b6b77f2da19fad4a Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 10 Jun 2026 05:16:53 -0400 Subject: [PATCH 01/30] test: Do not create data directory for memory databases (#7323) Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com> --- src/xrpld/app/misc/SHAMapStoreImp.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index 7259c233e4..33bbf1c613 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -387,8 +387,12 @@ void SHAMapStoreImp::dbPaths() { Section const section{app_.config().section(Sections::kNodeDatabase)}; - boost::filesystem::path dbPath = get(section, Keys::kPath); + // Skip creating the directory when an in-memory database is used. + if (boost::iequals(get(section, Keys::kType), "memory")) + return; + + boost::filesystem::path dbPath = get(section, Keys::kPath); if (boost::filesystem::exists(dbPath)) { if (!boost::filesystem::is_directory(dbPath)) From e11bf35691ca31aa1a8a8e52631d26374339fe5e Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Wed, 10 Jun 2026 10:42:52 +0100 Subject: [PATCH 02/30] fix: Update BasicConfig.h include path after upstream reorg BasicConfig.h moved from xrpl/basics/ to xrpl/config/ on develop (PR #7095 / reorg). Phase 1b's telemetry headers still referenced the old path, breaking a fresh compile with "BasicConfig.h: No such file or directory". Point both telemetry includes at the new location. Co-Authored-By: Claude Opus 4.8 --- include/xrpl/telemetry/Telemetry.h | 2 +- src/libxrpl/telemetry/TelemetryConfig.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/xrpl/telemetry/Telemetry.h b/include/xrpl/telemetry/Telemetry.h index fe7ef33bf6..f1144da676 100644 --- a/include/xrpl/telemetry/Telemetry.h +++ b/include/xrpl/telemetry/Telemetry.h @@ -70,8 +70,8 @@ The OTel SDK's TracerProvider and Tracer are internally thread-safe. */ -#include #include +#include #include #include diff --git a/src/libxrpl/telemetry/TelemetryConfig.cpp b/src/libxrpl/telemetry/TelemetryConfig.cpp index 0863623f66..98c6dbc8c1 100644 --- a/src/libxrpl/telemetry/TelemetryConfig.cpp +++ b/src/libxrpl/telemetry/TelemetryConfig.cpp @@ -7,7 +7,7 @@ See cfg/xrpld-example.cfg for the full list of available options. */ -#include +#include #include #include From 38fbab1d186d265d61cf29e478400e3300e1145e Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Wed, 10 Jun 2026 10:50:54 +0100 Subject: [PATCH 03/30] levelization Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> --- .github/scripts/levelization/results/ordering.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 644210f70e..c7b11a8855 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -47,6 +47,7 @@ libxrpl.shamap > xrpl.nodestore libxrpl.shamap > xrpl.protocol libxrpl.shamap > xrpl.shamap libxrpl.telemetry > xrpl.basics +libxrpl.telemetry > xrpl.config libxrpl.telemetry > xrpl.telemetry libxrpl.tx > xrpl.basics libxrpl.tx > xrpl.conditions @@ -250,7 +251,7 @@ xrpl.server > xrpl.shamap xrpl.shamap > xrpl.basics xrpl.shamap > xrpl.nodestore xrpl.shamap > xrpl.protocol -xrpl.telemetry > xrpl.basics +xrpl.telemetry > xrpl.config xrpl.telemetry > xrpld.consensus xrpl.telemetry > xrpld.rpc xrpl.tx > xrpl.basics From 8a4bf2dee60e4082b584c1702eabf93225f8da40 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:16:03 +0100 Subject: [PATCH 04/30] refactor: Retire fixUniversalNumber amendment (#5962) Signed-off-by: Pratik Mankawde Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) --- include/xrpl/protocol/AMMCore.h | 2 +- include/xrpl/protocol/IOUAmount.h | 33 ---- include/xrpl/protocol/Rules.h | 2 - include/xrpl/protocol/detail/features.macro | 2 +- src/libxrpl/protocol/AMMCore.cpp | 2 +- src/libxrpl/protocol/IOUAmount.cpp | 104 +---------- src/libxrpl/protocol/Rules.cpp | 4 - src/libxrpl/protocol/STAmount.cpp | 188 +++----------------- src/libxrpl/tx/Transactor.cpp | 3 - src/libxrpl/tx/apply.cpp | 2 - src/libxrpl/tx/applySteps.cpp | 4 +- src/test/app/AMM_test.cpp | 7 +- src/test/app/NFToken_test.cpp | 14 +- src/test/app/OfferMPT_test.cpp | 1 - src/test/app/Offer_test.cpp | 60 +++---- src/test/basics/Number_test.cpp | 1 - src/xrpld/app/misc/detail/TxQ.cpp | 5 - 17 files changed, 55 insertions(+), 379 deletions(-) diff --git a/include/xrpl/protocol/AMMCore.h b/include/xrpl/protocol/AMMCore.h index ced84c4c87..a83c8bfa84 100644 --- a/include/xrpl/protocol/AMMCore.h +++ b/include/xrpl/protocol/AMMCore.h @@ -65,7 +65,7 @@ invalidAMMAssetPair( std::optional ammAuctionTimeSlot(std::uint64_t current, STObject const& auctionSlot); -/** Return true if required AMM amendments are enabled +/** Return true if required AMM amendment is enabled */ bool ammEnabled(Rules const&); diff --git a/include/xrpl/protocol/IOUAmount.h b/include/xrpl/protocol/IOUAmount.h index 9e0fbe38eb..b057f1c245 100644 --- a/include/xrpl/protocol/IOUAmount.h +++ b/include/xrpl/protocol/IOUAmount.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include @@ -179,36 +178,4 @@ to_string(IOUAmount const& amount); IOUAmount mulRatio(IOUAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundUp); -// Since many uses of the number class do not have access to a ledger, -// getSTNumberSwitchover needs to be globally accessible. - -bool -getSTNumberSwitchover(); - -void -setSTNumberSwitchover(bool v); - -/** RAII class to set and restore the Number switchover. - */ - -class NumberSO -{ - bool saved_; - -public: - ~NumberSO() - { - setSTNumberSwitchover(saved_); - } - - NumberSO(NumberSO const&) = delete; - NumberSO& - operator=(NumberSO const&) = delete; - - explicit NumberSO(bool v) : saved_(getSTNumberSwitchover()) - { - setSTNumberSwitchover(v); - } -}; - } // namespace xrpl diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h index 9c17ff2391..47b20756db 100644 --- a/include/xrpl/protocol/Rules.h +++ b/include/xrpl/protocol/Rules.h @@ -122,7 +122,6 @@ private: std::optional saved_; }; -class NumberSO; class NumberMantissaScaleGuard; bool @@ -131,7 +130,6 @@ useRulesGuards(Rules const& rules); void createGuards( Rules const& rules, - std::optional& stNumberSO, std::optional& rulesGuard, std::optional& mantissaScaleGuard); diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index c99c1e5ce8..2b2f24ba53 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -64,7 +64,6 @@ XRPL_FIX (DisallowIncomingV1, Supported::Yes, VoteBehavior::DefaultNo XRPL_FEATURE(XChainBridge, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(AMM, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Clawback, Supported::Yes, VoteBehavior::DefaultNo) -XRPL_FIX (UniversalNumber, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(XRPFees, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (RemoveNFTokenAutoTrustLine, Supported::Yes, VoteBehavior::DefaultYes) @@ -112,6 +111,7 @@ XRPL_RETIRE_FIX(RmSmallIncreasedQOffers) XRPL_RETIRE_FIX(STAmountCanonicalize) XRPL_RETIRE_FIX(TakerDryOfferRemoval) XRPL_RETIRE_FIX(TrustLinesToSelf) +XRPL_RETIRE_FIX(UniversalNumber) XRPL_RETIRE_FEATURE(Checks) XRPL_RETIRE_FEATURE(CheckCashMakesTrustLine) diff --git a/src/libxrpl/protocol/AMMCore.cpp b/src/libxrpl/protocol/AMMCore.cpp index eccb581c6d..e58ab29257 100644 --- a/src/libxrpl/protocol/AMMCore.cpp +++ b/src/libxrpl/protocol/AMMCore.cpp @@ -127,7 +127,7 @@ ammAuctionTimeSlot(std::uint64_t current, STObject const& auctionSlot) bool ammEnabled(Rules const& rules) { - return rules.enabled(featureAMM) && rules.enabled(fixUniversalNumber); + return rules.enabled(featureAMM); } } // namespace xrpl diff --git a/src/libxrpl/protocol/IOUAmount.cpp b/src/libxrpl/protocol/IOUAmount.cpp index d214995809..acbf6724e1 100644 --- a/src/libxrpl/protocol/IOUAmount.cpp +++ b/src/libxrpl/protocol/IOUAmount.cpp @@ -1,6 +1,5 @@ #include -#include #include #include #include @@ -17,29 +16,6 @@ namespace xrpl { -namespace { - -// Use a static inside a function to help prevent order-of-initialization issues -LocalValue& -getStaticSTNumberSwitchover() -{ - static LocalValue kR{true}; - return kR; -} -} // namespace - -bool -getSTNumberSwitchover() -{ - return *getStaticSTNumberSwitchover(); -} - -void -setSTNumberSwitchover(bool v) -{ - *getStaticSTNumberSwitchover() = v; -} - /* The range for the mantissa when normalized */ // log(2^63,10) ~ 18.96 // @@ -75,56 +51,20 @@ IOUAmount::normalize() return; } - if (getSTNumberSwitchover()) - { - Number const v{mantissa_, exponent_}; - *this = fromNumber(v); - if (exponent_ > kMaxExponent) - Throw("value overflow"); - if (exponent_ < kMinExponent) - *this = beast::kZero; - return; - } - - bool const negative = (mantissa_ < 0); - - if (negative) - mantissa_ = -mantissa_; - - while ((mantissa_ < kMinMantissa) && (exponent_ > kMinExponent)) - { - mantissa_ *= 10; - --exponent_; - } - - while (mantissa_ > kMaxMantissa) - { - if (exponent_ >= kMaxExponent) - Throw("IOUAmount::normalize"); - - mantissa_ /= 10; - ++exponent_; - } - - if ((exponent_ < kMinExponent) || (mantissa_ < kMinMantissa)) - { - *this = beast::kZero; - return; - } - - if (exponent_ > kMaxExponent) - Throw("value overflow"); - - if (negative) - mantissa_ = -mantissa_; + Number const v{mantissa_, exponent_}; + *this = IOUAmount(v); } IOUAmount::IOUAmount(Number const& other) : IOUAmount(fromNumber(other)) { if (exponent_ > kMaxExponent) + { Throw("value overflow"); + } if (exponent_ < kMinExponent) + { *this = beast::kZero; + } } IOUAmount& @@ -139,37 +79,7 @@ IOUAmount::operator+=(IOUAmount const& other) return *this; } - if (getSTNumberSwitchover()) - { - *this = IOUAmount{Number{*this} + Number{other}}; - return *this; - } - auto m = other.mantissa_; - auto e = other.exponent_; - - while (exponent_ < e) - { - mantissa_ /= 10; - ++exponent_; - } - - while (e < exponent_) - { - m /= 10; - ++e; - } - - // This addition cannot overflow an std::int64_t but we may throw from - // normalize if the result isn't representable. - mantissa_ += m; - - if (mantissa_ >= -10 && mantissa_ <= 10) - { - *this = beast::kZero; - return *this; - } - - normalize(); + *this = IOUAmount{Number{*this} + Number{other}}; return *this; } diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 08a95145eb..e0968ea868 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include @@ -83,15 +82,12 @@ useRulesGuards(Rules const& rules) void createGuards( Rules const& rules, - std::optional& stNumberSO, std::optional& rulesGuard, std::optional& mantissaScaleGuard) { if (useRulesGuards(rules)) { // raii classes for the current ledger rules. - // fixUniversalNumber predates the rulesGuard and should be replaced. - stNumberSO.emplace(rules.enabled(fixUniversalNumber)); rulesGuard.emplace(rules); } else diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 1ba9cd042f..449efc9543 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -388,47 +388,9 @@ operator+(STAmount const& v1, STAmount const& v2) if (v1.holds()) return {v1.asset_, v1.mpt().value() + v2.mpt().value()}; - if (getSTNumberSwitchover()) - { - auto x = v1; - x = v1.iou() + v2.iou(); - return x; - } - - int ov1 = v1.exponent(), ov2 = v2.exponent(); - std::int64_t vv1 = static_cast(v1.mantissa()); - std::int64_t vv2 = static_cast(v2.mantissa()); - - if (v1.negative()) - vv1 = -vv1; - - if (v2.negative()) - vv2 = -vv2; - - while (ov1 < ov2) - { - vv1 /= 10; - ++ov1; - } - - while (ov2 < ov1) - { - vv2 /= 10; - ++ov2; - } - - // This addition cannot overflow an std::int64_t. It can overflow an - // STAmount and the constructor will throw. - - std::int64_t const fv = vv1 + vv2; - - if ((fv >= -10) && (fv <= 10)) - return {v1.getFName(), v1.asset()}; - - if (fv >= 0) - return STAmount{v1.getFName(), v1.asset(), static_cast(fv), ov1, false}; - - return STAmount{v1.getFName(), v1.asset(), static_cast(-fv), ov1, true}; + auto x = v1; + x = v1.iou() + v2.iou(); + return x; } STAmount @@ -877,53 +839,25 @@ STAmount::canonicalize() if (asset_.holds() && offset_ > 18) Throw("MPT amount out of range"); - if (getSTNumberSwitchover()) + Number const num(isNegative_, value_, offset_, Number::Unchecked{}); + auto set = [&](auto const& val) { + auto const value = val.value(); + isNegative_ = value < 0; + value_ = isNegative_ ? -value : value; + }; + if (native()) { - Number const num(isNegative_, value_, offset_, Number::Unchecked{}); - auto set = [&](auto const& val) { - auto const value = val.value(); - isNegative_ = value < 0; - value_ = isNegative_ ? -value : value; - }; - if (native()) - { - set(XRPAmount{num}); - } - else if (asset_.holds()) - { - set(MPTAmount{num}); - } - else - { - Throw("Unknown integral asset type"); - } - offset_ = 0; + set(XRPAmount{num}); + } + else if (asset_.holds()) + { + set(MPTAmount{num}); } else { - while (offset_ < 0) - { - value_ /= 10; - ++offset_; - } - - while (offset_ > 0) - { - // N.B. do not move the overflow check to after the - // multiplication - if (native() && value_ > kMaxNativeN) - { - Throw("Native currency amount out of range"); - } - else if (!native() && value_ > kMaxMpTokenAmount) - { - Throw("MPT amount out of range"); - } - - value_ *= 10; - --offset_; - } + Throw("Unknown integral asset type"); // LCOV_EXCL_LINE } + offset_ = 0; if (native() && value_ > kMaxNativeN) { @@ -937,53 +871,7 @@ STAmount::canonicalize() return; } - if (getSTNumberSwitchover()) - { - *this = iou(); - return; - } - - if (value_ == 0) - { - offset_ = -100; - isNegative_ = false; - return; - } - - while ((value_ < kMinValue) && (offset_ > kMinOffset)) - { - value_ *= 10; - --offset_; - } - - while (value_ > kMaxValue) - { - if (offset_ >= kMaxOffset) - Throw("value overflow"); - - value_ /= 10; - ++offset_; - } - - if ((offset_ < kMinOffset) || (value_ < kMinValue)) - { - value_ = 0; - isNegative_ = false; - offset_ = -100; - return; - } - - if (offset_ > kMaxOffset) - Throw("value overflow"); - - XRPL_ASSERT( - (value_ == 0) || ((value_ >= kMinValue) && (value_ <= kMaxValue)), - "xrpl::STAmount::canonicalize : value inside range"); - XRPL_ASSERT( - (value_ == 0) || ((offset_ >= kMinOffset) && (offset_ <= kMaxOffset)), - "xrpl::STAmount::canonicalize : offset inside range"); - XRPL_ASSERT( - (value_ != 0) || (offset_ != -100), "xrpl::STAmount::canonicalize : value or offset set"); + *this = iou(); } void @@ -1395,44 +1283,8 @@ multiply(STAmount const& v1, STAmount const& v2, Asset const& asset) return STAmount(asset, minV * maxV); } - if (getSTNumberSwitchover()) - { - auto const r = Number{v1} * Number{v2}; - return STAmount{asset, r}; - } - - std::uint64_t value1 = v1.mantissa(); - std::uint64_t value2 = v2.mantissa(); - int offset1 = v1.exponent(); - int offset2 = v2.exponent(); - - if (v1.integral()) - { - while (value1 < STAmount::kMinValue) - { - value1 *= 10; - --offset1; - } - } - - if (v2.integral()) - { - while (value2 < STAmount::kMinValue) - { - value2 *= 10; - --offset2; - } - } - - // We multiply the two mantissas (each is between 10^15 - // and 10^16), so their product is in the 10^30 to 10^32 - // range. Dividing their product by 10^14 maintains the - // precision, by scaling the result to 10^16 to 10^18. - return STAmount( - asset, - muldiv(value1, value2, kTenTO14) + 7, - offset1 + offset2 + 14, - v1.negative() != v2.negative()); + auto const r = Number{v1} * Number{v2}; + return STAmount{asset, r}; } // This is the legacy version of canonicalizeRound. It's been in use diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 51541cc2e3..68d3f36916 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -1199,8 +1198,6 @@ Transactor::operator()() // with_txn_type(). // // raii classes for the current ledger rules. - // fixUniversalNumber predate the rulesGuard and should be replaced. - NumberSO const stNumberSO{view().rules().enabled(fixUniversalNumber)}; CurrentTransactionRulesGuard const currentTransactionRulesGuard(view().rules()); #ifdef DEBUG diff --git a/src/libxrpl/tx/apply.cpp b/src/libxrpl/tx/apply.cpp index 0fc0275eb0..b70cb0d345 100644 --- a/src/libxrpl/tx/apply.cpp +++ b/src/libxrpl/tx/apply.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -133,7 +132,6 @@ template ApplyResult apply(ServiceRegistry& registry, OpenView& view, PreflightChecks&& preflightChecks) { - NumberSO const stNumberSO{view.rules().enabled(fixUniversalNumber)}; return doApply(preclaim(preflightChecks(), registry, view), registry, view); } diff --git a/src/libxrpl/tx/applySteps.cpp b/src/libxrpl/tx/applySteps.cpp index 217fdd717f..336bb2004b 100644 --- a/src/libxrpl/tx/applySteps.cpp +++ b/src/libxrpl/tx/applySteps.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -70,10 +69,9 @@ withTxnType(Rules const& rules, TxType txnType, F&& f) // // See also Transactor::operator(). // - std::optional stNumberSO; std::optional rulesGuard; std::optional mantissaScaleGuard; - createGuards(rules, stNumberSO, rulesGuard, mantissaScaleGuard); + createGuards(rules, rulesGuard, mantissaScaleGuard); switch (txnType) { diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 64972c24ab..e3a1cc935f 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -4333,15 +4333,10 @@ private: testAmendment() { testcase("Amendment"); - FeatureBitset const all{testableAmendments()}; - FeatureBitset const noAMM{all - featureAMM}; - FeatureBitset const noNumber{all - fixUniversalNumber}; - FeatureBitset const noAMMAndNumber{all - featureAMM - fixUniversalNumber}; using namespace jtx; + Env env{*this, testableAmendments() - featureAMM}; - for (auto const& feature : {noAMM, noNumber, noAMMAndNumber}) { - Env env{*this, feature}; fund(env, gw_, {alice_}, {USD(1'000)}, Fund::All); AMM amm(env, alice_, XRP(1'000), USD(1'000), Ter(temDISABLED)); diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 269bc72c53..ba8f09c449 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -2236,17 +2236,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // See the impact of rounding when the nft is sold for small amounts // of drops. - for (auto numberSwitchOver : {true}) { - if (numberSwitchOver) - { - env.enableFeature(fixUniversalNumber); - } - else - { - env.disableFeature(fixUniversalNumber); - } - // An nft with a transfer fee of 1 basis point. uint256 const nftID = token::getNextID(env, alice, 0u, tfTransferable, 1); env(token::mint(alice), Txflags(tfTransferable), token::XferFee(1)); @@ -2268,7 +2258,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // minter sells to carol. The payment is just small enough that // alice does not get any transfer fee. - auto pmt = numberSwitchOver ? drops(50000) : drops(99999); + auto pmt = drops(50000); STAmount carolBalance = env.balance(carol); uint256 const minterSellOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key; env(token::createOffer(minter, nftID, pmt), Txflags(tfSellNFToken)); @@ -2285,7 +2275,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // transfer that enables a transfer fee of 1 basis point. STAmount beckyBalance = env.balance(becky); uint256 const beckyBuyOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key; - pmt = numberSwitchOver ? drops(50001) : drops(100000); + pmt = drops(50001); env(token::createOffer(becky, nftID, pmt), token::Owner(carol)); env.close(); env(token::acceptBuyOffer(carol, beckyBuyOfferIndex)); diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp index e0f2f4eab0..ed0b2ffbe3 100644 --- a/src/test/app/OfferMPT_test.cpp +++ b/src/test/app/OfferMPT_test.cpp @@ -1827,7 +1827,6 @@ public: using namespace jtx; Env env{*this, features}; - env.enableFeature(fixUniversalNumber); auto const gw = Account{"gateway"}; auto const alice = Account{"alice"}; diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 83c58884e0..7382f4f090 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -1973,54 +1973,36 @@ public: using namespace jtx; - for (auto numberSwitchOver : {false, true}) - { - Env env{*this, features}; - if (numberSwitchOver) - { - env.enableFeature(fixUniversalNumber); - } - else - { - env.disableFeature(fixUniversalNumber); - } + Env env{*this, features}; - auto const gw = Account{"gateway"}; - auto const alice = Account{"alice"}; - auto const bob = Account{"bob"}; - auto const usd = gw["USD"]; + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const usd = gw["USD"]; - env.fund(XRP(10000), gw, alice, bob); - env.close(); + env.fund(XRP(10000), gw, alice, bob); + env.close(); - env(rate(gw, 1.005)); + env(rate(gw, 1.005)); - env(trust(alice, usd(1000))); - env(trust(bob, usd(1000))); - env(trust(gw, alice["USD"](50))); + env(trust(alice, usd(1000))); + env(trust(bob, usd(1000))); + env(trust(gw, alice["USD"](50))); - env(pay(gw, bob, bob["USD"](1))); - env(pay(alice, gw, usd(50))); + env(pay(gw, bob, bob["USD"](1))); + env(pay(alice, gw, usd(50))); - env(trust(gw, alice["USD"](0))); + env(trust(gw, alice["USD"](0))); - env(offer(alice, usd(50), XRP(150000))); - env(offer(bob, XRP(100), usd(0.1))); + env(offer(alice, usd(50), XRP(150000))); + env(offer(bob, XRP(100), usd(0.1))); - auto jrr = ledgerEntryState(env, alice, gw, "USD"); - BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "49.96666666666667"); + auto jrr = ledgerEntryState(env, alice, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "49.96666666666667"); - jrr = ledgerEntryState(env, bob, gw, "USD"); - json::Value const bobUSD = jrr[jss::node][sfBalance.fieldName][jss::value]; - if (!numberSwitchOver) - { - BEAST_EXPECT(bobUSD == "-0.966500000033334"); - } - else - { - BEAST_EXPECT(bobUSD == "-0.9665000000333333"); - } - } + jrr = ledgerEntryState(env, bob, gw, "USD"); + json::Value const bobUSD = jrr[jss::node][sfBalance.fieldName][jss::value]; + BEAST_EXPECT(bobUSD == "-0.9665000000333333"); } void diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index f4bd1c9d66..2d2745de14 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1514,7 +1514,6 @@ public: void testToStAmount() { - NumberSO const stNumberSO{true}; Issue const issue; Number const n{7'518'783'80596, -5}; SaveNumberRoundMode const save{Number::setround(Number::RoundingMode::ToNearest)}; diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index f98580ede4..0326828a70 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -16,8 +16,6 @@ #include #include #include -#include -#include #include #include #include @@ -306,7 +304,6 @@ TxQ::MaybeTx::apply(Application& app, OpenView& view, beast::Journal j) { // If the rules or flags change, preflight again XRPL_ASSERT(pfResult, "xrpl::TxQ::MaybeTx::apply : preflight result is set"); - NumberSO const stNumberSO{view.rules().enabled(fixUniversalNumber)}; // NOLINTBEGIN(bugprone-unchecked-optional-access) assert above if (pfResult->rules != view.rules() || pfResult->flags != flags) @@ -731,8 +728,6 @@ TxQ::apply( ApplyFlags flags, beast::Journal j) { - NumberSO const stNumberSO{view.rules().enabled(fixUniversalNumber)}; - // See if the transaction is valid, properly formed, // etc. before doing potentially expensive queue // replace and multi-transaction operations. From 97ca7d57bcedab341e7887cd7950299d2bde9598 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:44:57 +0200 Subject: [PATCH 05/30] perf: Dispatch "hasInvalidAmount()" on type tag instead of dynamic_cast (#7402) --- src/libxrpl/protocol/STAmount.cpp | 32 ++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 449efc9543..748d00f25a 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -1138,16 +1138,34 @@ hasInvalidAmount(STBase const& field, int depth, beast::Journal j) return true; } - if (auto const amount = dynamic_cast(&field)) - return !isLegalMPT(*amount) || !isLegalNet(*amount); + // Dispatch on the serialized type tag rather than RTTI: this is on the invariant-checking path + // and a dynamic_cast chain over every field of every modified entry is measurably expensive. + // The object-like tags below all denote STObject subclasses (STLedgerEntry, STTx), so the + // downcast is sound; nested fields are only ever plain STI_OBJECT / STI_ARRAY containers. + // safeDowncast keeps a dynamic_cast validity assert in debug builds while compiling to + // static_cast in release. + switch (field.getSType()) + { + case STI_AMOUNT: { + auto const& amount = safeDowncast(field); + return !isLegalMPT(amount) || !isLegalNet(amount); + } - if (auto const object = dynamic_cast(&field)) - return hasInvalidAmount(*object, depth + 1, j); + case STI_OBJECT: + case STI_LEDGERENTRY: + case STI_TRANSACTION: + return hasInvalidAmount(safeDowncast(field), depth + 1, j); - if (auto const array = dynamic_cast(&field)) - return hasInvalidAmount(*array, depth + 1, j); + case STI_ARRAY: + return hasInvalidAmount(safeDowncast(field), depth + 1, j); - return false; + default: { + XRPL_ASSERT( + dynamic_cast(&field) == nullptr, + "xrpl::hasInvalidAmount : unhandled STObject type"); + return false; + } + } } bool From 83cc5df72e9fa2f4d9d91a674908afcd6a50302a Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:05:53 +0200 Subject: [PATCH 06/30] fix: Disable transaction invariants (#7409) --- src/libxrpl/tx/Transactor.cpp | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 68d3f36916..aa7b81c015 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -1171,21 +1171,17 @@ Transactor::checkTransactionInvariants(TER result, XRPAmount fee) [[nodiscard]] TER Transactor::checkInvariants(TER result, XRPAmount fee) { - // Transaction invariants first (more specific). These check post-conditions of the specific - // transaction. If these fail, the transaction's core logic is wrong. - auto const txResult = checkTransactionInvariants(result, fee); - - // Protocol invariants second (broader). These check properties that must hold regardless of - // transaction type. - auto const protoResult = ctx_.checkInvariants(result, fee); - - // Fail if either check failed. tef (fatal) takes priority over tec. - if (protoResult == tefINVARIANT_FAILED) - return tefINVARIANT_FAILED; - if (txResult == tecINVARIANT_FAILED || protoResult == tecINVARIANT_FAILED) - return tecINVARIANT_FAILED; - - return result; + /* + * DISABLED for 3.2.0 — Must be re-introduced for 3.3.0 + * + * Transaction invariants are disabled due to a performance regression: + * the two-pass design (transaction-specific invariants + protocol invariants) + * iterates over modified ledger entries twice per transaction. + * + * Until resolved, only protocol invariants are checked (delegated to ctx_). + * This is safe because all transaction invariants in 3.2.0 are no-ops. + */ + return ctx_.checkInvariants(result, fee); } //------------------------------------------------------------------------------ ApplyResult From e205d0ef8ec113b354c4d1828284bda3bfb2e711 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:00:52 +0100 Subject: [PATCH 07/30] handle gTlDiscardCurrentSpan change Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> --- cmake/XrplCore.cmake | 2 +- include/xrpl/telemetry/Telemetry.h | 6 +++--- src/libxrpl/telemetry/SpanGuard.cpp | 8 ++++++++ src/libxrpl/telemetry/Telemetry.cpp | 3 +-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/cmake/XrplCore.cmake b/cmake/XrplCore.cmake index c4c9c180a1..c5748016c9 100644 --- a/cmake/XrplCore.cmake +++ b/cmake/XrplCore.cmake @@ -209,7 +209,7 @@ target_link_libraries(xrpl.libxrpl.tx PUBLIC xrpl.libxrpl.ledger) add_module(xrpl telemetry) target_link_libraries( xrpl.libxrpl.telemetry - PUBLIC xrpl.libxrpl.basics xrpl.libxrpl.beast + PUBLIC xrpl.libxrpl.basics xrpl.libxrpl.beast xrpl.libxrpl.config ) if(telemetry) target_link_libraries( diff --git a/include/xrpl/telemetry/Telemetry.h b/include/xrpl/telemetry/Telemetry.h index f1144da676..fdc118ed95 100644 --- a/include/xrpl/telemetry/Telemetry.h +++ b/include/xrpl/telemetry/Telemetry.h @@ -135,11 +135,11 @@ public: std::string serviceName = "xrpld"; /** OTel resource attribute `service.version` (set from BuildInfo). */ - std::string serviceVersion{}; + std::string serviceVersion; /** OTel resource attribute `service.instance.id` (defaults to node public key). */ - std::string serviceInstanceId{}; + std::string serviceInstanceId; /** OTLP/HTTP endpoint URL where spans are sent. */ std::string exporterEndpoint = "http://localhost:4318/v1/traces"; @@ -148,7 +148,7 @@ public: bool useTls = false; /** Path to a CA certificate bundle for TLS verification. */ - std::string tlsCertPath{}; + std::string tlsCertPath; /** Head-based sampling ratio. Intentionally fixed at 1.0 (sample everything) and NOT read from config. A per-node ratio would let diff --git a/src/libxrpl/telemetry/SpanGuard.cpp b/src/libxrpl/telemetry/SpanGuard.cpp index 1e7024150b..87739ae7b8 100644 --- a/src/libxrpl/telemetry/SpanGuard.cpp +++ b/src/libxrpl/telemetry/SpanGuard.cpp @@ -344,6 +344,14 @@ SpanGuard::discard() { gTlDiscardCurrentSpan = true; impl_->span->End(); + // Clear here so discard() owns the flag's whole lifetime + // (set -> End -> clear) in one scope, rather than relying on + // FilteringSpanProcessor::OnEnd() to clear it. Today every valid guard + // wraps a recording span (head sampling is 1.0), so OnEnd() always runs + // and clearing here is equivalent — but colocating set and clear keeps + // the flag leak-proof if a later phase can hand back a non-recording + // span (e.g. honoring a non-sampled remote parent during propagation). + gTlDiscardCurrentSpan = false; impl_->span = nullptr; // prevent ~Impl from calling End() again impl_.reset(); } diff --git a/src/libxrpl/telemetry/Telemetry.cpp b/src/libxrpl/telemetry/Telemetry.cpp index c4425bda84..9e55a40f4e 100644 --- a/src/libxrpl/telemetry/Telemetry.cpp +++ b/src/libxrpl/telemetry/Telemetry.cpp @@ -122,8 +122,7 @@ public: { // SpanGuard::discard() set the flag on this thread just before // calling Span::End(), which invokes OnEnd() synchronously. - // Clear the flag and drop the span. - gTlDiscardCurrentSpan = false; + // Drop the span. return; } delegate_->OnEnd(std::move(span)); From dd0b6754d4c33d906d54f317dcc9f4da800488ec Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 10 Jun 2026 15:45:51 +0100 Subject: [PATCH 08/30] ci: Add `gh` and `file` to nix packages (#7444) --- cspell.config.yaml | 1 + flake.lock | 6 +++--- nix/docker/check-tools.sh | 2 ++ nix/packages.nix | 2 ++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cspell.config.yaml b/cspell.config.yaml index cab2fc3da6..926ac06596 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -84,6 +84,7 @@ words: - coro - coros - cowid + - cpack - cryptocondition - cryptoconditional - cryptoconditions diff --git a/flake.lock b/flake.lock index 2013cfabd4..f8553af703 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1780243769, - "narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=", + "lastModified": 1780749050, + "narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "331800de5053fcebacf6813adb5db9c9dca22a0c", + "rev": "a799d3e3886da994fa307f817a6bc705ae538eeb", "type": "github" }, "original": { diff --git a/nix/docker/check-tools.sh b/nix/docker/check-tools.sh index 67bcdff8a9..276e5977ff 100755 --- a/nix/docker/check-tools.sh +++ b/nix/docker/check-tools.sh @@ -10,10 +10,12 @@ cmake --version conan --version curl --version doxygen --version +file --version g++ --version gcc --version gcov --version gcovr --version +gh --version git --version git-cliff --version gpg --version diff --git a/nix/packages.nix b/nix/packages.nix index d40472634b..fc4eff679e 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -13,7 +13,9 @@ in conan curlMinimal # needed for codecov/codecov-action doxygen + file # needed for cpack in Clio gcovr + gh git git-cliff gnumake From 34d7280df4972aff59d8e5997722e261b47ab643 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:47:43 +0100 Subject: [PATCH 09/30] addressed PR comments Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> --- src/xrpld/rpc/ServerHandler.h | 6 +- src/xrpld/rpc/detail/RPCHandler.cpp | 12 ++- src/xrpld/rpc/detail/ServerHandler.cpp | 106 +++++++++++++++++-------- 3 files changed, 91 insertions(+), 33 deletions(-) diff --git a/src/xrpld/rpc/ServerHandler.h b/src/xrpld/rpc/ServerHandler.h index 0aac05ea44..b1ecf71f82 100644 --- a/src/xrpld/rpc/ServerHandler.h +++ b/src/xrpld/rpc/ServerHandler.h @@ -177,7 +177,11 @@ private: void processSession(std::shared_ptr const&, std::shared_ptr coro); - void + /** Process an RPC request and write the reply to `output`. + @return false if the request resulted in an error response, true + otherwise. Lets the caller's enclosing span reflect the outcome. + */ + bool processRequest( Port const& port, std::string const& request, diff --git a/src/xrpld/rpc/detail/RPCHandler.cpp b/src/xrpld/rpc/detail/RPCHandler.cpp index 5fbc71bf4c..04358eeeb6 100644 --- a/src/xrpld/rpc/detail/RPCHandler.cpp +++ b/src/xrpld/rpc/detail/RPCHandler.cpp @@ -191,8 +191,17 @@ callMethod(JsonContext& context, Method method, std::string const& name, Object& rpc_span::attr::rpcStatus, ret ? std::string_view{rpc_span::val::error} : std::string_view{rpc_span::val::success}); - if (!ret) + // Reflect the result in the OTel span status, not just the attribute, + // so non-exception RPC errors (rpcTOO_BUSY, rpcNO_PERMISSION, ...) are + // visible to {status.code=error} queries. + if (ret) + { + span.setError(rpc_span::val::error); + } + else + { span.setOk(); + } return ret; } catch (std::exception& e) @@ -237,6 +246,7 @@ doCommand(RPC::JsonContext& context, json::Value& result) // "unknown" name only when the request truly omits both fields. 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::rpcStatus, rpc_span::val::error); span.setError(getErrorInfo(error).token.cStr()); injectError(error, result); diff --git a/src/xrpld/rpc/detail/ServerHandler.cpp b/src/xrpld/rpc/detail/ServerHandler.cpp index 7d9e74dec1..c1fd8f40eb 100644 --- a/src/xrpld/rpc/detail/ServerHandler.cpp +++ b/src/xrpld/rpc/detail/ServerHandler.cpp @@ -434,6 +434,8 @@ ServerHandler::processSession( session->close({boost::beast::websocket::policy_error, "threshold exceeded"}); // FIX: This rpcError is not delivered since the session // was just closed. + span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error); + span.setError(rpc_span::val::error); return rpcError(RpcSlowDown); } @@ -465,6 +467,8 @@ ServerHandler::processSession( jr[jss::api_version] = jv[jss::api_version]; is->getConsumer().charge(Resource::kFeeMalformedRpc); + span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error); + span.setError(rpc_span::val::error); return jr; } @@ -545,12 +549,19 @@ ServerHandler::processSession( } jr[jss::request] = rq; + // Mark the span according to the final result. Doing it here (rather + // than an unconditional setOk later) ensures error responses — from + // doCommand, a FORBID role, or the catch block above — are not + // overwritten as OK. + span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error); + span.setError(rpc_span::val::error); } else { if (jr[jss::result].isMember("forwarded") && jr[jss::result]["forwarded"]) jr = jr[jss::result]; jr[jss::status] = jss::success; + span.setOk(); } if (jv.isMember(jss::id)) @@ -563,7 +574,6 @@ ServerHandler::processSession( jr[jss::api_version] = jv[jss::api_version]; jr[jss::type] = jss::response; - span.setOk(); return jr; } @@ -576,7 +586,7 @@ ServerHandler::processSession( auto span = SpanGuard::span(TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::httpRequest); - processRequest( + bool const ok = processRequest( session->port(), buffersToString(session->request().body().data()), session->remoteAddress().atPort(0), @@ -598,7 +608,15 @@ ServerHandler::processSession( { session->close(true); } - span.setOk(); + // Reflect the request outcome on the wrapper span instead of always OK. + if (ok) + { + span.setOk(); + } + else + { + span.setError(rpc_span::val::error); + } } static json::Value @@ -617,7 +635,7 @@ constexpr json::Int kServerOverloaded = -32604; constexpr json::Int kForbidden = -32605; constexpr json::Int kWrongVersion = -32606; -void +bool ServerHandler::processRequest( Port const& port, std::string const& request, @@ -630,18 +648,29 @@ ServerHandler::processRequest( auto span = SpanGuard::span(TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::process); auto rpcJ = app_.getJournal("RPC"); + // Tracks whether any failure occurred. Set on every error path (early + // returns, the catch block, and per-request error replies) and used at the + // end to mark the span status. The HTTP status code alone is insufficient: + // it stays 200 for batch responses and for ripplerpc < 3.0, so relying on + // it would let payload-level errors end the span as successful. + bool spanHadError = false; + + // Marks the span as failed before sending an error reply, so the + // early-return validation paths below are not later seen as successful + // (the span would otherwise end UNSET, invisible to {status.code=error}). + auto httpReplyError = [&](int status, std::string const& message) { + spanHadError = true; + httpReply(status, message, output, rpcJ); + }; + json::Value jsonOrig; { json::Reader reader; if ((request.size() > RPC::Tuning::kMaxRequestSize) || !reader.parse(request, jsonOrig) || !jsonOrig || !jsonOrig.isObject()) { - httpReply( - 400, - "Unable to parse request: " + reader.getFormattedErrorMessages(), - output, - rpcJ); - return; + httpReplyError(400, "Unable to parse request: " + reader.getFormattedErrorMessages()); + return false; } } @@ -652,8 +681,8 @@ ServerHandler::processRequest( batch = true; if (!jsonOrig.isMember(jss::params) || !jsonOrig[jss::params].isArray()) { - httpReply(400, "Malformed batch request", output, rpcJ); - return; + httpReplyError(400, "Malformed batch request"); + return false; } size = jsonOrig[jss::params].size(); } @@ -691,8 +720,8 @@ ServerHandler::processRequest( { if (!batch) { - httpReply(400, jss::invalid_API_version.cStr(), output, rpcJ); - return; + httpReplyError(400, jss::invalid_API_version.cStr()); + return false; } json::Value r(json::ValueType::Object); r[jss::request] = jsonRPC; @@ -734,8 +763,8 @@ ServerHandler::processRequest( { if (!batch) { - httpReply(503, "Server is overloaded", output, rpcJ); - return; + httpReplyError(503, "Server is overloaded"); + return false; } json::Value r = jsonRPC; r[jss::error] = makeJsonError(kServerOverloaded, "Server is overloaded"); @@ -749,8 +778,8 @@ ServerHandler::processRequest( usage.charge(Resource::kFeeMalformedRpc); if (!batch) { - httpReply(403, "Forbidden", output, rpcJ); - return; + httpReplyError(403, "Forbidden"); + return false; } json::Value r = jsonRPC; r[jss::error] = makeJsonError(kForbidden, "Forbidden"); @@ -763,8 +792,8 @@ ServerHandler::processRequest( usage.charge(Resource::kFeeMalformedRpc); if (!batch) { - httpReply(400, "Null method", output, rpcJ); - return; + httpReplyError(400, "Null method"); + return false; } json::Value r = jsonRPC; r[jss::error] = makeJsonError(kMethodNotFound, "Null method"); @@ -778,8 +807,8 @@ ServerHandler::processRequest( usage.charge(Resource::kFeeMalformedRpc); if (!batch) { - httpReply(400, "method is not string", output, rpcJ); - return; + httpReplyError(400, "method is not string"); + return false; } json::Value r = jsonRPC; r[jss::error] = makeJsonError(kMethodNotFound, "method is not string"); @@ -793,8 +822,8 @@ ServerHandler::processRequest( usage.charge(Resource::kFeeMalformedRpc); if (!batch) { - httpReply(400, "method is empty", output, rpcJ); - return; + httpReplyError(400, "method is empty"); + return false; } json::Value r = jsonRPC; r[jss::error] = makeJsonError(kMethodNotFound, "method is empty"); @@ -819,8 +848,8 @@ ServerHandler::processRequest( else if (!params.isArray() || params.size() != 1) { usage.charge(Resource::kFeeMalformedRpc); - httpReply(400, "params unparsable", output, rpcJ); - return; + httpReplyError(400, "params unparsable"); + return false; } else { @@ -828,8 +857,8 @@ ServerHandler::processRequest( if (!params.isObjectOrNull()) { usage.charge(Resource::kFeeMalformedRpc); - httpReply(400, "params unparsable", output, rpcJ); - return; + httpReplyError(400, "params unparsable"); + return false; } } } @@ -846,8 +875,8 @@ ServerHandler::processRequest( usage.charge(Resource::kFeeMalformedRpc); if (!batch) { - httpReply(400, "ripplerpc is not a string", output, rpcJ); - return; + httpReplyError(400, "ripplerpc is not a string"); + return false; } json::Value r = jsonRPC; @@ -906,6 +935,7 @@ ServerHandler::processRequest( << " when processing request: " << json::Compact{json::Value{params}}; span.recordException(ex); span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error); + spanHadError = true; // LCOV_EXCL_STOP } @@ -922,6 +952,7 @@ ServerHandler::processRequest( { if (result.isMember(jss::error)) { + spanHadError = true; result[jss::status] = jss::error; result["code"] = result[jss::error_code]; result["message"] = result[jss::error_message]; @@ -942,6 +973,7 @@ ServerHandler::processRequest( // received. if (result.isMember(jss::error)) { + spanHadError = true; auto rq = params; if (rq.isObject()) @@ -1037,8 +1069,20 @@ ServerHandler::processRequest( } } - span.setOk(); + // Mark the span error if any request failed or the HTTP status is an error. + // spanHadError catches payload-level errors that httpStatus misses (batch + // responses and ripplerpc < 3.0 always return HTTP 200). + if (spanHadError || httpStatus >= 400) + { + span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error); + span.setError(rpc_span::val::error); + } + else + { + span.setOk(); + } httpReply(httpStatus, response, output, rpcJ); + return !spanHadError; } //------------------------------------------------------------------------------ From 56f2e2c4cf6de814446bb56c9b45648ce05927b3 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:57:22 +0100 Subject: [PATCH 10/30] add descriptive error message Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> --- src/xrpld/rpc/detail/ServerHandler.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/xrpld/rpc/detail/ServerHandler.cpp b/src/xrpld/rpc/detail/ServerHandler.cpp index c1fd8f40eb..282593a282 100644 --- a/src/xrpld/rpc/detail/ServerHandler.cpp +++ b/src/xrpld/rpc/detail/ServerHandler.cpp @@ -435,7 +435,7 @@ ServerHandler::processSession( // FIX: This rpcError is not delivered since the session // was just closed. span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error); - span.setError(rpc_span::val::error); + span.setError("resource threshold exceeded"); return rpcError(RpcSlowDown); } @@ -468,7 +468,7 @@ ServerHandler::processSession( is->getConsumer().charge(Resource::kFeeMalformedRpc); span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error); - span.setError(rpc_span::val::error); + span.setError(jr[jss::error].asString()); return jr; } @@ -554,7 +554,7 @@ ServerHandler::processSession( // doCommand, a FORBID role, or the catch block above — are not // overwritten as OK. span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error); - span.setError(rpc_span::val::error); + span.setError(jr[jss::error].asString()); } else { @@ -660,6 +660,7 @@ ServerHandler::processRequest( // (the span would otherwise end UNSET, invisible to {status.code=error}). auto httpReplyError = [&](int status, std::string const& message) { spanHadError = true; + span.setError(message); httpReply(status, message, output, rpcJ); }; From 4ed409f35ad1b04aef21dc8bca37ace7cc211b36 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:05:04 +0100 Subject: [PATCH 11/30] addressed code review comments Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> --- docker/telemetry/grafana/provisioning/datasources/tempo.yaml | 2 +- src/xrpld/rpc/detail/RpcSpanNames.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/telemetry/grafana/provisioning/datasources/tempo.yaml b/docker/telemetry/grafana/provisioning/datasources/tempo.yaml index 1af993961a..d6d2c05d2a 100644 --- a/docker/telemetry/grafana/provisioning/datasources/tempo.yaml +++ b/docker/telemetry/grafana/provisioning/datasources/tempo.yaml @@ -95,7 +95,7 @@ datasources: tag: command operator: "=" scope: span - type: static + type: dynamic - id: rpc-status tag: rpc_status operator: "=" diff --git a/src/xrpld/rpc/detail/RpcSpanNames.h b/src/xrpld/rpc/detail/RpcSpanNames.h index e7bae84c2f..88fd299f29 100644 --- a/src/xrpld/rpc/detail/RpcSpanNames.h +++ b/src/xrpld/rpc/detail/RpcSpanNames.h @@ -159,7 +159,7 @@ using telemetry::attr_val::error; using telemetry::attr_val::success; inline constexpr auto admin = makeStr("admin"); inline constexpr auto user = makeStr("user"); -inline constexpr auto unknownCommand = makeStr("unknown_command"); +inline constexpr auto unknownCommand = makeStr("unknown"); /// "invalid_json" — WS message parse failure or oversize. inline constexpr auto invalidJson = makeStr("invalid_json"); } // namespace val From ffc197b914a2d6f2016e2d31df7cf9eaacb0be3a Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:07:10 +0100 Subject: [PATCH 12/30] fix(telemetry): use underscore attr names and SpanNames constants in Phase 4 - tempo.yaml: align consensus filter tags with emitted keys (consensus_mode, consensus_round, ledger_seq) instead of dotted form - haveConsensus(): set span attributes before early-return paths so the consensus.check span carries diagnostics even when consensus is not reached - replace hardcoded consensus phase/result/vote literals with ConsensusSpanNames.h val constants; add val::phaseOpen/Establish/Accepted - ConsensusReceiveTracing.h: use canonical consensus::span constants instead of duplicate inline detail:: names - SpanGuardFactory test: use rpc_span / consensus::span constants now that levelization permits the dependency Co-Authored-By: Claude Opus 4.8 --- .../scripts/levelization/results/loops.txt | 3 + .../scripts/levelization/results/ordering.txt | 5 +- conan.lock | 2 +- .../provisioning/datasources/tempo.yaml | 6 +- .../libxrpl/telemetry/SpanGuardFactory.cpp | 32 ++++---- .../libxrpl/telemetry/TelemetryConfig.cpp | 2 +- src/xrpld/app/consensus/RCLConsensus.cpp | 5 +- src/xrpld/consensus/Consensus.h | 81 +++++++++++-------- src/xrpld/consensus/ConsensusSpanNames.h | 4 + src/xrpld/telemetry/ConsensusReceiveTracing.h | 26 +++--- 10 files changed, 93 insertions(+), 73 deletions(-) diff --git a/.github/scripts/levelization/results/loops.txt b/.github/scripts/levelization/results/loops.txt index 16e62bb0a7..1f44b6159a 100644 --- a/.github/scripts/levelization/results/loops.txt +++ b/.github/scripts/levelization/results/loops.txt @@ -4,6 +4,9 @@ Loop: test.jtx test.toplevel Loop: test.jtx test.unit_test test.unit_test ~= test.jtx +Loop: xrpl.telemetry xrpld.consensus + xrpld.consensus ~= xrpl.telemetry + Loop: xrpl.telemetry xrpld.rpc xrpld.rpc > xrpl.telemetry diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 39d06d61eb..102387de06 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -207,6 +207,8 @@ test.unit_test > xrpl.protocol tests.libxrpl > xrpl.basics tests.libxrpl > xrpl.config tests.libxrpl > xrpl.core +tests.libxrpl > xrpld.consensus +tests.libxrpl > xrpld.rpc tests.libxrpl > xrpl.json tests.libxrpl > xrpl.ledger tests.libxrpl > xrpl.net @@ -253,7 +255,6 @@ xrpl.shamap > xrpl.basics xrpl.shamap > xrpl.nodestore xrpl.shamap > xrpl.protocol xrpl.telemetry > xrpl.config -xrpl.telemetry > xrpld.consensus xrpl.tx > xrpl.basics xrpl.tx > xrpl.core xrpl.tx > xrpl.ledger @@ -280,7 +281,6 @@ xrpld.consensus > xrpl.basics xrpld.consensus > xrpl.json xrpld.consensus > xrpl.ledger xrpld.consensus > xrpl.protocol -xrpld.consensus > xrpl.telemetry xrpld.core > xrpl.basics xrpld.core > xrpl.config xrpld.core > xrpl.core @@ -332,4 +332,5 @@ xrpld.shamap > xrpld.core xrpld.shamap > xrpl.protocol xrpld.shamap > xrpl.shamap xrpld.telemetry > xrpl.basics +xrpld.telemetry > xrpld.consensus xrpld.telemetry > xrpl.telemetry diff --git a/conan.lock b/conan.lock index f83d3eccc6..ca5927801c 100644 --- a/conan.lock +++ b/conan.lock @@ -16,7 +16,7 @@ "nlohmann_json/3.11.3#45828be26eb619a2e04ca517bb7b828d%1701220705.259", "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914", "libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492", - "libcurl/8.20.0#465ac276192c197ddc6a9f4494004278%1779353234.048", + "libcurl/8.20.0#c90b0c91a33d9a79b519c1c70bafc823%1780907438.587", "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03", "libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1778091117.848", "jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228", diff --git a/docker/telemetry/grafana/provisioning/datasources/tempo.yaml b/docker/telemetry/grafana/provisioning/datasources/tempo.yaml index 9d772ba96a..3e4065ccb9 100644 --- a/docker/telemetry/grafana/provisioning/datasources/tempo.yaml +++ b/docker/telemetry/grafana/provisioning/datasources/tempo.yaml @@ -126,17 +126,17 @@ datasources: type: dynamic # Phase 4: Consensus tracing filters - id: consensus-mode - tag: xrpl.consensus.mode + tag: consensus_mode operator: "=" scope: span type: static - id: consensus-round - tag: xrpl.consensus.round + tag: consensus_round operator: "=" scope: span type: dynamic - id: consensus-ledger-seq - tag: xrpl.ledger.seq + tag: ledger_seq operator: "=" scope: span type: static diff --git a/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp b/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp index aa935b2151..7f2df65485 100644 --- a/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp +++ b/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp @@ -1,12 +1,13 @@ +#include +#include + #include -#include #include #include #include #include -#include #include using namespace xrpl; @@ -30,8 +31,8 @@ TEST(SpanGuardFactory, category_span_returns_null_when_disabled) auto span = SpanGuard::span(TraceCategory::Rpc, "rpc", "test"); EXPECT_FALSE(span); - span.setAttribute("xrpl.rpc.command", "test"); - span.setAttribute("xrpl.rpc.status", "success"); + span.setAttribute(rpc_span::attr::command, "test"); + span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::success); } TEST(SpanGuardFactory, child_span_null_when_no_parent) @@ -87,21 +88,22 @@ TEST(SpanGuardFactory, consensus_close_time_attributes) { // Verify the consensus attribute pattern compiles and // doesn't crash with null SpanGuard. + namespace cs = consensus::span; { auto span = telemetry::SpanGuard::span( - telemetry::TraceCategory::Consensus, telemetry::seg::consensus, "accept.apply"); - span.setAttribute("xrpl.consensus.ledger.seq", static_cast(42)); - span.setAttribute("xrpl.consensus.close_time", static_cast(780000000)); - span.setAttribute("xrpl.consensus.close_time_correct", true); - span.setAttribute("xrpl.consensus.close_resolution_ms", static_cast(30000)); - span.setAttribute("xrpl.consensus.state", std::string("finished")); - span.setAttribute("xrpl.consensus.proposing", true); - span.setAttribute("xrpl.consensus.round_time_ms", static_cast(3500)); + telemetry::TraceCategory::Consensus, telemetry::seg::consensus, cs::op::acceptApply); + span.setAttribute(cs::attr::ledgerSeq, static_cast(42)); + span.setAttribute(cs::attr::closeTime, static_cast(780000000)); + span.setAttribute(cs::attr::closeTimeCorrect, true); + span.setAttribute(cs::attr::closeResolutionMs, static_cast(30000)); + span.setAttribute(cs::attr::consensusState, cs::val::finished); + span.setAttribute(cs::attr::proposing, true); + span.setAttribute(cs::attr::roundTimeMs, static_cast(3500)); } { auto span = telemetry::SpanGuard::span( - telemetry::TraceCategory::Consensus, telemetry::seg::consensus, "accept.apply"); - span.setAttribute("xrpl.consensus.close_time_correct", false); - span.setAttribute("xrpl.consensus.state", std::string("moved_on")); + telemetry::TraceCategory::Consensus, telemetry::seg::consensus, cs::op::acceptApply); + span.setAttribute(cs::attr::closeTimeCorrect, false); + span.setAttribute(cs::attr::consensusState, cs::val::movedOn); } } diff --git a/src/tests/libxrpl/telemetry/TelemetryConfig.cpp b/src/tests/libxrpl/telemetry/TelemetryConfig.cpp index 137b30a62f..c8469bb44b 100644 --- a/src/tests/libxrpl/telemetry/TelemetryConfig.cpp +++ b/src/tests/libxrpl/telemetry/TelemetryConfig.cpp @@ -1,5 +1,5 @@ -#include #include +#include #include #include diff --git a/src/xrpld/app/consensus/RCLConsensus.cpp b/src/xrpld/app/consensus/RCLConsensus.cpp index db8a44675e..277b39938f 100644 --- a/src/xrpld/app/consensus/RCLConsensus.cpp +++ b/src/xrpld/app/consensus/RCLConsensus.cpp @@ -588,7 +588,8 @@ RCLConsensus::Adaptor::doAccept( static_cast( std::chrono::duration_cast(closeResolution).count())); doAcceptSpan.setAttribute( - cs::attr::consensusState, std::string(consensusFail ? "moved_on" : "finished")); + cs::attr::consensusState, + consensusFail ? std::string_view{cs::val::movedOn} : std::string_view{cs::val::finished}); doAcceptSpan.setAttribute(cs::attr::proposing, proposing); doAcceptSpan.setAttribute( cs::attr::roundTimeMs, static_cast(result.roundTime.read().count())); @@ -1284,7 +1285,7 @@ RCLConsensus::Adaptor::startRoundTracing(RCLCxLedger const& prevLgr) roundSpan_->setAttribute(cs::attr::previousProposers, static_cast(prevProposers_)); roundSpan_->setAttribute( cs::attr::previousRoundTimeMs, static_cast(prevRoundTime_.load().count())); - roundSpan_->setAttribute(cs::attr::consensusPhase, "open"); + roundSpan_->setAttribute(cs::attr::consensusPhase, cs::val::phaseOpen); roundSpan_->addEvent(cs::event::phaseOpen); diff --git a/src/xrpld/consensus/Consensus.h b/src/xrpld/consensus/Consensus.h index dbbcc8a0a9..ecd1b5fc60 100644 --- a/src/xrpld/consensus/Consensus.h +++ b/src/xrpld/consensus/Consensus.h @@ -744,7 +744,9 @@ Consensus::startRoundInternal( // after the new round span is in place. if (reason == StartRoundReason::Recovered) { - adaptor_.onPhaseEvent(telemetry::consensus::span::event::phaseOpen, "open"); + adaptor_.onPhaseEvent( + telemetry::consensus::span::event::phaseOpen, + telemetry::consensus::span::val::phaseOpen); } mode_.set(mode, adaptor_); now_ = now; @@ -994,7 +996,9 @@ Consensus::simulate( result_->proposers = prevProposers_ = currPeerPositions_.size(); prevRoundTime_ = result_->roundTime.read(); phase_ = ConsensusPhase::Accepted; - adaptor_.onPhaseEvent(telemetry::consensus::span::event::phaseAccepted, "accepted"); + adaptor_.onPhaseEvent( + telemetry::consensus::span::event::phaseAccepted, + telemetry::consensus::span::val::phaseAccepted); adaptor_.onForceAccept( *result_, previousLedger_, closeResolution_, rawCloseTimes_, mode_.get(), getJson(true)); // NOLINTEND(bugprone-unchecked-optional-access) @@ -1474,7 +1478,9 @@ Consensus::phaseEstablish(std::unique_ptr const& clo } } phase_ = ConsensusPhase::Accepted; - adaptor_.onPhaseEvent(telemetry::consensus::span::event::phaseAccepted, "accepted"); + adaptor_.onPhaseEvent( + telemetry::consensus::span::event::phaseAccepted, + telemetry::consensus::span::val::phaseAccepted); JLOG(j_.debug()) << "transitioned to ConsensusPhase::Accepted"; adaptor_.onAccept( *result_, @@ -1508,7 +1514,9 @@ Consensus::closeLedger(std::unique_ptr const& clog) } openSpan_.reset(); phase_ = ConsensusPhase::Establish; - adaptor_.onPhaseEvent(telemetry::consensus::span::event::phaseEstablish, "establish"); + adaptor_.onPhaseEvent( + telemetry::consensus::span::event::phaseEstablish, + telemetry::consensus::span::val::phaseEstablish); JLOG(j_.debug()) << "transitioned to ConsensusPhase::Establish"; rawCloseTimes_.self = now_; peerUnchangedCounter_ = 0; @@ -1638,7 +1646,9 @@ Consensus::updateOurPositions(std::unique_ptr const& span.addEvent( consensus::span::event::disputeResolve, {{consensus::span::attr::txId, to_string(txId)}, - {consensus::span::attr::disputeOurVote, dispute.getOurVote() ? "yes" : "no"}, + {consensus::span::attr::disputeOurVote, + dispute.getOurVote() ? std::string_view{consensus::span::val::yes} + : std::string_view{consensus::span::val::no}}, {consensus::span::attr::disputeYays, yaysStr}, {consensus::span::attr::disputeNays, naysStr}}); } @@ -1831,6 +1841,38 @@ Consensus::haveConsensus(std::unique_ptr const& clog j_, clog); + // Set span attributes before the early-return branches below so the + // consensus.check span carries diagnostic data even when consensus is + // not reached (the No / Expired paths return early). + span.setAttribute(consensus::span::attr::agreeCount, static_cast(agree)); + span.setAttribute(consensus::span::attr::disagreeCount, static_cast(disagree)); + span.setAttribute( + consensus::span::attr::convergePercent, static_cast(convergePercent_)); + span.setAttribute(consensus::span::attr::haveCloseTimeConsensus, haveCloseTimeConsensus_); + span.setAttribute( + consensus::span::attr::thresholdPercent, + static_cast(adaptor_.parms().avCtConsensusPct)); + span.setAttribute( + consensus::span::attr::proposersFinished, static_cast(currentFinished)); + span.setAttribute(consensus::span::attr::consensusStalled, stalled); + span.setAttribute( + consensus::span::attr::establishCounter, static_cast(establishCounter_)); + + std::string_view stateStr = consensus::span::val::no; + if (result_->state == ConsensusState::Yes) + { + stateStr = consensus::span::val::yes; + } + else if (result_->state == ConsensusState::MovedOn) + { + stateStr = consensus::span::val::movedOn; + } + else if (result_->state == ConsensusState::Expired) + { + stateStr = consensus::span::val::expired; + } + span.setAttribute(consensus::span::attr::consensusResult, stateStr); + if (result_->state == ConsensusState::No) { CLOG(clog) << "No consensus. "; @@ -1872,35 +1914,6 @@ Consensus::haveConsensus(std::unique_ptr const& clog CLOG(clog) << "Unable to reach consensus " << json::Compact{getJson(true)} << ". "; } - span.setAttribute(consensus::span::attr::agreeCount, static_cast(agree)); - span.setAttribute(consensus::span::attr::disagreeCount, static_cast(disagree)); - span.setAttribute( - consensus::span::attr::convergePercent, static_cast(convergePercent_)); - span.setAttribute(consensus::span::attr::haveCloseTimeConsensus, haveCloseTimeConsensus_); - span.setAttribute( - consensus::span::attr::thresholdPercent, - static_cast(adaptor_.parms().avCtConsensusPct)); - span.setAttribute( - consensus::span::attr::proposersFinished, static_cast(currentFinished)); - span.setAttribute(consensus::span::attr::consensusStalled, stalled); - span.setAttribute( - consensus::span::attr::establishCounter, static_cast(establishCounter_)); - - char const* stateStr = "no"; - if (result_->state == ConsensusState::Yes) - { - stateStr = "yes"; - } - else if (result_->state == ConsensusState::MovedOn) - { - stateStr = "moved_on"; - } - else if (result_->state == ConsensusState::Expired) - { - stateStr = "expired"; - } - span.setAttribute(consensus::span::attr::consensusResult, stateStr); - CLOG(clog) << "Consensus has been reached. "; // NOLINTEND(bugprone-unchecked-optional-access) return true; diff --git a/src/xrpld/consensus/ConsensusSpanNames.h b/src/xrpld/consensus/ConsensusSpanNames.h index e9b08b8439..7aa362adb1 100644 --- a/src/xrpld/consensus/ConsensusSpanNames.h +++ b/src/xrpld/consensus/ConsensusSpanNames.h @@ -238,6 +238,10 @@ 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"); } // namespace val } // namespace xrpl::telemetry::consensus::span diff --git a/src/xrpld/telemetry/ConsensusReceiveTracing.h b/src/xrpld/telemetry/ConsensusReceiveTracing.h index 63bc34b243..3c7ea42ca6 100644 --- a/src/xrpld/telemetry/ConsensusReceiveTracing.h +++ b/src/xrpld/telemetry/ConsensusReceiveTracing.h @@ -31,25 +31,19 @@ * span.setAttribute(...); * @endcode * - * @note These span names use inline string_view literals. When - * ConsensusSpanNames.h (from Phase 4) is available, callers should - * migrate to using the constexpr constants defined there. + * @note Span names come from the canonical constants in + * ConsensusSpanNames.h (consensus::span::proposalReceive / + * validationReceive) so they stay in sync with the rest of Phase 4. */ +#include + #include #include #include namespace xrpl::telemetry { -// Inline span name constants for consensus receive spans. -// Phase 4 will provide these via ConsensusSpanNames.h; these are -// temporary definitions for the propagation infrastructure. -namespace detail { -inline constexpr std::string_view proposalReceiveName = "consensus.proposal.receive"; -inline constexpr std::string_view validationReceiveName = "consensus.validation.receive"; -} // namespace detail - /** Create a "consensus.proposal.receive" span for an incoming proposal. * * If the message carries a TraceContext with a valid span_id, the @@ -75,7 +69,7 @@ proposalReceiveSpan([[maybe_unused]] protocol::TMProposeSet const& msg) // trace_id so the receiving span shares the same trace. return SpanGuard::hashSpan( TraceCategory::Consensus, - detail::proposalReceiveName, + consensus::span::proposalReceive, reinterpret_cast(tc.trace_id().data()), tc.trace_id().size(), reinterpret_cast(tc.span_id().data()), @@ -86,7 +80,8 @@ proposalReceiveSpan([[maybe_unused]] protocol::TMProposeSet const& msg) } #endif // No propagated context — create a standalone span. - return SpanGuard::span(TraceCategory::Consensus, "consensus", "proposal.receive"); + return SpanGuard::span( + TraceCategory::Consensus, seg::consensus, consensus::span::op::proposalReceive); } /** Create a "consensus.validation.receive" span for an incoming validation. @@ -111,7 +106,7 @@ validationReceiveSpan([[maybe_unused]] protocol::TMValidation const& msg) { return SpanGuard::hashSpan( TraceCategory::Consensus, - detail::validationReceiveName, + consensus::span::validationReceive, reinterpret_cast(tc.trace_id().data()), tc.trace_id().size(), reinterpret_cast(tc.span_id().data()), @@ -122,7 +117,8 @@ validationReceiveSpan([[maybe_unused]] protocol::TMValidation const& msg) } #endif // No propagated context — create a standalone span. - return SpanGuard::span(TraceCategory::Consensus, "consensus", "validation.receive"); + return SpanGuard::span( + TraceCategory::Consensus, seg::consensus, consensus::span::op::validationReceive); } } // namespace xrpl::telemetry From 6c1d4d92bbc63c76dc058352b617aca8c2a7e8f9 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:28:02 +0100 Subject: [PATCH 13/30] fix(telemetry): keep libxrpl SpanGuard test free of xrpld headers The xrpl_tests target links only xrpl.libxrpl and cannot include the xrpld-level ConsensusSpanNames.h / RpcSpanNames.h (CI build failure on ubuntu-gcc-debug-arm64). Revert the test to underscore-form attribute literals, restore the SpanNames.h include for seg::consensus, and drop the now-unused tests.libxrpl -> xrpld.* levelization entries. Co-Authored-By: Claude Opus 4.8 --- .../scripts/levelization/results/ordering.txt | 2 - .../libxrpl/telemetry/SpanGuardFactory.cpp | 43 +++++++++++-------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 102387de06..d091753158 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -207,8 +207,6 @@ test.unit_test > xrpl.protocol tests.libxrpl > xrpl.basics tests.libxrpl > xrpl.config tests.libxrpl > xrpl.core -tests.libxrpl > xrpld.consensus -tests.libxrpl > xrpld.rpc tests.libxrpl > xrpl.json tests.libxrpl > xrpl.ledger tests.libxrpl > xrpl.net diff --git a/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp b/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp index 7f2df65485..0d3802df8b 100644 --- a/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp +++ b/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp @@ -1,13 +1,12 @@ -#include -#include - #include +#include #include #include #include #include +#include #include using namespace xrpl; @@ -31,8 +30,12 @@ TEST(SpanGuardFactory, category_span_returns_null_when_disabled) auto span = SpanGuard::span(TraceCategory::Rpc, "rpc", "test"); EXPECT_FALSE(span); - span.setAttribute(rpc_span::attr::command, "test"); - span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::success); + // Attribute keys use the underscore convention for span attributes (the + // dotted xrpl.. form is reserved for resource attributes). The + // canonical constants live in the xrpld-level *SpanNames.h headers, which a + // libxrpl test cannot include, so the keys are written as literals here. + span.setAttribute("command", "test"); + span.setAttribute("rpc_status", "success"); } TEST(SpanGuardFactory, child_span_null_when_no_parent) @@ -86,24 +89,26 @@ TEST(SpanGuardFactory, discard_safe_on_null) TEST(SpanGuardFactory, consensus_close_time_attributes) { - // Verify the consensus attribute pattern compiles and - // doesn't crash with null SpanGuard. - namespace cs = consensus::span; + // Verify the consensus attribute pattern compiles and doesn't crash with a + // null SpanGuard. Attribute keys/values use the underscore convention; the + // canonical consensus::span constants are defined in the xrpld-level + // ConsensusSpanNames.h, which a libxrpl test cannot include, so the keys are + // written as literals here. { auto span = telemetry::SpanGuard::span( - telemetry::TraceCategory::Consensus, telemetry::seg::consensus, cs::op::acceptApply); - span.setAttribute(cs::attr::ledgerSeq, static_cast(42)); - span.setAttribute(cs::attr::closeTime, static_cast(780000000)); - span.setAttribute(cs::attr::closeTimeCorrect, true); - span.setAttribute(cs::attr::closeResolutionMs, static_cast(30000)); - span.setAttribute(cs::attr::consensusState, cs::val::finished); - span.setAttribute(cs::attr::proposing, true); - span.setAttribute(cs::attr::roundTimeMs, static_cast(3500)); + telemetry::TraceCategory::Consensus, telemetry::seg::consensus, "accept.apply"); + span.setAttribute("ledger_seq", static_cast(42)); + span.setAttribute("close_time", static_cast(780000000)); + span.setAttribute("close_time_correct", true); + span.setAttribute("close_resolution_ms", static_cast(30000)); + span.setAttribute("consensus_state", std::string("finished")); + span.setAttribute("proposing", true); + span.setAttribute("round_time_ms", static_cast(3500)); } { auto span = telemetry::SpanGuard::span( - telemetry::TraceCategory::Consensus, telemetry::seg::consensus, cs::op::acceptApply); - span.setAttribute(cs::attr::closeTimeCorrect, false); - span.setAttribute(cs::attr::consensusState, cs::val::movedOn); + telemetry::TraceCategory::Consensus, telemetry::seg::consensus, "accept.apply"); + span.setAttribute("close_time_correct", false); + span.setAttribute("consensus_state", std::string("moved_on")); } } From 1f359f719cbf475e6c010ad0fcf5b4044c6768d1 Mon Sep 17 00:00:00 2001 From: Shi Cheng <97218929+shichengripple001@users.noreply.github.com> Date: Thu, 11 Jun 2026 01:24:44 +0800 Subject: [PATCH 14/30] fix: Add [[maybe_unused]] to fix320Enabled for assert=OFF builds (#7446) Co-authored-by: Claude Sonnet 4.6 --- src/libxrpl/ledger/helpers/LendingHelpers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/ledger/helpers/LendingHelpers.cpp b/src/libxrpl/ledger/helpers/LendingHelpers.cpp index 676b473132..f7ec8a8bc3 100644 --- a/src/libxrpl/ledger/helpers/LendingHelpers.cpp +++ b/src/libxrpl/ledger/helpers/LendingHelpers.cpp @@ -813,7 +813,7 @@ doOverpayment( // 3. The overpayment's penalty interest part (= untrackedInterest // for the overpayment path; see computeOverpaymentComponents): // trackedInterestPart() - bool const fix320Enabled = rules.enabled(fixCleanup3_2_0); + [[maybe_unused]] bool const fix320Enabled = rules.enabled(fixCleanup3_2_0); XRPL_ASSERT_IF( fix320Enabled, overpaymentComponents.trackedPrincipalDelta == From 8000adfa797f3d5c81284cb3bcc8c6e9213549ec Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 10 Jun 2026 19:08:34 +0100 Subject: [PATCH 15/30] ci: Make configurations launch on certain event types (#7447) --- .github/scripts/strategy-matrix/generate.py | 57 ++++++++++++++++--- .github/scripts/strategy-matrix/linux.json | 3 +- .github/scripts/strategy-matrix/macos.json | 3 +- .github/scripts/strategy-matrix/windows.json | 6 +- .../workflows/reusable-strategy-matrix.yml | 3 +- 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/.github/scripts/strategy-matrix/generate.py b/.github/scripts/strategy-matrix/generate.py index aaf84a51d0..6353567f27 100755 --- a/.github/scripts/strategy-matrix/generate.py +++ b/.github/scripts/strategy-matrix/generate.py @@ -27,6 +27,19 @@ def get_cmake_args(build_type: str, extra_args: str) -> str: return " ".join(args) +def runs_on_event(exclude_event_types: list[str], event: str | None) -> bool: + """Whether a config should run for the current event. + + 'exclude_event_types' is a list of GitHub event names (e.g. + ["pull_request"]) on which the config should NOT run; an empty list means + the config runs on every event. When no event is given (event is None), no + filtering is applied. + """ + if event is None: + return True + return event not in exclude_event_types + + # --------------------------------------------------------------------------- # Input types — shapes of the JSON config files # --------------------------------------------------------------------------- @@ -43,6 +56,9 @@ class LinuxConfig: suffix: str = "" extra_cmake_args: str = "" image: str = "" # only used by package_configs entries + # List of GitHub event names (e.g. "pull_request") on which this config + # should NOT run. Empty means it runs on every event. + exclude_event_types: list[str] = dataclasses.field(default_factory=list) @dataclasses.dataclass @@ -77,6 +93,9 @@ class PlatformConfig: build_type: list[str] build_only: bool = False # if true, skip tests (e.g. macos/Windows Debug) extra_cmake_args: str = "" + # List of GitHub event names (e.g. "pull_request") on which this config + # should NOT run. Empty means it runs on every event. + exclude_event_types: list[str] = dataclasses.field(default_factory=list) def __post_init__(self) -> None: if isinstance(self.build_type, str): @@ -151,16 +170,21 @@ _ARCHS: dict[str, Architecture] = { } -def expand_linux_matrix(linux: LinuxFile) -> list[MatrixEntry]: +def expand_linux_matrix( + linux: LinuxFile, event: str | None = None +) -> list[MatrixEntry]: """Expand a LinuxFile into a flat list of matrix entries. Each config entry is expanded over the cross-product of its - compiler, build_type, sanitizers, and architecture lists. + compiler, build_type, sanitizers, and architecture lists. Configs that + exclude the current event are skipped. """ entries: list[MatrixEntry] = [] for distro, configs in linux.configs.items(): for cfg in configs: + if not runs_on_event(cfg.exclude_event_types, event): + continue # An empty sanitizers list means "one entry with no sanitizer". effective_sanitizers = cfg.sanitizers or [""] effective_archs = {arch: _ARCHS[arch] for arch in cfg.arch} @@ -218,13 +242,20 @@ def expand_linux_packaging(linux: LinuxFile) -> list[PackagingEntry]: return entries -def expand_platform_matrix(pf: PlatformFile) -> list[MatrixEntry]: - """Expand a PlatformFile (macOS or Windows) into matrix entries.""" +def expand_platform_matrix( + pf: PlatformFile, event: str | None = None +) -> list[MatrixEntry]: + """Expand a PlatformFile (macOS or Windows) into matrix entries. + + Configs that exclude the current event are skipped. + """ platform_name, arch = pf.platform.split("/") is_windows = platform_name == "windows" entries: list[MatrixEntry] = [] for cfg in pf.configs: + if not runs_on_event(cfg.exclude_event_types, event): + continue for build_type in cfg.build_type: entries.append( MatrixEntry( @@ -262,6 +293,14 @@ if __name__ == "__main__": help="Emit the Linux packaging matrix instead of the build/test matrix.", action="store_true", ) + parser.add_argument( + "-e", + "--event", + help="The GitHub event name that triggered the workflow (e.g. 'push', " + "'pull_request'). Configs are filtered by their 'event_type'. If " + "omitted, no filtering is applied.", + default=None, + ) args = parser.parse_args() matrix: list[MatrixEntry] | list[PackagingEntry] = [] @@ -270,12 +309,16 @@ if __name__ == "__main__": matrix = expand_linux_packaging(LinuxFile.load(THIS_DIR / "linux.json")) else: if args.config in ("linux", None): - matrix += expand_linux_matrix(LinuxFile.load(THIS_DIR / "linux.json")) + matrix += expand_linux_matrix( + LinuxFile.load(THIS_DIR / "linux.json"), args.event + ) if args.config in ("macos", None): - matrix += expand_platform_matrix(PlatformFile.load(THIS_DIR / "macos.json")) + matrix += expand_platform_matrix( + PlatformFile.load(THIS_DIR / "macos.json"), args.event + ) if args.config in ("windows", None): matrix += expand_platform_matrix( - PlatformFile.load(THIS_DIR / "windows.json") + PlatformFile.load(THIS_DIR / "windows.json"), args.event ) print(f"matrix={json.dumps({'include': [dataclasses.asdict(e) for e in matrix]})}") diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index edacdbde4c..e6c807ac95 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -41,7 +41,8 @@ "build_type": ["Debug"], "arch": ["amd64"], "suffix": "unity", - "extra_cmake_args": "-Dunity=ON" + "extra_cmake_args": "-Dunity=ON", + "exclude_event_types": ["pull_request"] } ], diff --git a/.github/scripts/strategy-matrix/macos.json b/.github/scripts/strategy-matrix/macos.json index 5b9e32f88e..66d7a55a43 100644 --- a/.github/scripts/strategy-matrix/macos.json +++ b/.github/scripts/strategy-matrix/macos.json @@ -9,7 +9,8 @@ { "build_type": "Debug", "extra_cmake_args": "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", - "build_only": true + "build_only": true, + "exclude_event_types": ["pull_request"] } ] } diff --git a/.github/scripts/strategy-matrix/windows.json b/.github/scripts/strategy-matrix/windows.json index e4678b60db..e25f9ad131 100644 --- a/.github/scripts/strategy-matrix/windows.json +++ b/.github/scripts/strategy-matrix/windows.json @@ -3,6 +3,10 @@ "runner": ["self-hosted", "Windows", "devbox"], "configs": [ { "build_type": "Release" }, - { "build_type": "Debug", "build_only": true } + { + "build_type": "Debug", + "build_only": true, + "exclude_event_types": ["pull_request"] + } ] } diff --git a/.github/workflows/reusable-strategy-matrix.yml b/.github/workflows/reusable-strategy-matrix.yml index ea134b43b2..4518a8ffef 100644 --- a/.github/workflows/reusable-strategy-matrix.yml +++ b/.github/workflows/reusable-strategy-matrix.yml @@ -35,4 +35,5 @@ jobs: id: generate env: GENERATE_CONFIG: ${{ inputs.os != '' && format('--config={0}', inputs.os) || '' }} - run: ./generate.py ${GENERATE_CONFIG} >>"${GITHUB_OUTPUT}" + GENERATE_EVENT: ${{ github.event_name }} + run: ./generate.py ${GENERATE_CONFIG} --event="${GENERATE_EVENT}" >>"${GITHUB_OUTPUT}" From 2f6b466feb716198bf62dacd55b23332dd09e16c Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 10 Jun 2026 19:24:34 +0100 Subject: [PATCH 16/30] ci: Make sanitizer flags lists in the profile, not a string (#7449) --- conan/profiles/sanitizers | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/conan/profiles/sanitizers b/conan/profiles/sanitizers index 4a05fda734..083807ea9e 100644 --- a/conan/profiles/sanitizers +++ b/conan/profiles/sanitizers @@ -52,52 +52,50 @@ include(default) {% endif %} {# Frame pointer required for meaningful stack traces; -O1 for reasonable performance #} -{% set compile_flags = ["-fno-omit-frame-pointer", "-O1"] %} +{% set sanitizer_compiler_flags = ["-fno-omit-frame-pointer", "-O1"] %} {% if compiler == "gcc" %} {# Suppress false positive warnings with GCC #} - {% set _ = compile_flags.append("-Wno-stringop-overflow") %} + {% set _ = sanitizer_compiler_flags.append("-Wno-stringop-overflow") %} {% set relocation_flags = [] %} {% if arch == "x86_64" and enable_asan %} {# Large code model prevents relocation errors in instrumented ASAN binaries #} - {% set _ = compile_flags.append("-mcmodel=large") %} + {% set _ = sanitizer_compiler_flags.append("-mcmodel=large") %} {% set _ = relocation_flags.append("-mcmodel=large") %} {% elif enable_tsan %} {# GCC doesn't support atomic_thread_fence with TSAN; suppress warnings #} - {% set _ = compile_flags.append("-Wno-tsan") %} + {% set _ = sanitizer_compiler_flags.append("-Wno-tsan") %} {% if arch == "x86_64" %} {# Medium code model for TSAN; large is incompatible #} - {% set _ = compile_flags.append("-mcmodel=medium") %} + {% set _ = sanitizer_compiler_flags.append("-mcmodel=medium") %} {% set _ = relocation_flags.append("-mcmodel=medium") %} {% endif %} {% endif %} {% set fsanitize = "-fsanitize=" ~ ",".join(sanitizer_types) %} - {% set _ = compile_flags.append(fsanitize) %} + {% set _ = sanitizer_compiler_flags.append(fsanitize) %} {% set _ = relocation_flags.append(fsanitize) %} - {% set sanitizer_compiler_flags = " ".join(compile_flags) %} - {% set sanitizer_linker_flags = " ".join(relocation_flags) %} + {% set sanitizer_linker_flags = relocation_flags %} {% elif compiler == "clang" or compiler == "apple-clang" %} {% set fsanitize = "-fsanitize=" ~ ",".join(sanitizer_types) %} - {% set _ = compile_flags.append(fsanitize) %} + {% set _ = sanitizer_compiler_flags.append(fsanitize) %} - {% set sanitizer_compiler_flags = " ".join(compile_flags) %} - {% set sanitizer_linker_flags = fsanitize %} + {% set sanitizer_linker_flags = [fsanitize] %} {% endif %} [conf] tools.build:defines+={{defines}} -tools.build:cxxflags+=['{{sanitizer_compiler_flags}}'] -tools.build:sharedlinkflags+=['{{sanitizer_linker_flags}}'] -tools.build:exelinkflags+=['{{sanitizer_linker_flags}}'] +tools.build:cxxflags+={{sanitizer_compiler_flags}} +tools.build:sharedlinkflags+={{sanitizer_linker_flags}} +tools.build:exelinkflags+={{sanitizer_linker_flags}} tools.info.package_id:confs+=["tools.build:cxxflags", "tools.build:exelinkflags", "tools.build:sharedlinkflags", "tools.build:defines"] # &: means "apply only to the consumer/root package" -&:tools.cmake.cmaketoolchain:extra_variables={"SANITIZERS": "{{sanitizers}}", "SANITIZERS_COMPILER_FLAGS": "{{sanitizer_compiler_flags}}", "SANITIZERS_LINKER_FLAGS": "{{sanitizer_linker_flags}}"} +&:tools.cmake.cmaketoolchain:extra_variables={"SANITIZERS": "{{sanitizers}}", "SANITIZERS_COMPILER_FLAGS": "{{sanitizer_compiler_flags | join(' ')}}", "SANITIZERS_LINKER_FLAGS": "{{sanitizer_linker_flags | join(' ')}}"} [options] {% if enable_asan %} From 09c36d066ec6e1ffdcc6b98075c991e202873d5f Mon Sep 17 00:00:00 2001 From: Zhiyuan Wang <96991820+Kassaking7@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:42:41 -0400 Subject: [PATCH 17/30] fix: Correct hybrid offer deletion on credential expiry (#6843) Co-authored-by: Bart --- include/xrpl/protocol/detail/features.macro | 1 + src/libxrpl/tx/paths/OfferStream.cpp | 9 +- src/test/app/PermissionedDEX_test.cpp | 801 ++++++++++++-------- 3 files changed, 475 insertions(+), 336 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 2b2f24ba53..d3500ab144 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FIX (Cleanup3_3_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_2_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_1_3, Supported::Yes, VoteBehavior::DefaultYes) diff --git a/src/libxrpl/tx/paths/OfferStream.cpp b/src/libxrpl/tx/paths/OfferStream.cpp index b7defb4df8..ecc8416a2b 100644 --- a/src/libxrpl/tx/paths/OfferStream.cpp +++ b/src/libxrpl/tx/paths/OfferStream.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -249,7 +250,13 @@ TOfferStreamBase::step() continue; } - if (entry->isFieldPresent(sfDomainID) && + // Pre-fixCleanup3_3_0: validate domain membership for any book. + // Post-fixCleanup3_3_0: only validate when walking a domain book. + // Hybrid offers carry sfDomainID but also participate in the open + // book; expiry of the owner's domain credential should not evict + // the offer from the open book. + if ((!view_.rules().enabled(fixCleanup3_3_0) || book_.domain.has_value()) && + entry->isFieldPresent(sfDomainID) && !permissioned_dex::offerInDomain( view_, entry->key(), entry->getFieldH256(sfDomainID), j_)) { diff --git a/src/test/app/PermissionedDEX_test.cpp b/src/test/app/PermissionedDEX_test.cpp index d534f20248..99e69ce482 100644 --- a/src/test/app/PermissionedDEX_test.cpp +++ b/src/test/app/PermissionedDEX_test.cpp @@ -185,15 +185,15 @@ class PermissionedDEX_test : public beast::unit_test::Suite // test preflight { Env env(*this, features - featurePermissionedDEX); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID), Ter(temDISABLED)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID), Ter(temDISABLED)); env.close(); env.enableFeature(featurePermissionedDEX); env.close(); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); } @@ -214,7 +214,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite // preclaim - someone outside of the domain cannot create domain offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // create devin account who is not part of the domain @@ -223,7 +223,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); env(offer(devin, XRP(10), USD(10)), Domain(domainID), Ter(tecNO_PERMISSION)); @@ -247,7 +247,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite // preclaim - someone with expired cred cannot create domain offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // create devin account who is not part of the domain @@ -256,7 +256,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); auto jv = credentials::create(devin, domainOwner, credType); @@ -282,13 +282,13 @@ class PermissionedDEX_test : public beast::unit_test::Suite // preclaim - cannot create an offer in a non existent domain { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); uint256 const badDomain{ "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134" "E5"}; - env(offer(bob_, XRP(10), USD(10)), Domain(badDomain), Ter(tecNO_PERMISSION)); + env(offer(bob, XRP(10), USD(10)), Domain(badDomain), Ter(tecNO_PERMISSION)); env.close(); } @@ -296,68 +296,68 @@ class PermissionedDEX_test : public beast::unit_test::Suite // domain { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(credentials::deleteCred(domainOwner, gw_, domainOwner, credType)); + env(credentials::deleteCred(domainOwner, gw, domainOwner, credType)); env.close(); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); } // apply - offer can be created even if takerpays issuer is not in // domain { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(credentials::deleteCred(domainOwner, gw_, domainOwner, credType)); + env(credentials::deleteCred(domainOwner, gw, domainOwner, credType)); env.close(); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, USD(10), XRP(10)), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, USD(10), XRP(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, USD(10), XRP(10), 0, true)); } // apply - two domain offers cross with each other { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); - BEAST_EXPECT(ownerCount(env, bob_) == 3); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(ownerCount(env, bob) == 3); // a non domain offer cannot cross with domain offer - env(offer(carol_, USD(10), XRP(10))); + env(offer(carol, USD(10), XRP(10))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, USD(10), XRP(10)), Domain(domainID)); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, alice_, aliceOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, alice_) == 2); + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); } // apply - create lots of domain offers { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); std::vector offerSeqs; @@ -365,19 +365,19 @@ class PermissionedDEX_test : public beast::unit_test::Suite for (size_t i = 0; i <= 100; i++) { - auto const bobOfferSeq{env.seq(bob_)}; + auto const bobOfferSeq{env.seq(bob)}; offerSeqs.emplace_back(bobOfferSeq); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); } for (auto const offerSeq : offerSeqs) { - env(offerCancel(bob_, offerSeq)); + env(offerCancel(bob, offerSeq)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, offerSeq)); + BEAST_EXPECT(!offerExists(env, bob, offerSeq)); } } } @@ -390,10 +390,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite // test preflight - without enabling featurePermissionedDEX amendment { Env env(*this, features - featurePermissionedDEX); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(pay(bob_, alice_, USD(10)), + env(pay(bob, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -403,10 +403,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.enableFeature(featurePermissionedDEX); env.close(); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - env(pay(bob_, alice_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(bob, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); } @@ -431,13 +431,13 @@ class PermissionedDEX_test : public beast::unit_test::Suite // preclaim - cannot send payment with non existent domain { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); uint256 const badDomain{ "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134" "E5"}; - env(pay(bob_, alice_, USD(10)), + env(pay(bob, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(badDomain), @@ -448,10 +448,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite // preclaim - payment with non-domain destination fails { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // create devin account who is not part of the domain @@ -460,11 +460,11 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); // devin is not part of domain - env(pay(alice_, devin, USD(10)), + env(pay(alice, devin, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -476,7 +476,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // devin has not yet accepted cred - env(pay(alice_, devin, USD(10)), + env(pay(alice, devin, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -487,17 +487,17 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // devin can now receive payment after he is in domain - env(pay(alice_, devin, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(alice, devin, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); } // preclaim - non-domain sender cannot send payment { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // create devin account who is not part of the domain @@ -506,11 +506,11 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); // devin tries to send domain payment - env(pay(devin, alice_, USD(10)), + env(pay(devin, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -522,7 +522,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // devin has not yet accepted cred - env(pay(devin, alice_, USD(10)), + env(pay(devin, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -533,28 +533,28 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // devin can now send payment after he is in domain - env(pay(devin, alice_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(devin, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); } // apply - domain owner can always send and receive domain payment { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // domain owner can always be destination - env(pay(alice_, domainOwner, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(alice, domainOwner, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // domain owner can send - env(pay(domainOwner, alice_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(domainOwner, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); } } @@ -567,22 +567,22 @@ class PermissionedDEX_test : public beast::unit_test::Suite // test domain cross currency payment consuming one offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // create a regular offer without domain - auto const regularOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10))); + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, XRP(10), USD(10))); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); - auto const regularDirKey = getDefaultOfferDirKey(env, bob_, regularOfferSeq); + auto const regularDirKey = getDefaultOfferDirKey(env, bob, regularOfferSeq); BEAST_EXPECT(regularDirKey); BEAST_EXPECT(checkDirectorySize( env, *regularDirKey, 1)); // NOLINT(bugprone-unchecked-optional-access) // a domain payment cannot consume regular offers - env(pay(alice_, carol_, USD(10)), + env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -590,23 +590,23 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // create a domain offer - auto const domainOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const domainOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, domainOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, domainOfferSeq, XRP(10), USD(10), 0, true)); - auto const domainDirKey = getDefaultOfferDirKey(env, bob_, domainOfferSeq); + auto const domainDirKey = getDefaultOfferDirKey(env, bob, domainOfferSeq); BEAST_EXPECT(domainDirKey); BEAST_EXPECT(checkDirectorySize( env, *domainDirKey, 1)); // NOLINT(bugprone-unchecked-optional-access) // cross-currency permissioned payment consumed // domain offer instead of regular offer - env(pay(alice_, carol_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, domainOfferSeq)); - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, XRP(10), USD(10))); + BEAST_EXPECT(!offerExists(env, bob, domainOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); // domain directory is empty BEAST_EXPECT(checkDirectorySize( @@ -618,79 +618,79 @@ class PermissionedDEX_test : public beast::unit_test::Suite // test domain payment consuming two offers in the path { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const eur = gw_["EUR"]; - env.trust(eur(1000), alice_); + auto const eur = gw["EUR"]; + env.trust(eur(1000), alice); env.close(); - env.trust(eur(1000), bob_); + env.trust(eur(1000), bob); env.close(); - env.trust(eur(1000), carol_); + env.trust(eur(1000), carol); env.close(); - env(pay(gw_, bob_, eur(100))); + env(pay(gw, bob, eur(100))); env.close(); // create XRP/USD domain offer - auto const usdOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const usdOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); // payment fail because there isn't eur offer - env(pay(alice_, carol_, eur(10)), + env(pay(alice, carol, eur(10)), Path(~USD, ~eur), Sendmax(XRP(10)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); - // bob_ creates a regular USD/EUR offer - auto const regularOfferSeq{env.seq(bob_)}; - env(offer(bob_, USD(10), eur(10))); + // bob creates a regular USD/EUR offer + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), eur(10))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, USD(10), eur(10))); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, USD(10), eur(10))); - // alice_ tries to pay again, but still fails because the regular + // alice tries to pay again, but still fails because the regular // offer cannot be consumed - env(pay(alice_, carol_, eur(10)), + env(pay(alice, carol, eur(10)), Path(~USD, ~eur), Sendmax(XRP(10)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - // bob_ creates a domain USD/EUR offer - auto const eurOfferSeq{env.seq(bob_)}; - env(offer(bob_, USD(10), eur(10)), Domain(domainID)); + // bob creates a domain USD/EUR offer + auto const eurOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), eur(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(10), eur(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(10), eur(10), 0, true)); - // alice_ successfully consume two domain offers: xrp/usd and usd/eur - env(pay(alice_, carol_, eur(5)), Sendmax(XRP(5)), Domain(domainID), Path(~USD, ~eur)); + // alice successfully consume two domain offers: xrp/usd and usd/eur + env(pay(alice, carol, eur(5)), Sendmax(XRP(5)), Domain(domainID), Path(~USD, ~eur)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(5), USD(5), 0, true)); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(5), eur(5), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(5), eur(5), 0, true)); - // alice_ successfully consume two domain offers and deletes them + // alice successfully consume two domain offers and deletes them // we compute path this time using `paths` - env(pay(alice_, carol_, eur(5)), Sendmax(XRP(5)), Domain(domainID), Paths(XRP)); + env(pay(alice, carol, eur(5)), Sendmax(XRP(5)), Domain(domainID), Paths(XRP)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, usdOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, eurOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, usdOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, eurOfferSeq)); // regular offer is not consumed - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, USD(10), eur(10))); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, USD(10), eur(10))); } // domain payment cannot consume offer from another domain { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // Fund devin and create USD trustline @@ -700,7 +700,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); auto const badCredType = "badCred"; @@ -720,24 +720,24 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // domain payment can't consume an offer from another domain - env(pay(alice_, carol_, USD(10)), + env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - // bob_ creates an offer under the right domain - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + // bob creates an offer under the right domain + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); // domain payment now consumes from the right domain - env(pay(alice_, carol_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); } // sanity check: devin, who is part of the domain but doesn't have a @@ -745,10 +745,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite // offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // fund devin but don't create a USD trustline with gateway @@ -764,14 +764,14 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // successful payment because offer is consumed - env(pay(devin, alice_, USD(10)), Sendmax(XRP(10)), Domain(domainID)); + env(pay(devin, alice, USD(10)), Sendmax(XRP(10)), Domain(domainID)); env.close(); } // offer becomes unfunded when offer owner's cred expires { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // create devin account who is not part of the domain @@ -780,7 +780,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); auto jv = credentials::create(devin, domainOwner, credType); @@ -797,7 +797,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // devin's offer can still be consumed while his cred isn't expired - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); env.close(); BEAST_EXPECT(checkOffer(env, devin, offerSeq, XRP(5), USD(5), 0, true)); @@ -805,7 +805,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(std::chrono::seconds(20)); // devin's offer is unfunded now due to expired cred - env(pay(alice_, carol_, USD(5)), + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID), @@ -817,30 +817,30 @@ class PermissionedDEX_test : public beast::unit_test::Suite // offer becomes unfunded when offer owner's cred is removed { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const offerSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const offerSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - // bob_'s offer can still be consumed while his cred exists - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); + // bob's offer can still be consumed while his cred exists + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, offerSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true)); - // remove bob_'s cred - env(credentials::deleteCred(domainOwner, bob_, domainOwner, credType)); + // remove bob's cred + env(credentials::deleteCred(domainOwner, bob, domainOwner, credType)); env.close(); - // bob_'s offer is unfunded now due to expired cred - env(pay(alice_, carol_, USD(5)), + // bob's offer is unfunded now due to expired cred + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, offerSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true)); } } @@ -853,34 +853,34 @@ class PermissionedDEX_test : public beast::unit_test::Suite // payment. If the domain wishes to control who is allowed to ripple // through, they should set the rippling individually Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const eura = alice_["EUR"]; - auto const eurb = bob_["EUR"]; + auto const eura = alice["EUR"]; + auto const eurb = bob["EUR"]; - env.trust(eura(100), bob_); - env.trust(eurb(100), carol_); + env.trust(eura(100), bob); + env.trust(eurb(100), carol); env.close(); - // remove bob_ from domain - env(credentials::deleteCred(domainOwner, bob_, domainOwner, credType)); + // remove bob from domain + env(credentials::deleteCred(domainOwner, bob, domainOwner, credType)); env.close(); - // alice_ can still ripple through bob_ even though he's not part + // alice can still ripple through bob even though he's not part // of the domain, this is intentional - env(pay(alice_, carol_, eurb(10)), Paths(eura), Domain(domainID)); + env(pay(alice, carol, eurb(10)), Paths(eura), Domain(domainID)); env.close(); - env.require(Balance(bob_, eura(10)), Balance(carol_, eurb(10))); + env.require(Balance(bob, eura(10)), Balance(carol, eurb(10))); - // carol_ sets no ripple on bob_ - env(trust(carol_, bob_["EUR"](0), bob_, tfSetNoRipple)); + // carol sets no ripple on bob + env(trust(carol, bob["EUR"](0), bob, tfSetNoRipple)); env.close(); - // payment no longer works because carol_ has no ripple on bob_ - env(pay(alice_, carol_, eurb(5)), Paths(eura), Domain(domainID), Ter(tecPATH_DRY)); + // payment no longer works because carol has no ripple on bob + env(pay(alice, carol, eurb(5)), Paths(eura), Domain(domainID), Ter(tecPATH_DRY)); env.close(); - env.require(Balance(bob_, eura(10)), Balance(carol_, eurb(10))); + env.require(Balance(bob, eura(10)), Balance(carol, eurb(10))); } void @@ -891,37 +891,37 @@ class PermissionedDEX_test : public beast::unit_test::Suite // whether the issuer is in the domain should NOT affect whether an // offer can be consumed in domain payment Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // create an xrp/usd offer with usd as takergets - auto const bobOffer1Seq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const bobOffer1Seq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // create an usd/xrp offer with usd as takerpays - auto const bobOffer2Seq{env.seq(bob_)}; - env(offer(bob_, USD(10), XRP(10)), Domain(domainID), Txflags(tfPassive)); + auto const bobOffer2Seq{env.seq(bob)}; + env(offer(bob, USD(10), XRP(10)), Domain(domainID), Txflags(tfPassive)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOffer1Seq, XRP(10), USD(10), 0, true)); - BEAST_EXPECT(checkOffer(env, bob_, bobOffer2Seq, USD(10), XRP(10), lsfPassive, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOffer1Seq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOffer2Seq, USD(10), XRP(10), lsfPassive, true)); // remove gateway from domain - env(credentials::deleteCred(domainOwner, gw_, domainOwner, credType)); + env(credentials::deleteCred(domainOwner, gw, domainOwner, credType)); env.close(); // payment succeeds even if issuer is not in domain // xrp/usd offer is consumed - env(pay(alice_, carol_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, bobOffer1Seq)); + BEAST_EXPECT(!offerExists(env, bob, bobOffer1Seq)); // payment succeeds even if issuer is not in domain // usd/xrp offer is consumed - env(pay(alice_, carol_, XRP(10)), Path(~XRP), Sendmax(USD(10)), Domain(domainID)); + env(pay(alice, carol, XRP(10)), Path(~XRP), Sendmax(USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, bobOffer2Seq)); + BEAST_EXPECT(!offerExists(env, bob, bobOffer2Seq)); } void @@ -932,36 +932,36 @@ class PermissionedDEX_test : public beast::unit_test::Suite // checking that an unfunded offer will be implicitly removed by a // successful payment tx Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, XRP(100), USD(100)), Domain(domainID)); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, XRP(100), USD(100)), Domain(domainID)); env.close(); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(20), USD(20)), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(20), USD(20)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(20), USD(20), 0, true)); - BEAST_EXPECT(checkOffer(env, alice_, aliceOfferSeq, XRP(100), USD(100), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(20), USD(20), 0, true)); + BEAST_EXPECT(checkOffer(env, alice, aliceOfferSeq, XRP(100), USD(100), 0, true)); - auto const domainDirKey = getDefaultOfferDirKey(env, bob_, bobOfferSeq); + auto const domainDirKey = getDefaultOfferDirKey(env, bob, bobOfferSeq); BEAST_EXPECT(domainDirKey); BEAST_EXPECT(checkDirectorySize( env, *domainDirKey, 2)); // NOLINT(bugprone-unchecked-optional-access) - // remove alice_ from domain and thus alice_'s offer becomes unfunded - env(credentials::deleteCred(domainOwner, alice_, domainOwner, credType)); + // remove alice from domain and thus alice's offer becomes unfunded + env(credentials::deleteCred(domainOwner, alice, domainOwner, credType)); env.close(); - env(pay(gw_, carol_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(gw, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); - // alice_'s unfunded offer is removed implicitly - BEAST_EXPECT(!offerExists(env, alice_, aliceOfferSeq)); + // alice's unfunded offer is removed implicitly + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); BEAST_EXPECT(checkDirectorySize( env, *domainDirKey, 1)); // NOLINT(bugprone-unchecked-optional-access) } @@ -972,12 +972,12 @@ class PermissionedDEX_test : public beast::unit_test::Suite testcase("AMM not used"); Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - AMM const amm(env, alice_, XRP(10), USD(50)); + AMM const amm(env, alice, XRP(10), USD(50)); // a domain payment isn't able to consume AMM - env(pay(bob_, carol_, USD(5)), + env(pay(bob, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID), @@ -985,7 +985,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // a non domain payment can use AMM - env(pay(bob_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5))); + env(pay(bob, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); env.close(); // USD amount in AMM is changed @@ -1001,126 +1001,126 @@ class PermissionedDEX_test : public beast::unit_test::Suite // test preflight - invalid hybrid flag { Env env(*this, features - featurePermissionedDEX); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), + env(offer(bob, XRP(10), USD(10)), Domain(domainID), Txflags(tfHybrid), Ter(temDISABLED)); env.close(); - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Ter(temINVALID_FLAG)); + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Ter(temINVALID_FLAG)); env.close(); env.enableFeature(featurePermissionedDEX); env.close(); // hybrid offer must have domainID - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Ter(temINVALID_FLAG)); + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Ter(temINVALID_FLAG)); env.close(); // hybrid offer must have domainID - auto const offerSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + auto const offerSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, offerSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, offerSeq, XRP(10), USD(10), lsfHybrid, true)); } // apply - domain offer can cross with hybrid { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); - BEAST_EXPECT(offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, bob_) == 3); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, bob) == 3); - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, USD(10), XRP(10)), Domain(domainID)); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, alice_, aliceOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, alice_) == 2); + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); } // apply - open offer can cross with hybrid { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - BEAST_EXPECT(offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, bob_) == 3); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, bob) == 3); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, USD(10), XRP(10))); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10))); env.close(); - BEAST_EXPECT(!offerExists(env, alice_, aliceOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, alice_) == 2); + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); } // apply - by default, hybrid offer tries to cross with offers in the // domain book { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); - BEAST_EXPECT(ownerCount(env, bob_) == 3); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(ownerCount(env, bob) == 3); // hybrid offer auto crosses with domain offer - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, USD(10), XRP(10)), Domain(domainID), Txflags(tfHybrid)); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), Domain(domainID), Txflags(tfHybrid)); env.close(); - BEAST_EXPECT(!offerExists(env, alice_, aliceOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, alice_) == 2); + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); } // apply - hybrid offer does not automatically cross with open offers // because by default, it only tries to cross domain offers { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10))); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, false)); - BEAST_EXPECT(ownerCount(env, bob_) == 3); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false)); + BEAST_EXPECT(ownerCount(env, bob) == 3); // hybrid offer auto crosses with domain offer - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, USD(10), XRP(10)), Domain(domainID), Txflags(tfHybrid)); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), Domain(domainID), Txflags(tfHybrid)); env.close(); - BEAST_EXPECT(offerExists(env, alice_, aliceOfferSeq)); - BEAST_EXPECT(offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, false)); - BEAST_EXPECT(checkOffer(env, alice_, aliceOfferSeq, USD(10), XRP(10), lsfHybrid, true)); - BEAST_EXPECT(ownerCount(env, alice_) == 3); + BEAST_EXPECT(offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false)); + BEAST_EXPECT(checkOffer(env, alice, aliceOfferSeq, USD(10), XRP(10), lsfHybrid, true)); + BEAST_EXPECT(ownerCount(env, alice) == 3); } } @@ -1129,58 +1129,97 @@ class PermissionedDEX_test : public beast::unit_test::Suite { testcase("Hybrid invalid offer"); - // bob_ has a hybrid offer and then he is removed from domain. - // in this case, the hybrid offer will be considered as unfunded even in - // a regular payment + // bob has a hybrid offer and then he is removed from the domain. + // Domain payments must not consume the offer; regular open-book + // payments follow the fixCleanup3_3_0 behavior checked below. Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const hybridOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(50), USD(50)), Txflags(tfHybrid), Domain(domainID)); + auto const hybridOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(50), USD(50)), Txflags(tfHybrid), Domain(domainID)); env.close(); - // remove bob_ from domain - env(credentials::deleteCred(domainOwner, bob_, domainOwner, credType)); + // remove bob from domain + env(credentials::deleteCred(domainOwner, bob, domainOwner, credType)); env.close(); - // bob_'s hybrid offer is unfunded and can not be consumed in a domain + // bob's hybrid offer is unfunded and can not be consumed in a domain // payment - env(pay(alice_, carol_, USD(5)), + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); - // bob_'s unfunded hybrid offer can't be consumed even with a regular - // payment - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5)), Ter(tecPATH_PARTIAL)); - env.close(); - BEAST_EXPECT(checkOffer(env, bob_, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); + if (features[fixCleanup3_3_0]) + { + // Post-fixCleanup3_3_0: hybrid offer can still be consumed via a regular + // open-book payment even though the domain credential was revoked. + auto const carolBalBefore = env.balance(carol, USD); + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); + env.close(); + BEAST_EXPECT(env.balance(carol, USD) - carolBalBefore == USD(5)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(45), USD(45), lsfHybrid, true)); - // create a regular offer - auto const regularOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10))); - env.close(); - BEAST_EXPECT(offerExists(env, bob_, regularOfferSeq)); - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, XRP(10), USD(10))); + // create a regular offer alongside the hybrid one + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); + env.close(); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); - auto const sleHybridOffer = env.le(keylet::offer(bob_.id(), hybridOfferSeq)); - BEAST_EXPECT(sleHybridOffer); - auto const openDir = - sleHybridOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory); - BEAST_EXPECT(checkDirectorySize(env, openDir, 2)); + auto const sleHybridOffer = env.le(keylet::offer(bob.id(), hybridOfferSeq)); + if (!BEAST_EXPECT(sleHybridOffer)) + return; + auto const openDir = + sleHybridOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory); + // both offers are in the open book directory + BEAST_EXPECT(checkDirectorySize(env, openDir, 2)); - // this normal payment should consume the regular offer and remove the - // unfunded hybrid offer - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5))); - env.close(); + // A regular payment crosses the hybrid offer first (FIFO, older + // offer), then stops; the regular offer is untouched. + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); + env.close(); - BEAST_EXPECT(!offerExists(env, bob_, hybridOfferSeq)); - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, XRP(5), USD(5))); - BEAST_EXPECT(checkDirectorySize(env, openDir, 1)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(40), USD(40), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); + BEAST_EXPECT(checkDirectorySize(env, openDir, 2)); + } + else + { + // Pre-fixCleanup3_3_0: the open-book traversal + // also runs the offerInDomain eviction check, so the hybrid offer + // is treated as unfunded and the regular payment fails. + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); + + // create a regular offer + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); + env.close(); + BEAST_EXPECT(offerExists(env, bob, regularOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); + + auto const sleHybridOffer = env.le(keylet::offer(bob.id(), hybridOfferSeq)); + if (!BEAST_EXPECT(sleHybridOffer)) + return; + auto const openDir = + sleHybridOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory); + BEAST_EXPECT(checkDirectorySize(env, openDir, 2)); + + // This payment crosses the regular offer and permanently evicts the + // hybrid offer from the open book (since the payment succeeds, the + // sandbox, including the hybrid eviction, is committed). + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); + env.close(); + + BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(5), USD(5))); + BEAST_EXPECT(checkDirectorySize(env, openDir, 1)); + } } void @@ -1191,29 +1230,29 @@ class PermissionedDEX_test : public beast::unit_test::Suite // both non domain and domain payments can consume hybrid offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const hybridOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + auto const hybridOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); - // hybrid offer can't be consumed since bob_ is not in domain anymore - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5))); + // hybrid offer can't be consumed since bob is not in domain anymore + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, hybridOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq)); } // someone from another domain can't cross hybrid if they specified // wrong domainID { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // Fund accounts @@ -1235,8 +1274,8 @@ class PermissionedDEX_test : public beast::unit_test::Suite env(credentials::accept(devin, badDomainOwner, badCredType)); env.close(); - auto const hybridOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + auto const hybridOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); // other domains can't consume the offer @@ -1246,107 +1285,197 @@ class PermissionedDEX_test : public beast::unit_test::Suite Domain(badDomainID), Ter(tecPATH_DRY)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, hybridOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(10), USD(10), lsfHybrid, true)); - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); - // hybrid offer can't be consumed since bob_ is not in domain anymore - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5))); + // hybrid offer can't be consumed since bob is not in domain anymore + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, hybridOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq)); } // test domain payment consuming two offers w/ hybrid offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const eur = gw_["EUR"]; - env.trust(eur(1000), alice_); + auto const eur = gw["EUR"]; + env.trust(eur(1000), alice); env.close(); - env.trust(eur(1000), bob_); + env.trust(eur(1000), bob); env.close(); - env.trust(eur(1000), carol_); + env.trust(eur(1000), carol); env.close(); - env(pay(gw_, bob_, eur(100))); + env(pay(gw, bob, eur(100))); env.close(); - auto const usdOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const usdOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); // payment fail because there isn't eur offer - env(pay(alice_, carol_, eur(5)), + env(pay(alice, carol, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); - // bob_ creates a hybrid eur offer - auto const eurOfferSeq{env.seq(bob_)}; - env(offer(bob_, USD(10), eur(10)), Domain(domainID), Txflags(tfHybrid)); + // bob creates a hybrid eur offer + auto const eurOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), eur(10)), Domain(domainID), Txflags(tfHybrid)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(10), eur(10), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(10), eur(10), lsfHybrid, true)); - // alice_ successfully consume two domain offers: xrp/usd and usd/eur - env(pay(alice_, carol_, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5)), Domain(domainID)); + // alice successfully consume two domain offers: xrp/usd and usd/eur + env(pay(alice, carol, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(5), USD(5), 0, true)); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(5), eur(5), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(5), eur(5), lsfHybrid, true)); } // test regular payment using a regular offer and a hybrid offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const eur = gw_["EUR"]; - env.trust(eur(1000), alice_); + auto const eur = gw["EUR"]; + env.trust(eur(1000), alice); env.close(); - env.trust(eur(1000), bob_); + env.trust(eur(1000), bob); env.close(); - env.trust(eur(1000), carol_); + env.trust(eur(1000), carol); env.close(); - env(pay(gw_, bob_, eur(100))); + env(pay(gw, bob, eur(100))); env.close(); - // bob_ creates a regular usd offer - auto const usdOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10))); + // bob creates a regular usd offer + auto const usdOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(10), USD(10), 0, false)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, false)); - // bob_ creates a hybrid eur offer - auto const eurOfferSeq{env.seq(bob_)}; - env(offer(bob_, USD(10), eur(10)), Domain(domainID), Txflags(tfHybrid)); + // bob creates a hybrid eur offer + auto const eurOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), eur(10)), Domain(domainID), Txflags(tfHybrid)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(10), eur(10), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(10), eur(10), lsfHybrid, true)); - // alice_ successfully consume two offers: xrp/usd and usd/eur - env(pay(alice_, carol_, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5))); + // alice successfully consume two offers: xrp/usd and usd/eur + env(pay(alice, carol, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(5), USD(5), 0, false)); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(5), eur(5), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, false)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(5), eur(5), lsfHybrid, true)); } } + // Test that a hybrid offer remains crossable in the open book after the + // owner's domain credential expires. A domain payment after expiry should + // fail (domain book evicts the offer in its sandbox), but the open book + // remains usable. + void + testHybridOpenBookAfterCredentialExpiry(FeatureBitset features) + { + testcase("Hybrid open book after credential expiry"); + + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + Account const devin("devin"); + env.fund(XRP(100000), devin); + env.close(); + env.trust(USD(1000), devin); + env.close(); + env(pay(gw, devin, USD(100))); + env.close(); + + // Give devin a credential that expires far enough in the future to + // survive the setup env.close() calls. + auto jv = credentials::create(devin, domainOwner, credType); + uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count(); + jv[sfExpiration.jsonName] = t + 100; + env(jv); + env.close(); + env(credentials::accept(devin, domainOwner, credType)); + env.close(); + + // Devin creates a hybrid offer: sell USD(10) for XRP(10). + // The offer is placed in both the domain book and the open book. + auto const hybridOfferSeq{env.seq(devin)}; + env(offer(devin, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + env.close(); + + BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + + // A non-domain open-book payment partially crosses the offer while + // devin's credential is still valid. + auto carolBalance = env.balance(carol, USD); + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); + env.close(); + BEAST_EXPECT(env.balance(carol, USD) - carolBalance == USD(5)); + BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); + + // Advance time so that devin's credential expires. + env.close(std::chrono::seconds(100)); + + // Confirm devin can no longer create domain offers. + env(offer(devin, XRP(1), USD(1)), Domain(domainID), Ter(tecNO_PERMISSION)); + env.close(); + + // The hybrid offer must still exist in the open book after expiry. + BEAST_EXPECT(offerExists(env, devin, hybridOfferSeq)); + + // A non-domain open-book payment must cross (not evict) the + // remaining portion of devin's hybrid offer. + carolBalance = env.balance(carol, USD); + env(pay(alice, carol, USD(2)), Path(~USD), Sendmax(XRP(2))); + env.close(); + + // Carol received USD; the offer was crossed, not evicted. + BEAST_EXPECT(env.balance(carol, USD) - carolBalance == USD(2)); + // Offer still exists with 3 USD / 3 XRP remaining. + BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(3), USD(3), lsfHybrid, true)); + + // A domain payment now fails because the domain book evicts devin's + // offer (his credential has expired). The eviction is rolled back with + // the failed sandbox, so the offer is NOT permanently removed. + env(pay(alice, carol, USD(1)), + Path(~USD), + Sendmax(XRP(1)), + Domain(domainID), + Ter(tecPATH_PARTIAL)); + env.close(); + + // Offer still intact in the open book; domain payment did not + // permanently delete it. + BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(3), USD(3), lsfHybrid, true)); + + // The open book can still fully consume the remaining portion. + carolBalance = env.balance(carol, USD); + env(pay(alice, carol, USD(3)), Path(~USD), Sendmax(XRP(3))); + env.close(); + BEAST_EXPECT(env.balance(carol, USD) - carolBalance == USD(3)); + BEAST_EXPECT(!offerExists(env, devin, hybridOfferSeq)); + } + void testHybridOfferDirectories(FeatureBitset features) { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); std::vector offerSeqs; @@ -1362,12 +1491,12 @@ class PermissionedDEX_test : public beast::unit_test::Suite for (size_t i = 1; i <= dirCnt; i++) { - auto const bobOfferSeq{env.seq(bob_)}; + auto const bobOfferSeq{env.seq(bob)}; offerSeqs.emplace_back(bobOfferSeq); - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - auto const sleOffer = env.le(keylet::offer(bob_.id(), bobOfferSeq)); + auto const sleOffer = env.le(keylet::offer(bob.id(), bobOfferSeq)); BEAST_EXPECT(sleOffer); BEAST_EXPECT(sleOffer->getFieldH256(sfBookDirectory) == domainDir); BEAST_EXPECT(sleOffer->getFieldArray(sfAdditionalBooks).size() == 1); @@ -1375,17 +1504,17 @@ class PermissionedDEX_test : public beast::unit_test::Suite sleOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory) == openDir); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); BEAST_EXPECT(checkDirectorySize(env, domainDir, i)); BEAST_EXPECT(checkDirectorySize(env, openDir, i)); } for (auto const offerSeq : offerSeqs) { - env(offerCancel(bob_, offerSeq)); + env(offerCancel(bob, offerSeq)); env.close(); dirCnt--; - BEAST_EXPECT(!offerExists(env, bob_, offerSeq)); + BEAST_EXPECT(!offerExists(env, bob, offerSeq)); BEAST_EXPECT(checkDirectorySize(env, domainDir, dirCnt)); BEAST_EXPECT(checkDirectorySize(env, openDir, dirCnt)); } @@ -1397,34 +1526,34 @@ class PermissionedDEX_test : public beast::unit_test::Suite testcase("Auto bridge"); Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const eur = gw_["EUR"]; + auto const eur = gw["EUR"]; - for (auto const& account : {alice_, bob_, carol_}) + for (auto const& account : {alice, bob, carol}) { env(trust(account, eur(10000))); env.close(); } - env(pay(gw_, carol_, eur(1))); + env(pay(gw, carol, eur(1))); env.close(); - auto const aliceOfferSeq{env.seq(alice_)}; - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(alice_, XRP(100), USD(1)), Domain(domainID)); - env(offer(bob_, eur(1), XRP(100)), Domain(domainID)); + auto const aliceOfferSeq{env.seq(alice)}; + auto const bobOfferSeq{env.seq(bob)}; + env(offer(alice, XRP(100), USD(1)), Domain(domainID)); + env(offer(bob, eur(1), XRP(100)), Domain(domainID)); env.close(); - // carol_'s offer should cross bob_ and alice_'s offers due to auto + // carol's offer should cross bob and alice's offers due to auto // bridging - auto const carolOfferSeq{env.seq(carol_)}; - env(offer(carol_, USD(1), eur(1)), Domain(domainID)); + auto const carolOfferSeq{env.seq(carol)}; + env(offer(carol, USD(1), eur(1)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, aliceOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, carolOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, carolOfferSeq)); } void @@ -1819,7 +1948,9 @@ public: // Test hybrid offers testHybridOfferCreate(all); testHybridBookStep(all); + testHybridInvalidOffer(all - fixCleanup3_3_0); testHybridInvalidOffer(all); + testHybridOpenBookAfterCredentialExpiry(all); testHybridOfferDirectories(all); testHybridMalformedOffer(all); testHybridMalformedOffer(all - fixCleanup3_1_3); From fdcbf37e0b8f1fe3a1057d7d769eb67d2889cacc Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:34:45 +0100 Subject: [PATCH 18/30] minor code review comments Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> --- src/libxrpl/telemetry/TelemetryConfig.cpp | 12 ++++++ .../libxrpl/telemetry/TelemetryConfig.cpp | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/libxrpl/telemetry/TelemetryConfig.cpp b/src/libxrpl/telemetry/TelemetryConfig.cpp index fb3b61067b..da6f340b50 100644 --- a/src/libxrpl/telemetry/TelemetryConfig.cpp +++ b/src/libxrpl/telemetry/TelemetryConfig.cpp @@ -7,11 +7,13 @@ See cfg/xrpld-example.cfg for the full list of available options. */ +#include #include #include #include #include +#include #include namespace xrpl::telemetry { @@ -102,6 +104,16 @@ setupTelemetry( setup.tlsClientCertPath = section.valueOr(key::tlsClientCert, ""); setup.tlsClientKeyPath = section.valueOr(key::tlsClientKey, ""); + // Mutual TLS needs both the client certificate and its private key. + // Supplying only one fails later with a cryptic SSL handshake error, so + // reject the partial configuration here with an actionable message. + if (setup.tlsClientCertPath.empty() != setup.tlsClientKeyPath.empty()) + { + Throw( + "[telemetry] tls_client_cert and tls_client_key must be set together " + "(set both for mutual TLS, or neither for one-way TLS)."); + } + // Head sampling is intentionally fixed at 1.0 (sample everything) and is // not read from config. A per-node ratio would let nodes make divergent // keep/drop decisions for the same distributed trace, producing broken diff --git a/src/tests/libxrpl/telemetry/TelemetryConfig.cpp b/src/tests/libxrpl/telemetry/TelemetryConfig.cpp index c8469bb44b..b9458b66ac 100644 --- a/src/tests/libxrpl/telemetry/TelemetryConfig.cpp +++ b/src/tests/libxrpl/telemetry/TelemetryConfig.cpp @@ -4,6 +4,8 @@ #include +#include + using namespace xrpl; TEST(TelemetryConfig, setup_defaults) @@ -83,6 +85,43 @@ TEST(TelemetryConfig, parse_full_section) EXPECT_FALSE(setup.traceLedger); } +TEST(TelemetryConfig, mtls_cert_and_key_both_set) +{ + Section section; + section.set("use_tls", "1"); + section.set("tls_client_cert", "/etc/ssl/client.pem"); + section.set("tls_client_key", "/etc/ssl/client.key"); + + auto setup = telemetry::setupTelemetry(section, "nHUtest123", "2.0.0", 0); + EXPECT_EQ(setup.tlsClientCertPath, "/etc/ssl/client.pem"); + EXPECT_EQ(setup.tlsClientKeyPath, "/etc/ssl/client.key"); +} + +TEST(TelemetryConfig, mtls_cert_without_key_throws) +{ + Section section; + section.set("tls_client_cert", "/etc/ssl/client.pem"); + EXPECT_THROW(telemetry::setupTelemetry(section, "nHUtest123", "2.0.0", 0), std::runtime_error); +} + +TEST(TelemetryConfig, mtls_key_without_cert_throws) +{ + Section section; + section.set("tls_client_key", "/etc/ssl/client.key"); + EXPECT_THROW(telemetry::setupTelemetry(section, "nHUtest123", "2.0.0", 0), std::runtime_error); +} + +TEST(TelemetryConfig, mtls_neither_set_is_one_way_tls) +{ + Section section; + section.set("use_tls", "1"); + section.set("tls_ca_cert", "/etc/ssl/ca.pem"); + + auto setup = telemetry::setupTelemetry(section, "nHUtest123", "2.0.0", 0); + EXPECT_TRUE(setup.tlsClientCertPath.empty()); + EXPECT_TRUE(setup.tlsClientKeyPath.empty()); +} + TEST(TelemetryConfig, null_telemetry_factory) { telemetry::Telemetry::Setup setup; From c0272f131446c26c1ef6cc65245d62054d9e39b3 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:29:53 +0100 Subject: [PATCH 19/30] docs: Add span attribute naming convention (OTel phase 1a) Establish the single, authoritative naming convention for OpenTelemetry span attribute keys so the code, collector, Tempo, dashboards, and docs stay in sync. - CONTRIBUTING.md: new "Telemetry span attribute naming" section under the Style guide as the permanent, canonical home for the rules. - OpenTelemetryPlan/02-design-decisions.md: new section 2.3.3 stating the decided convention as design, and section 2.4 attribute schema realigned to the underscore form (exact key spelling defers to the *SpanNames.h constants). - Sweep the remaining plan docs: convert dotted xrpl.. span attribute keys to the underscore form; leave span names and the OTel-standard service.*/http.* and xrpl.network.* resource keys dotted. Co-Authored-By: Claude Opus 4.8 --- CONTRIBUTING.md | 29 +++ OpenTelemetryPlan/00-tracing-fundamentals.md | 2 +- OpenTelemetryPlan/01-architecture-analysis.md | 36 ++-- OpenTelemetryPlan/02-design-decisions.md | 178 +++++++++++------- .../03-implementation-strategy.md | 6 +- OpenTelemetryPlan/04-code-samples.md | 36 ++-- .../05-configuration-reference.md | 20 +- .../07-observability-backends.md | 22 +-- OpenTelemetryPlan/POC_taskList.md | 30 +-- OpenTelemetryPlan/presentation.md | 30 +-- 10 files changed, 229 insertions(+), 160 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25dd7ac059..bc56d5a6bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -300,6 +300,35 @@ If you wish to automatically fix whatever clang-tidy finds _and_ is capable of f run-clang-tidy -p build -quiet -fix -allow-no-checks src tests ``` +## Telemetry span attribute naming + +OpenTelemetry span attribute keys follow these rules so they stay consistent +across the code, the OTel collector, Tempo, Grafana dashboards, and docs. The +constants in the `*SpanNames.h` headers are the single source of truth; every +other layer must match them. A CI check enforces this end to end. + +1. Per-span unique attribute: bare field name — the span name already carries + the domain (e.g. `command`, `local`, `version` on `rpc.command` / `tx.process`). +2. Collision qualifier: `_` when a bare name would collide across + domains (in the shared spanmetrics label space) or with the OTel-reserved + `status` key (e.g. `rpc_status`, `grpc_status`, `proposal_trusted`, + `validation_trusted`). +3. Shared cross-span attribute: `_` underscore form + (e.g. `tx_hash`, `peer_id`, `ledger_seq`, `consensus_round`). +4. Resource attribute: dotted `xrpl..` — reserved ONLY for + process/network identity set once at startup (`xrpl.network.id`, + `xrpl.network.type`). Never use the dotted `xrpl.` form for span attributes. +5. Span names use `[.]` (dotted). Only attribute _keys_ + follow rules 1–4. + +Standard OpenTelemetry semantic-convention keys keep their canonical dotted +form (e.g. `service.*` resource attributes, `http.*` span attributes); the +"no dotted form" rule above applies to xrpl-custom keys, not to OTel-standard +conventions. + +Always reference the `*SpanNames.h` constants — never pass string literals as +attribute keys or values to `setAttribute`/`addEvent`. + ## Contracts and instrumentation We are using [Antithesis](https://antithesis.com/) for continuous fuzzing, diff --git a/OpenTelemetryPlan/00-tracing-fundamentals.md b/OpenTelemetryPlan/00-tracing-fundamentals.md index d7cc40c5ac..7839273ff2 100644 --- a/OpenTelemetryPlan/00-tracing-fundamentals.md +++ b/OpenTelemetryPlan/00-tracing-fundamentals.md @@ -68,7 +68,7 @@ A **span** represents a single unit of work within a trace. Each span has: | `name` | Operation name | `rpc.submit` | | `start_time` | When work began (local time) | `2024-01-15T10:30:00Z` | | `end_time` | When work completed (local time) | `2024-01-15T10:30:00.050Z` | -| `attributes` | Key-value metadata | `tx.hash=ABC...` | +| `attributes` | Key-value metadata | `tx_hash=ABC...` | | `status` | OK, ERROR MSG | `OK` | ### 3. Trace Context diff --git a/OpenTelemetryPlan/01-architecture-analysis.md b/OpenTelemetryPlan/01-architecture-analysis.md index c62ac3454c..1161a1645b 100644 --- a/OpenTelemetryPlan/01-architecture-analysis.md +++ b/OpenTelemetryPlan/01-architecture-analysis.md @@ -199,7 +199,7 @@ Consensus rounds are multi-phase operations that benefit significantly from trac ```mermaid flowchart TB subgraph round["consensus.round (root span)"] - attrs["Attributes:
xrpl.consensus.ledger.seq = 12345678
xrpl.consensus.mode = proposing
xrpl.consensus.proposers = 35"] + attrs["Attributes:
ledger_seq = 12345678
consensus_mode = proposing
proposers = 35"] subgraph open["consensus.phase.open"] open_desc["Duration: ~3s
Waiting for transactions"] @@ -211,7 +211,7 @@ flowchart TB end subgraph accept["consensus.phase.accept"] - acc_attrs["transactions_applied = 150
ledger.hash = DEF456..."] + acc_attrs["transactions_applied = 150
ledger_hash = DEF456..."] acc_children["├── ledger.build
└── ledger.validate"] end @@ -247,14 +247,14 @@ flowchart TB subgraph request["rpc.request (root span)"] http["HTTP Request — POST /
traceparent:
00-abc123...-def456...-01"] - attrs["Attributes:
http.method = POST
net.peer.ip = 192.168.1.100
xrpl.rpc.command = submit"] + attrs["Attributes:
http.method = POST
net.peer.ip = 192.168.1.100
command = submit"] subgraph enqueue["jobqueue.enqueue"] - job_attr["xrpl.job.type = jtCLIENT_RPC"] + job_attr["job_type = jtCLIENT_RPC"] end subgraph command["rpc.command.submit"] - cmd_attrs["xrpl.rpc.version = 2
xrpl.rpc.role = user"] + cmd_attrs["version = 2
rpc_role = user"] cmd_children["├── tx.deserialize
├── tx.validate_local
└── tx.submit_to_network"] end @@ -354,17 +354,17 @@ After implementing OpenTelemetry, operators and developers will gain visibility ### 1.8.1 What You Will See: Traces -| Trace Type | Description | Example Query in Grafana/Tempo | -| -------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------- | -| **Transaction Lifecycle** | Full journey from RPC submission through validation, relay, consensus, and ledger inclusion | `{service.name="xrpld" && xrpl.tx.hash="ABC123..."}` | -| **Cross-Node Propagation** | Transaction path across multiple xrpld nodes with timing | `{xrpl.tx.relay_count > 0}` | -| **Consensus Rounds** | Complete round with all phases (open, establish, accept) | `{span.name=~"consensus.round.*"}` | -| **RPC Request Processing** | Individual command execution with timing breakdown | `{xrpl.rpc.command="account_info"}` | -| **Ledger Acquisition** | Peer-to-peer ledger data requests and responses | `{span.name="ledger.acquire"}` | -| **PathFinding Latency** | Path computation time and cache effectiveness for payment RPCs | `{span.name="pathfind.compute"}` | -| **TxQ Behavior** | Queue depth, eviction patterns, fee escalation during congestion | `{span.name=~"txq.*"}` | -| **Ledger Sync** | Full acquisition timeline including delta and transaction fetches | `{span.name=~"ledger.acquire.*"}` | -| **Validator Health** | UNL fetch success, manifest updates, stale list detection | `{span.name=~"validator.*"}` | +| Trace Type | Description | Example Query in Grafana/Tempo | +| -------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------- | +| **Transaction Lifecycle** | Full journey from RPC submission through validation, relay, consensus, and ledger inclusion | `{service.name="xrpld" && tx_hash="ABC123..."}` | +| **Cross-Node Propagation** | Transaction path across multiple xrpld nodes with timing | `{relay_count > 0}` | +| **Consensus Rounds** | Complete round with all phases (open, establish, accept) | `{span.name=~"consensus.round.*"}` | +| **RPC Request Processing** | Individual command execution with timing breakdown | `{command="account_info"}` | +| **Ledger Acquisition** | Peer-to-peer ledger data requests and responses | `{span.name="ledger.acquire"}` | +| **PathFinding Latency** | Path computation time and cache effectiveness for payment RPCs | `{span.name="pathfind.compute"}` | +| **TxQ Behavior** | Queue depth, eviction patterns, fee escalation during congestion | `{span.name=~"txq.*"}` | +| **Ledger Sync** | Full acquisition timeline including delta and transaction fetches | `{span.name=~"ledger.acquire.*"}` | +| **Validator Health** | UNL fetch success, manifest updates, stale list detection | `{span.name=~"validator.*"}` | ### 1.8.2 What You Will See: Metrics (Derived from Traces) @@ -456,9 +456,9 @@ xychart-beta ### 1.8.5 Developer Debugging Workflow -1. **Find Transaction**: Query by `xrpl.tx.hash` to get full trace +1. **Find Transaction**: Query by `tx_hash` to get full trace 2. **Identify Bottleneck**: Look at span durations to find slowest component -3. **Check Attributes**: Review `xrpl.tx.validity`, `xrpl.rpc.status` for errors +3. **Check Attributes**: Review `validity`, `rpc_status` for errors 4. **Correlate Logs**: Use `trace_id` to find related PerfLog entries 5. **Compare Nodes**: Filter by `service.instance.id` to compare behavior across nodes diff --git a/OpenTelemetryPlan/02-design-decisions.md b/OpenTelemetryPlan/02-design-decisions.md index 62b0d83fa3..58ef3abdc1 100644 --- a/OpenTelemetryPlan/02-design-decisions.md +++ b/OpenTelemetryPlan/02-design-decisions.md @@ -215,6 +215,39 @@ job: execute: "Job execution" ``` +### 2.3.3 Attribute Naming Conventions + +Span **names** follow §2.3.1 (dotted `.`). Span +**attribute keys** follow the rules below. The constants in the `*SpanNames.h` +headers are the single source of truth; the collector, Tempo, the Grafana +dashboards, and the runbook all consume these exact keys, so every layer must +agree with the code. A CI check enforces this end to end. + +1. **Per-span unique attribute** → bare field name. The span name already + carries the domain, so no prefix is needed (e.g. `command`, `version`, + `local` on `rpc.command`). +2. **Collision qualifier** → `_` when a bare name would collide + across domains in the shared spanmetrics label space, or with the + OTel-reserved `status` key (e.g. `rpc_status`, `grpc_status`, + `proposal_trusted`, `validation_trusted`). +3. **Shared cross-span attribute** → `_` underscore form, used + wherever the same field appears on more than one span (e.g. `tx_hash`, + `peer_id`, `ledger_seq`, `consensus_round`, `consensus_mode`). +4. **Resource attribute** → dotted `xrpl..`, reserved ONLY + for process/network identity set once at startup (`xrpl.network.id`, + `xrpl.network.type`). Span attributes are never dotted in the `xrpl.` form — + it blurs the resource/span scope boundary and parses awkwardly in TraceQL. +5. **Span names** use `[.]` (dotted, per §2.3.1). Only + attribute _keys_ follow rules 1–4. + +Standard OpenTelemetry semantic-convention keys keep their canonical dotted +form (e.g. `service.*` resource attributes, `http.*` span attributes); the +"no dotted form" rule applies to xrpl-custom keys only. + +The same rules are recorded in `CONTRIBUTING.md` (the permanent home, since +`OpenTelemetryPlan/` is removed once the rollout completes). The attribute +examples in §2.4 below follow these rules. + --- ## 2.4 Attribute Schema @@ -238,128 +271,135 @@ resource::SemanticConventions::SERVICE_INSTANCE_ID = ### 2.4.2 Span Attributes by Category +> Span attribute keys use the underscore form from §2.3.3 (shared/qualified +> keys are `_`; per-span unique keys are bare). The dotted form +> is reserved for the resource attributes in §2.4.1 above. This catalog lists +> the planned attribute set by category; the exact emitted key for each +> implemented span is defined by the `*SpanNames.h` constants, which are the +> single source of truth where the two differ. + #### Transaction Attributes ```cpp -"xrpl.tx.hash" = string // Transaction hash (hex) -"xrpl.tx.type" = string // "Payment", "OfferCreate", etc. -"xrpl.tx.account" = string // Source account (redacted in prod) -"xrpl.tx.sequence" = int64 // Account sequence number -"xrpl.tx.fee" = int64 // Fee in drops -"xrpl.tx.result" = string // "tesSUCCESS", "tecPATH_DRY", etc. -"xrpl.tx.ledger_index" = int64 // Ledger containing transaction +"tx_hash" = string // Transaction hash (hex) +"tx_type" = string // "Payment", "OfferCreate", etc. +"tx_account" = string // Source account (redacted in prod) +"tx_sequence" = int64 // Account sequence number +"tx_fee" = int64 // Fee in drops +"tx_result" = string // "tesSUCCESS", "tecPATH_DRY", etc. +"ledger_index" = int64 // Ledger containing transaction ``` #### Consensus Attributes ```cpp -"xrpl.consensus.round" = int64 // Round number -"xrpl.consensus.phase" = string // "open", "establish", "accept" -"xrpl.consensus.mode" = string // "proposing", "observing", etc. -"xrpl.consensus.proposers" = int64 // Number of proposers -"xrpl.consensus.ledger.prev" = string // Previous ledger hash -"xrpl.consensus.ledger.seq" = int64 // Ledger sequence -"xrpl.consensus.tx_count" = int64 // Transactions in consensus set -"xrpl.consensus.duration_ms" = float64 // Round duration +"consensus_round" = int64 // Round number +"consensus_phase" = string // "open", "establish", "accept" +"consensus_mode" = string // "proposing", "observing", etc. +"proposers" = int64 // Number of proposers +"prev_ledger_prefix" = string // Previous ledger hash prefix +"ledger_seq" = int64 // Ledger sequence +"tx_count" = int64 // Transactions in consensus set +"round_time_ms" = float64 // Round duration ``` #### RPC Attributes ```cpp -"xrpl.rpc.command" = string // Command name -"xrpl.rpc.version" = int64 // API version -"xrpl.rpc.role" = string // "admin" or "user" -"xrpl.rpc.params" = string // Sanitized parameters (optional) +"command" = string // Command name (per-span unique on rpc.command) +"version" = int64 // API version +"rpc_role" = string // "admin" or "user" (qualified — "role" is generic) +"params" = string // Sanitized parameters (optional) ``` #### Peer & Message Attributes ```cpp -"xrpl.peer.id" = string // Peer public key (base58) -"xrpl.peer.address" = string // IP:port -"xrpl.peer.latency_ms" = float64 // Measured latency -"xrpl.peer.cluster" = string // Cluster name if clustered -"xrpl.message.type" = string // Protocol message type name -"xrpl.message.size_bytes" = int64 // Message size -"xrpl.message.compressed" = bool // Whether compressed +"peer_id" = string // Peer public key (base58) +"peer_address" = string // IP:port +"peer_latency_ms" = float64 // Measured latency +"peer_cluster" = string // Cluster name if clustered +"message_type" = string // Protocol message type name +"message_size_bytes" = int64 // Message size +"message_compressed" = bool // Whether compressed ``` #### Ledger & Job Attributes ```cpp -"xrpl.ledger.hash" = string // Ledger hash -"xrpl.ledger.index" = int64 // Ledger sequence/index -"xrpl.ledger.close_time" = int64 // Close time (epoch) -"xrpl.ledger.tx_count" = int64 // Transaction count -"xrpl.job.type" = string // Job type name -"xrpl.job.queue_ms" = float64 // Time spent in queue -"xrpl.job.worker" = int64 // Worker thread ID +"ledger_hash" = string // Ledger hash +"ledger_index" = int64 // Ledger sequence/index +"close_time" = int64 // Close time (epoch) +"ledger_tx_count" = int64 // Transaction count +"job_type" = string // Job type name +"job_queue_ms" = float64 // Time spent in queue +"job_worker" = int64 // Worker thread ID ``` #### PathFinding Attributes ```cpp -"xrpl.pathfind.source_currency" = string // Source currency code -"xrpl.pathfind.dest_currency" = string // Destination currency code -"xrpl.pathfind.path_count" = int64 // Number of paths found -"xrpl.pathfind.cache_hit" = bool // RippleLineCache hit +"pathfind_source_currency" = string // Source currency code +"pathfind_dest_currency" = string // Destination currency code +"pathfind_path_count" = int64 // Number of paths found +"pathfind_cache_hit" = bool // RippleLineCache hit ``` #### TxQ Attributes ```cpp -"xrpl.txq.queue_depth" = int64 // Current queue depth -"xrpl.txq.fee_level" = int64 // Fee level of transaction -"xrpl.txq.eviction_reason" = string // Why transaction was evicted +"txq_queue_depth" = int64 // Current queue depth +"txq_fee_level" = int64 // Fee level of transaction +"txq_eviction_reason" = string // Why transaction was evicted ``` #### Fee Attributes ```cpp -"xrpl.fee.load_factor" = int64 // Current load factor -"xrpl.fee.escalation_level" = int64 // Fee escalation multiplier +"fee_load_factor" = int64 // Current load factor +"fee_escalation_level" = int64 // Fee escalation multiplier ``` #### Validator Attributes ```cpp -"xrpl.validator.list_size" = int64 // UNL size -"xrpl.validator.list_age_sec" = int64 // Seconds since last update +"validator_list_size" = int64 // UNL size +"validator_list_age_sec" = int64 // Seconds since last update ``` #### Amendment Attributes ```cpp -"xrpl.amendment.name" = string // Amendment name -"xrpl.amendment.status" = string // "enabled", "vetoed", "supported" +"amendment_name" = string // Amendment name +"amendment_status" = string // "enabled", "vetoed", "supported" ``` #### SHAMap Attributes ```cpp -"xrpl.shamap.type" = string // "transaction", "state", "account_state" -"xrpl.shamap.missing_nodes" = int64 // Number of missing nodes during sync -"xrpl.shamap.duration_ms" = float64 // Sync duration +"shamap_type" = string // "transaction", "state", "account_state" +"shamap_missing_nodes" = int64 // Number of missing nodes during sync +"shamap_duration_ms" = float64 // Sync duration ``` ### 2.4.3 Data Collection Summary The following table summarizes what data is collected by category: -| Category | Attributes Collected | Purpose | -| --------------- | ---------------------------------------------------------------------- | ---------------------------- | -| **Transaction** | `tx.hash`, `tx.type`, `tx.result`, `tx.fee`, `ledger_index` | Trace transaction lifecycle | -| **Consensus** | `round`, `phase`, `mode`, `proposers` (public keys), `duration_ms` | Analyze consensus timing | -| **RPC** | `command`, `version`, `status`, `duration_ms` | Monitor RPC performance | -| **Peer** | `peer.id` (public key), `latency_ms`, `message.type`, `message.size` | Network topology analysis | -| **Ledger** | `ledger.hash`, `ledger.index`, `close_time`, `tx_count` | Ledger progression tracking | -| **Job** | `job.type`, `queue_ms`, `worker` | JobQueue performance | -| **PathFinding** | `pathfind.source_currency`, `dest_currency`, `path_count`, `cache_hit` | Payment path analysis | -| **TxQ** | `txq.queue_depth`, `fee_level`, `eviction_reason` | Queue depth and fee tracking | -| **Fee** | `fee.load_factor`, `escalation_level` | Fee escalation monitoring | -| **Validator** | `validator.list_size`, `list_age_sec` | UNL health monitoring | -| **Amendment** | `amendment.name`, `status` | Protocol upgrade tracking | -| **SHAMap** | `shamap.type`, `missing_nodes`, `duration_ms` | State tree sync performance | +| Category | Attributes Collected | Purpose | +| --------------- | ------------------------------------------------------------------------------------------------- | ---------------------------- | +| **Transaction** | `tx_hash`, `tx_type`, `tx_result`, `tx_fee`, `ledger_index` | Trace transaction lifecycle | +| **Consensus** | `consensus_round`, `consensus_phase`, `consensus_mode`, `proposers`, `round_time_ms` | Analyze consensus timing | +| **RPC** | `command`, `version`, `rpc_status`, `duration_ms` | Monitor RPC performance | +| **Peer** | `peer_id` (public key), `peer_latency_ms`, `message_type`, `message_size_bytes` | Network topology analysis | +| **Ledger** | `ledger_hash`, `ledger_index`, `close_time`, `ledger_tx_count` | Ledger progression tracking | +| **Job** | `job_type`, `job_queue_ms`, `job_worker` | JobQueue performance | +| **PathFinding** | `pathfind_source_currency`, `pathfind_dest_currency`, `pathfind_path_count`, `pathfind_cache_hit` | Payment path analysis | +| **TxQ** | `txq_queue_depth`, `txq_fee_level`, `txq_eviction_reason` | Queue depth and fee tracking | +| **Fee** | `fee_load_factor`, `fee_escalation_level` | Fee escalation monitoring | +| **Validator** | `validator_list_size`, `validator_list_age_sec` | UNL health monitoring | +| **Amendment** | `amendment_name`, `amendment_status` | Protocol upgrade tracking | +| **SHAMap** | `shamap_type`, `shamap_missing_nodes`, `shamap_duration_ms` | State tree sync performance | ### 2.4.4 Privacy & Sensitive Data Policy @@ -384,7 +424,7 @@ The following data is explicitly **excluded** from telemetry collection: | Mechanism | Description | | ----------------------------- | ------------------------------------------------------------------------- | -| **Account Hashing** | `xrpl.tx.account` is hashed at collector level before storage | +| **Account Hashing** | `tx_account` is hashed at collector level before storage | | **Configurable Redaction** | Sensitive fields can be excluded via `[telemetry]` config section | | **Sampling** | Only 10% of traces recorded by default, reducing data exposure | | **Local Control** | Node operators have full control over what gets exported | @@ -400,13 +440,13 @@ processors: attributes: actions: # Hash account addresses before storage - - key: xrpl.tx.account + - key: tx_account action: hash # Remove IP addresses entirely - - key: xrpl.peer.address + - key: peer_address action: delete # Redact specific fields - - key: xrpl.rpc.params + - key: params action: delete ``` @@ -544,8 +584,8 @@ insight.timing("consensus.round", duration); ```cpp // Example OpenTelemetry span auto span = telemetry.startSpan("tx.relay"); -span->SetAttribute("tx.hash", hash); -span->SetAttribute("peer.id", peerId); +span->SetAttribute("tx_hash", hash); +span->SetAttribute("peer_id", peerId); // Span automatically linked to parent via context ``` diff --git a/OpenTelemetryPlan/03-implementation-strategy.md b/OpenTelemetryPlan/03-implementation-strategy.md index 4f6beb61d8..8a3eda6338 100644 --- a/OpenTelemetryPlan/03-implementation-strategy.md +++ b/OpenTelemetryPlan/03-implementation-strategy.md @@ -494,11 +494,11 @@ void ServerHandler::onRequest(...) { // After (only ~10 lines added) void ServerHandler::onRequest(...) { XRPL_TRACE_RPC(app_.getTelemetry(), "rpc.request"); // +1 line - XRPL_TRACE_SET_ATTR("xrpl.rpc.command", command); // +1 line + XRPL_TRACE_SET_ATTR("command", command); // +1 line auto result = processRequest(req); - XRPL_TRACE_SET_ATTR("xrpl.rpc.status", status); // +1 line + XRPL_TRACE_SET_ATTR("rpc_status", status); // +1 line send(result); } ``` @@ -514,7 +514,7 @@ void RCLConsensusAdaptor::startRound(...) { // After (context storage required) void RCLConsensusAdaptor::startRound(...) { XRPL_TRACE_CONSENSUS(app_.getTelemetry(), "consensus.round"); - XRPL_TRACE_SET_ATTR("xrpl.consensus.ledger.seq", seq); + XRPL_TRACE_SET_ATTR("ledger_seq", seq); // Store context for child spans in phase transitions currentRoundContext_ = _xrpl_guard_->context(); // New member variable diff --git a/OpenTelemetryPlan/04-code-samples.md b/OpenTelemetryPlan/04-code-samples.md index d70bcbc760..497ff9d5ea 100644 --- a/OpenTelemetryPlan/04-code-samples.md +++ b/OpenTelemetryPlan/04-code-samples.md @@ -674,9 +674,9 @@ PeerImp::handleTransaction( auto stx = std::make_shared(sit); // Add transaction attributes - guard.setAttribute("xrpl.tx.hash", to_string(stx->getTransactionID())); - guard.setAttribute("xrpl.tx.type", stx->getTxnType()); - guard.setAttribute("xrpl.peer.id", remote_address_.to_string()); + guard.setAttribute("tx_hash", to_string(stx->getTransactionID())); + guard.setAttribute("tx_type", stx->getTxnType()); + guard.setAttribute("peer_id", remote_address_.to_string()); // Check if we've seen this transaction (HashRouter) auto const [flags, suppressed] = @@ -686,7 +686,7 @@ PeerImp::handleTransaction( if (suppressed) { - guard.setAttribute("xrpl.tx.suppressed", true); + guard.setAttribute("suppressed", true); guard.addEvent("tx.duplicate"); return; // Already processing this transaction } @@ -697,7 +697,7 @@ PeerImp::handleTransaction( telemetry::SpanGuard validateGuard(validateSpan); auto [validity, reason] = checkTransaction(stx); - validateGuard.setAttribute("xrpl.tx.validity", + validateGuard.setAttribute("validity", validity == Validity::Valid ? "valid" : "invalid"); if (validity != Validity::Valid) @@ -732,7 +732,7 @@ PeerImp::handleTransaction( protoCtx, // Pass trace context exclusions); - relayGuard.setAttribute("xrpl.tx.relay_count", + relayGuard.setAttribute("relay_count", static_cast(relayCount)); } @@ -763,12 +763,12 @@ RCLConsensusAdaptor::startRound( { XRPL_TRACE_CONSENSUS(app_.getTelemetry(), "consensus.round"); - XRPL_TRACE_SET_ATTR("xrpl.consensus.ledger.prev", to_string(prevLedgerHash)); - XRPL_TRACE_SET_ATTR("xrpl.consensus.ledger.seq", + XRPL_TRACE_SET_ATTR("prev_ledger_prefix", to_string(prevLedgerHash)); + XRPL_TRACE_SET_ATTR("ledger_seq", static_cast(prevLedger.seq() + 1)); - XRPL_TRACE_SET_ATTR("xrpl.consensus.proposers", + XRPL_TRACE_SET_ATTR("proposers", static_cast(peers.size())); - XRPL_TRACE_SET_ATTR("xrpl.consensus.mode", + XRPL_TRACE_SET_ATTR("consensus_mode", proposing ? "proposing" : "observing"); // Store trace context for use in phase transitions @@ -788,7 +788,7 @@ RCLConsensusAdaptor::phaseTransition(ConsensusPhase newPhase) currentRoundContext_); telemetry::SpanGuard guard(span); - guard.setAttribute("xrpl.consensus.phase", to_string(newPhase)); + guard.setAttribute("consensus_phase", to_string(newPhase)); guard.addEvent("phase.enter"); auto const startTime = std::chrono::steady_clock::now(); @@ -798,7 +798,7 @@ RCLConsensusAdaptor::phaseTransition(ConsensusPhase newPhase) auto result = doPhaseTransition(newPhase); auto const duration = std::chrono::steady_clock::now() - startTime; - guard.setAttribute("xrpl.consensus.phase_duration_ms", + guard.setAttribute("consensus_phase_duration_ms", std::chrono::duration(duration).count()); guard.setOk(); @@ -830,9 +830,9 @@ RCLConsensusAdaptor::peerProposal( opentelemetry::trace::SpanKind::kServer); telemetry::SpanGuard guard(span); - guard.setAttribute("xrpl.consensus.proposer", + guard.setAttribute("proposer", toBase58(TokenType::NodePublic, proposal.nodeId())); - guard.setAttribute("xrpl.consensus.round", + guard.setAttribute("consensus_round", static_cast(proposal.proposal().proposeSeq())); // ... existing implementation ... @@ -901,7 +901,7 @@ ServerHandler::onRequest( ? jv["method"].asString() : "unknown"; - guard.setAttribute("xrpl.rpc.command", command); + guard.setAttribute("command", command); // Create child span for command execution auto cmdSpan = app_.getTelemetry().startSpan( @@ -915,7 +915,7 @@ ServerHandler::onRequest( // Record result attributes if (result.isMember("status")) { - cmdGuard.setAttribute("xrpl.rpc.status", + cmdGuard.setAttribute("rpc_status", result["status"].asString()); } @@ -1027,8 +1027,8 @@ JobQueue::processTask(int instance) if (telemetry_.isEnabled()) { guard.emplace(telemetry_.startSpan("job.execute")); - guard->setAttribute("xrpl.job.type", to_string(job.type())); - guard->setAttribute("xrpl.job.worker", + guard->setAttribute("job_type", to_string(job.type())); + guard->setAttribute("job_worker", static_cast(instance)); } #endif diff --git a/OpenTelemetryPlan/05-configuration-reference.md b/OpenTelemetryPlan/05-configuration-reference.md index 0ea40c08a9..9702b1af6c 100644 --- a/OpenTelemetryPlan/05-configuration-reference.md +++ b/OpenTelemetryPlan/05-configuration-reference.md @@ -481,7 +481,7 @@ processors: - name: rpc-spans type: string_attribute string_attribute: - key: xrpl.rpc.command + key: command values: [".*"] enabled_regex_matching: true - name: latency @@ -498,7 +498,7 @@ processors: attributes: actions: # Hash sensitive data - - key: xrpl.tx.account + - key: tx_account action: hash # Add deployment info - key: deployment.environment @@ -674,7 +674,7 @@ datasources: httpMethod: GET tracesToLogs: datasourceUid: loki - tags: ["service.name", "xrpl.tx.hash"] + tags: ["service.name", "tx_hash"] mappedTags: [{ key: "trace_id", value: "traceID" }] mapTagNamesEnabled: true filterByTraceID: true @@ -739,7 +739,7 @@ providers: "targets": [ { "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && span.xrpl.rpc.command != \"\"} | histogram_over_time(duration) by (span.xrpl.rpc.command)" + "query": "{resource.service.name=\"xrpld\" && span.command != \"\"} | histogram_over_time(duration) by (span.command)" } ], "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 } @@ -751,7 +751,7 @@ providers: "targets": [ { "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && status.code=error} | rate() by (span.xrpl.rpc.command)" + "query": "{resource.service.name=\"xrpld\" && status.code=error} | rate() by (span.command)" } ], "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 } @@ -763,7 +763,7 @@ providers: "targets": [ { "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && span.xrpl.rpc.command != \"\"} | avg(duration) by (span.xrpl.rpc.command) | topk(10)" + "query": "{resource.service.name=\"xrpld\" && span.command != \"\"} | avg(duration) by (span.command) | topk(10)" } ], "gridPos": { "h": 8, "w": 24, "x": 0, "y": 8 } @@ -810,7 +810,7 @@ providers: "targets": [ { "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"tx.relay\"} | avg(span.xrpl.tx.relay_count)" + "query": "{resource.service.name=\"xrpld\" && name=\"tx.relay\"} | avg(span.relay_count)" } ], "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 } @@ -837,7 +837,7 @@ Common queries for xrpld traces: ``` # Find all traces for a specific transaction hash -{resource.service.name="xrpld" && span.xrpl.tx.hash="ABC123..."} +{resource.service.name="xrpld" && span.tx_hash="ABC123..."} # Find slow RPC commands (>100ms) {resource.service.name="xrpld" && name=~"rpc.command.*"} | duration > 100ms @@ -849,7 +849,7 @@ Common queries for xrpld traces: {resource.service.name="xrpld" && name="tx.validate" && status.code=error} # Find transactions relayed to many peers -{resource.service.name="xrpld" && name="tx.relay"} | span.xrpl.tx.relay_count > 10 +{resource.service.name="xrpld" && name="tx.relay"} | span.relay_count > 10 # Compare latency across nodes {resource.service.name="xrpld" && name="rpc.command.account_info"} | avg(duration) by (resource.service.instance.id) @@ -909,7 +909,7 @@ In Tempo data source configuration, set up the derived field: jsonData: tracesToLogs: datasourceUid: loki - tags: ["trace_id", "xrpl.tx.hash"] + tags: ["trace_id", "tx_hash"] filterByTraceID: true filterBySpanID: false ``` diff --git a/OpenTelemetryPlan/07-observability-backends.md b/OpenTelemetryPlan/07-observability-backends.md index 5d1638670a..b8e35d91f5 100644 --- a/OpenTelemetryPlan/07-observability-backends.md +++ b/OpenTelemetryPlan/07-observability-backends.md @@ -279,7 +279,7 @@ Pre-built dashboards for xrpld observability. "targets": [ { "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"consensus.round\"} | avg(span.xrpl.consensus.proposers)" + "query": "{resource.service.name=\"xrpld\" && name=\"consensus.round\"} | avg(span.proposers)" } ], "gridPos": { "h": 4, "w": 6, "x": 0, "y": 8 } @@ -503,18 +503,18 @@ flowchart TB - **xrpld Node (three sources)**: A single node emits three independent data streams -- OpenTelemetry spans, PerfLog JSON logs, and Beast Insight StatsD metrics. - **Data Collection layer**: Each stream has its own collector -- OTel Collector for spans, Promtail/Fluentd for logs, and a StatsD exporter for metrics. They operate independently. - **Storage layer (Tempo, Loki, Prometheus)**: Each data type lands in a purpose-built store optimized for its query patterns (trace search, log grep, metric aggregation). -- **Grafana Correlation Panel**: The key integration point -- Grafana queries all three stores and links them via shared fields (`trace_id`, `xrpl.tx.hash`, `ledger_seq`), enabling a single-pane debugging experience. +- **Grafana Correlation Panel**: The key integration point -- Grafana queries all three stores and links them via shared fields (`trace_id`, `tx_hash`, `ledger_seq`), enabling a single-pane debugging experience. ### 7.7.2 Correlation Fields -| Source | Field | Link To | Purpose | -| ----------- | --------------------------- | ------------- | -------------------------- | -| **Trace** | `trace_id` | Logs | Find log entries for trace | -| **Trace** | `xrpl.tx.hash` | Logs, Metrics | Find TX-related data | -| **Trace** | `xrpl.consensus.ledger.seq` | Logs | Find ledger-related logs | -| **PerfLog** | `trace_id` (new) | Traces | Jump to trace from log | -| **PerfLog** | `ledger_seq` | Traces | Find consensus trace | -| **Insight** | `exemplar.trace_id` | Traces | Jump from metric spike | +| Source | Field | Link To | Purpose | +| ----------- | ------------------- | ------------- | -------------------------- | +| **Trace** | `trace_id` | Logs | Find log entries for trace | +| **Trace** | `tx_hash` | Logs, Metrics | Find TX-related data | +| **Trace** | `ledger_seq` | Logs | Find ledger-related logs | +| **PerfLog** | `trace_id` (new) | Traces | Jump to trace from log | +| **PerfLog** | `ledger_seq` | Traces | Find consensus trace | +| **Insight** | `exemplar.trace_id` | Traces | Jump from metric spike | ### 7.7.3 Example: Debugging a Slow Transaction @@ -522,7 +522,7 @@ flowchart TB ``` # In Grafana Explore with Tempo -{resource.service.name="xrpld" && span.xrpl.tx.hash="ABC123..."} +{resource.service.name="xrpld" && span.tx_hash="ABC123..."} ``` **Step 2: Get the trace_id from the trace view** diff --git a/OpenTelemetryPlan/POC_taskList.md b/OpenTelemetryPlan/POC_taskList.md index 32fd73170f..3e2b96d86a 100644 --- a/OpenTelemetryPlan/POC_taskList.md +++ b/OpenTelemetryPlan/POC_taskList.md @@ -344,12 +344,12 @@ - `#include` the `TracingInstrumentation.h` header - In `ServerHandler::onRequest(Session& session)`: - At the top of the method, add: `XRPL_TRACE_RPC(app_.getTelemetry(), "rpc.request");` - - After the RPC command name is extracted, set attribute: `XRPL_TRACE_SET_ATTR("xrpl.rpc.command", command);` + - After the RPC command name is extracted, set attribute: `XRPL_TRACE_SET_ATTR("command", command);` - After the response status is known, set: `XRPL_TRACE_SET_ATTR("http.status_code", static_cast(statusCode));` - Wrap error paths with: `XRPL_TRACE_EXCEPTION(e);` - In `ServerHandler::processRequest(...)`: - Add a child span: `XRPL_TRACE_RPC(app_.getTelemetry(), "rpc.process");` - - Set method attribute: `XRPL_TRACE_SET_ATTR("xrpl.rpc.method", request_method);` + - Set method attribute: `XRPL_TRACE_SET_ATTR("rpc_method", request_method);` - In `ServerHandler::onWSMessage(...)` (WebSocket path): - Add: `XRPL_TRACE_RPC(app_.getTelemetry(), "rpc.ws.message");` @@ -370,7 +370,7 @@ - [01-architecture-analysis.md §1.5](./01-architecture-analysis.md) — RPC request flow diagram: HTTP request -> attributes -> jobqueue.enqueue -> rpc.command -> response - [01-architecture-analysis.md §1.6](./01-architecture-analysis.md) — Key trace points table: `rpc.request` in `ServerHandler.cpp::onRequest()` (Priority: High) - [02-design-decisions.md §2.3](./02-design-decisions.md) — Span naming convention: `rpc.request`, `rpc.command.*` -- [02-design-decisions.md §2.4.2](./02-design-decisions.md) — RPC span attributes: `xrpl.rpc.command`, `xrpl.rpc.version`, `xrpl.rpc.role`, `xrpl.rpc.params` +- [02-design-decisions.md §2.4.2](./02-design-decisions.md) — RPC span attributes: `command`, `version`, `rpc_role`, `params` - [03-implementation-strategy.md §3.9.2](./03-implementation-strategy.md) — File impact: `ServerHandler.cpp` ~40 lines added, ~10 changed (Low risk) --- @@ -386,17 +386,17 @@ - In `doCommand(RPC::JsonContext& context, Json::Value& result)`: - At the top: `XRPL_TRACE_RPC(context.app.getTelemetry(), "rpc.command." + context.method);` - Set attributes: - - `XRPL_TRACE_SET_ATTR("xrpl.rpc.command", context.method);` - - `XRPL_TRACE_SET_ATTR("xrpl.rpc.version", static_cast(context.apiVersion));` - - `XRPL_TRACE_SET_ATTR("xrpl.rpc.role", (context.role == Role::ADMIN) ? "admin" : "user");` - - On success: `XRPL_TRACE_SET_ATTR("xrpl.rpc.status", "success");` - - On error: `XRPL_TRACE_SET_ATTR("xrpl.rpc.status", "error");` and set the error message + - `XRPL_TRACE_SET_ATTR("command", context.method);` + - `XRPL_TRACE_SET_ATTR("version", static_cast(context.apiVersion));` + - `XRPL_TRACE_SET_ATTR("rpc_role", (context.role == Role::ADMIN) ? "admin" : "user");` + - On success: `XRPL_TRACE_SET_ATTR("rpc_status", "success");` + - On error: `XRPL_TRACE_SET_ATTR("rpc_status", "error");` and set the error message - After this, traces in Tempo/Grafana should look like: ``` - rpc.request (xrpl.rpc.command=account_info) + rpc.request (command=account_info) └── rpc.process - └── rpc.command.account_info (xrpl.rpc.version=2, xrpl.rpc.role=user, xrpl.rpc.status=success) + └── rpc.command.account_info (version=2, rpc_role=user, rpc_status=success) ``` **Key modified file**: @@ -407,7 +407,7 @@ - [04-code-samples.md §4.5.3](./04-code-samples.md) — `ServerHandler::onRequest()` code sample (includes child span pattern for `rpc.command.*`) - [02-design-decisions.md §2.3](./02-design-decisions.md) — Span naming: `rpc.command.*` pattern with dynamic command name (e.g., `rpc.command.server_info`) -- [02-design-decisions.md §2.4.2](./02-design-decisions.md) — RPC attribute schema: `xrpl.rpc.command`, `xrpl.rpc.version`, `xrpl.rpc.role`, `xrpl.rpc.status` +- [02-design-decisions.md §2.4.2](./02-design-decisions.md) — RPC attribute schema: `command`, `version`, `rpc_role`, `rpc_status` - [01-architecture-analysis.md §1.6](./01-architecture-analysis.md) — Key trace points table: `rpc.command.*` in `RPCHandler.cpp::doCommand()` (Priority: High) - [02-design-decisions.md §2.6.5](./02-design-decisions.md) — Correlation with PerfLog: how `doCommand()` can link trace_id with existing PerfLog entries - [03-implementation-strategy.md §3.4.4](./03-implementation-strategy.md) — RPC request overhead budget: ~1.75 μs total per request @@ -480,7 +480,7 @@ - Navigate to Explore → select Tempo datasource - Search for service `xrpld` - Confirm you see traces with spans: `rpc.request` -> `rpc.process` -> `rpc.command.server_info` - - Click into a trace and verify attributes: `xrpl.rpc.command`, `xrpl.rpc.status`, `xrpl.rpc.version` + - Click into a trace and verify attributes: `command`, `rpc_status`, `version` 7. **Verify zero-overhead when disabled**: - Rebuild with `XRPL_ENABLE_TELEMETRY=OFF`, or set `enabled=0` in config @@ -494,7 +494,7 @@ - [ ] xrpld starts and connects to OTel Collector (check xrpld logs for telemetry messages) - [ ] Traces appear in Grafana/Tempo under service "xrpld" - [ ] Span hierarchy is correct (parent-child relationships) -- [ ] Span attributes are populated (`xrpl.rpc.command`, `xrpl.rpc.status`, etc.) +- [ ] Span attributes are populated (`command`, `rpc_status`, etc.) - [ ] Error spans show error status and message - [ ] Building with `XRPL_ENABLE_TELEMETRY=OFF` produces no regressions - [ ] Setting `enabled=0` at runtime produces no traces and no errors @@ -580,8 +580,8 @@ The current POC exports **traces only**. Grafana's Explore view can query Tempo explicit: buckets: [1ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 5s] dimensions: - - name: xrpl.rpc.command - - name: xrpl.rpc.status + - name: command + - name: rpc_status exporters: prometheus: diff --git a/OpenTelemetryPlan/presentation.md b/OpenTelemetryPlan/presentation.md index 479aa8fa55..535807a96e 100644 --- a/OpenTelemetryPlan/presentation.md +++ b/OpenTelemetryPlan/presentation.md @@ -618,14 +618,14 @@ flowchart LR ### What Data is Collected -| Category | Attributes Collected | Purpose | -| --------------- | ------------------------------------------------------------------------------------ | --------------------------- | -| **Transaction** | `tx.hash`, `tx.type`, `tx.result`, `tx.fee`, `ledger_index` | Trace transaction lifecycle | -| **Consensus** | `round`, `phase`, `mode`, `proposers` (count of proposing validators), `duration_ms` | Analyze consensus timing | -| **RPC** | `command`, `version`, `status`, `duration_ms` | Monitor RPC performance | -| **Peer** | `peer.id`(public key), `latency_ms`, `message.type`, `message.size` | Network topology analysis | -| **Ledger** | `ledger.hash`, `ledger.index`, `close_time`, `tx_count` | Ledger progression tracking | -| **Job** | `job.type`, `queue_ms`, `worker` | JobQueue performance | +| Category | Attributes Collected | Purpose | +| --------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------- | +| **Transaction** | `tx_hash`, `tx_type`, `tx_result`, `tx_fee`, `ledger_index` | Trace transaction lifecycle | +| **Consensus** | `consensus_round`, `consensus_phase`, `consensus_mode`, `proposers` (count of proposing validators), `round_time_ms` | Analyze consensus timing | +| **RPC** | `command`, `version`, `rpc_status`, `duration_ms` | Monitor RPC performance | +| **Peer** | `peer_id`(public key), `peer_latency_ms`, `message_type`, `message_size_bytes` | Network topology analysis | +| **Ledger** | `ledger_hash`, `ledger_index`, `close_time`, `ledger_tx_count` | Ledger progression tracking | +| **Job** | `job_type`, `job_queue_ms`, `job_worker` | JobQueue performance | ### What is NOT Collected (Privacy Guarantees) @@ -658,13 +658,13 @@ flowchart LR ### Privacy Protection Mechanisms -| Mechanism | Description | -| -------------------------- | ------------------------------------------------------------- | -| **Account Hashing** | `xrpl.tx.account` is hashed at collector level before storage | -| **Configurable Redaction** | Sensitive fields can be excluded via config | -| **Sampling** | Only 10% of traces recorded by default (reduces exposure) | -| **Local Control** | Node operators control what gets exported | -| **No Raw Payloads** | Transaction content is never recorded, only metadata | +| Mechanism | Description | +| -------------------------- | --------------------------------------------------------- | +| **Account Hashing** | `tx_account` is hashed at collector level before storage | +| **Configurable Redaction** | Sensitive fields can be excluded via config | +| **Sampling** | Only 10% of traces recorded by default (reduces exposure) | +| **Local Control** | Node operators control what gets exported | +| **No Raw Payloads** | Transaction content is never recorded, only metadata | > **Key Principle**: Telemetry collects **operational metadata** (timing, counts, hashes) — never **sensitive content** (keys, balances, amounts). From d6450631bf94cc5ed450a27681fa200b7d6fb7d2 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:29:01 +0100 Subject: [PATCH 20/30] removed code blocks from plan docs Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> --- OpenTelemetryPlan/00-tracing-fundamentals.md | 11 +- OpenTelemetryPlan/02-design-decisions.md | 423 +++--- .../03-implementation-strategy.md | 62 +- OpenTelemetryPlan/04-code-samples.md | 1129 ----------------- .../05-configuration-reference.md | 751 +---------- .../07-observability-backends.md | 317 +---- OpenTelemetryPlan/08-appendix.md | 2 - OpenTelemetryPlan/OpenTelemetryPlan.md | 33 +- OpenTelemetryPlan/POC_taskList.md | 636 ---------- 9 files changed, 225 insertions(+), 3139 deletions(-) delete mode 100644 OpenTelemetryPlan/04-code-samples.md delete mode 100644 OpenTelemetryPlan/POC_taskList.md diff --git a/OpenTelemetryPlan/00-tracing-fundamentals.md b/OpenTelemetryPlan/00-tracing-fundamentals.md index 7839273ff2..9c6f96d7af 100644 --- a/OpenTelemetryPlan/00-tracing-fundamentals.md +++ b/OpenTelemetryPlan/00-tracing-fundamentals.md @@ -494,16 +494,7 @@ traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 ### Protocol Buffers (xrpld P2P messages) -```protobuf -message TMTransaction { - bytes rawTransaction = 1; - // ... existing fields ... - - // Trace context extension - bytes trace_parent = 100; // W3C traceparent - bytes trace_state = 101; // W3C tracestate -} -``` +xrpld P2P messages such as `TMTransaction` carry the trace context in two added byte fields alongside the existing payload: `trace_parent` holds the W3C traceparent (`trace_id`, `span_id`, and `trace_flags`), and `trace_state` holds the optional W3C tracestate. Together they propagate the trace across the P2P boundary so a receiving node can attach its spans to the sender's span. --- diff --git a/OpenTelemetryPlan/02-design-decisions.md b/OpenTelemetryPlan/02-design-decisions.md index 58ef3abdc1..732eda3661 100644 --- a/OpenTelemetryPlan/02-design-decisions.md +++ b/OpenTelemetryPlan/02-design-decisions.md @@ -1,7 +1,7 @@ # Design Decisions > **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md) -> **Related**: [Architecture Analysis](./01-architecture-analysis.md) | [Code Samples](./04-code-samples.md) +> **Related**: [Architecture Analysis](./01-architecture-analysis.md) --- @@ -72,14 +72,10 @@ flowchart TB ### 2.2.1 OTLP/HTTP (Shipped in Phase 1b) -```cpp -// Configuration for OTLP over HTTP (the only exporter currently wired up). -namespace otlp = opentelemetry::exporter::otlp; - -otlp::OtlpHttpExporterOptions opts; -opts.url = "http://localhost:4318/v1/traces"; -opts.content_type = otlp::HttpRequestContentType::kJson; // or kBinary -``` +OTLP/HTTP is the only exporter wired up in Phase 1b. It is configured via +`OtlpHttpExporterOptions` with the collector traces endpoint +(`http://localhost:4318/v1/traces` by default) and a JSON content type +(binary protobuf is also available). ### 2.2.2 OTLP/gRPC (Future Work — Planned Upgrade) @@ -100,17 +96,9 @@ Required to land this upgrade: 4. Update the runbook and dashboards to document the alternate port and TLS settings. -Example Phase 1b+ gRPC configuration (when wired up): - -```cpp -// Configuration for OTLP over gRPC (future work). -namespace otlp = opentelemetry::exporter::otlp; - -otlp::OtlpGrpcExporterOptions opts; -opts.endpoint = ":4317"; -opts.use_ssl_credentials = true; -opts.ssl_credentials_cacert_path = "/path/to/ca.crt"; -``` +When wired up, the gRPC path will use `OtlpGrpcExporterOptions` configured with +the collector endpoint (host on port 4317), TLS credentials enabled, and a CA +certificate path. Until that work lands, `OtlpGrpcExporterOptions` is **not** used by any code path in Phase 1b through Phase 5. @@ -135,85 +123,44 @@ path in Phase 1b through Phase 5. ### 2.3.2 Complete Span Catalog -```yaml -# Transaction Spans -tx: - receive: "Transaction received from network" - validate: "Transaction signature/format validation" - process: "Full transaction processing" - relay: "Transaction relay to peers" - apply: "Apply transaction to ledger" - -# Consensus Spans -consensus: - round: "Complete consensus round" - phase: - open: "Open phase - collecting transactions" - establish: "Establish phase - reaching agreement" - accept: "Accept phase - applying consensus" - proposal: - receive: "Receive peer proposal" - send: "Send our proposal" - validation: - receive: "Receive peer validation" - send: "Send our validation" - -# RPC Spans -rpc: - request: "HTTP/WebSocket request handling" - command: - "*": "Specific RPC command (dynamic)" - -# Peer Spans -peer: - connect: "Peer connection establishment" - disconnect: "Peer disconnection" - message: - send: "Send protocol message" - receive: "Receive protocol message" - -# Ledger Spans -ledger: - acquire: "Ledger acquisition from network" - build: "Build new ledger" - validate: "Ledger validation" - close: "Close ledger" - replay: "Ledger replay executed" - delta: "Delta-based ledger acquired" - -# PathFinding Spans -pathfind: - request: "Path request initiated" - compute: "Path computation executed" - -# TxQ Spans -txq: - enqueue: "Transaction queued" - apply: "Queued transaction applied" - -# Fee/Load Spans -fee: - escalate: "Fee escalation triggered" - -# Validator Spans -validator: - list: - fetch: "UNL list fetched" - manifest: "Manifest update processed" - -# Amendment Spans -amendment: - vote: "Amendment voting executed" - -# SHAMap Spans -shamap: - sync: "State tree synchronization" - -# Job Spans -job: - enqueue: "Job added to queue" - execute: "Job execution" -``` +| Span name | Description | +| ------------------------------ | --------------------------------------- | +| `tx.receive` | Transaction received from network | +| `tx.validate` | Transaction signature/format validation | +| `tx.process` | Full transaction processing | +| `tx.relay` | Transaction relay to peers | +| `tx.apply` | Apply transaction to ledger | +| `consensus.round` | Complete consensus round | +| `consensus.phase.open` | Open phase - collecting transactions | +| `consensus.phase.establish` | Establish phase - reaching agreement | +| `consensus.phase.accept` | Accept phase - applying consensus | +| `consensus.proposal.receive` | Receive peer proposal | +| `consensus.proposal.send` | Send our proposal | +| `consensus.validation.receive` | Receive peer validation | +| `consensus.validation.send` | Send our validation | +| `rpc.request` | HTTP/WebSocket request handling | +| `rpc.command.*` | Specific RPC command (dynamic) | +| `peer.connect` | Peer connection establishment | +| `peer.disconnect` | Peer disconnection | +| `peer.message.send` | Send protocol message | +| `peer.message.receive` | Receive protocol message | +| `ledger.acquire` | Ledger acquisition from network | +| `ledger.build` | Build new ledger | +| `ledger.validate` | Ledger validation | +| `ledger.close` | Close ledger | +| `ledger.replay` | Ledger replay executed | +| `ledger.delta` | Delta-based ledger acquired | +| `pathfind.request` | Path request initiated | +| `pathfind.compute` | Path computation executed | +| `txq.enqueue` | Transaction queued | +| `txq.apply` | Queued transaction applied | +| `fee.escalate` | Fee escalation triggered | +| `validator.list.fetch` | UNL list fetched | +| `validator.manifest` | Manifest update processed | +| `amendment.vote` | Amendment voting executed | +| `shamap.sync` | State tree synchronization | +| `job.enqueue` | Job added to queue | +| `job.execute` | Job execution | ### 2.3.3 Attribute Naming Conventions @@ -256,18 +203,19 @@ examples in §2.4 below follow these rules. ### 2.4.1 Resource Attributes (Set Once at Startup) -```cpp -// Standard OpenTelemetry semantic conventions -resource::SemanticConventions::SERVICE_NAME = "xrpld" -resource::SemanticConventions::SERVICE_VERSION = BuildInfo::getVersionString() -resource::SemanticConventions::SERVICE_INSTANCE_ID = +Resource attributes identify the process and are set once at startup. They use +the standard OpenTelemetry semantic conventions plus custom dotted `xrpl.*` +keys (the dotted form is reserved for resource scope per §2.3.3). -// Custom xrpld attributes -"xrpl.network.id" = // e.g., 0 for mainnet -"xrpl.network.type" = "mainnet" | "testnet" | "devnet" | "standalone" -"xrpl.node.type" = "validator" | "stock" | "reporting" -"xrpl.node.cluster" = // If clustered -``` +| Key | Type / value | Description | +| --------------------- | ---------------------------------------------------------- | ------------------------------ | +| `service.name` | `"xrpld"` | Standard `SERVICE_NAME` | +| `service.version` | `BuildInfo::getVersionString()` | Standard `SERVICE_VERSION` | +| `service.instance.id` | node public key (base58) | Standard `SERVICE_INSTANCE_ID` | +| `xrpl.network.id` | network id (e.g. 0 for mainnet) | Network identifier | +| `xrpl.network.type` | `"mainnet"` \| `"testnet"` \| `"devnet"` \| `"standalone"` | Network kind | +| `xrpl.node.type` | `"validator"` \| `"stock"` \| `"reporting"` | Node role | +| `xrpl.node.cluster` | cluster name | Cluster name, if clustered | ### 2.4.2 Span Attributes by Category @@ -280,107 +228,107 @@ resource::SemanticConventions::SERVICE_INSTANCE_ID = #### Transaction Attributes -```cpp -"tx_hash" = string // Transaction hash (hex) -"tx_type" = string // "Payment", "OfferCreate", etc. -"tx_account" = string // Source account (redacted in prod) -"tx_sequence" = int64 // Account sequence number -"tx_fee" = int64 // Fee in drops -"tx_result" = string // "tesSUCCESS", "tecPATH_DRY", etc. -"ledger_index" = int64 // Ledger containing transaction -``` +| Key | Type | Description | +| -------------- | ------ | ------------------------------------- | +| `tx_hash` | string | Transaction hash (hex) | +| `tx_type` | string | `"Payment"`, `"OfferCreate"`, etc. | +| `tx_account` | string | Source account (redacted in prod) | +| `tx_sequence` | int64 | Account sequence number | +| `tx_fee` | int64 | Fee in drops | +| `tx_result` | string | `"tesSUCCESS"`, `"tecPATH_DRY"`, etc. | +| `ledger_index` | int64 | Ledger containing transaction | #### Consensus Attributes -```cpp -"consensus_round" = int64 // Round number -"consensus_phase" = string // "open", "establish", "accept" -"consensus_mode" = string // "proposing", "observing", etc. -"proposers" = int64 // Number of proposers -"prev_ledger_prefix" = string // Previous ledger hash prefix -"ledger_seq" = int64 // Ledger sequence -"tx_count" = int64 // Transactions in consensus set -"round_time_ms" = float64 // Round duration -``` +| Key | Type | Description | +| -------------------- | ------- | ----------------------------------- | +| `consensus_round` | int64 | Round number | +| `consensus_phase` | string | `"open"`, `"establish"`, `"accept"` | +| `consensus_mode` | string | `"proposing"`, `"observing"`, etc. | +| `proposers` | int64 | Number of proposers | +| `prev_ledger_prefix` | string | Previous ledger hash prefix | +| `ledger_seq` | int64 | Ledger sequence | +| `tx_count` | int64 | Transactions in consensus set | +| `round_time_ms` | float64 | Round duration | #### RPC Attributes -```cpp -"command" = string // Command name (per-span unique on rpc.command) -"version" = int64 // API version -"rpc_role" = string // "admin" or "user" (qualified — "role" is generic) -"params" = string // Sanitized parameters (optional) -``` +| Key | Type | Description | +| ---------- | ------ | ----------------------------------------------------- | +| `command` | string | Command name (per-span unique on `rpc.command`) | +| `version` | int64 | API version | +| `rpc_role` | string | `"admin"` or `"user"` (qualified — `role` is generic) | +| `params` | string | Sanitized parameters (optional) | #### Peer & Message Attributes -```cpp -"peer_id" = string // Peer public key (base58) -"peer_address" = string // IP:port -"peer_latency_ms" = float64 // Measured latency -"peer_cluster" = string // Cluster name if clustered -"message_type" = string // Protocol message type name -"message_size_bytes" = int64 // Message size -"message_compressed" = bool // Whether compressed -``` +| Key | Type | Description | +| -------------------- | ------- | -------------------------- | +| `peer_id` | string | Peer public key (base58) | +| `peer_address` | string | IP:port | +| `peer_latency_ms` | float64 | Measured latency | +| `peer_cluster` | string | Cluster name if clustered | +| `message_type` | string | Protocol message type name | +| `message_size_bytes` | int64 | Message size | +| `message_compressed` | bool | Whether compressed | #### Ledger & Job Attributes -```cpp -"ledger_hash" = string // Ledger hash -"ledger_index" = int64 // Ledger sequence/index -"close_time" = int64 // Close time (epoch) -"ledger_tx_count" = int64 // Transaction count -"job_type" = string // Job type name -"job_queue_ms" = float64 // Time spent in queue -"job_worker" = int64 // Worker thread ID -``` +| Key | Type | Description | +| ----------------- | ------- | --------------------- | +| `ledger_hash` | string | Ledger hash | +| `ledger_index` | int64 | Ledger sequence/index | +| `close_time` | int64 | Close time (epoch) | +| `ledger_tx_count` | int64 | Transaction count | +| `job_type` | string | Job type name | +| `job_queue_ms` | float64 | Time spent in queue | +| `job_worker` | int64 | Worker thread ID | #### PathFinding Attributes -```cpp -"pathfind_source_currency" = string // Source currency code -"pathfind_dest_currency" = string // Destination currency code -"pathfind_path_count" = int64 // Number of paths found -"pathfind_cache_hit" = bool // RippleLineCache hit -``` +| Key | Type | Description | +| -------------------------- | ------ | ------------------------- | +| `pathfind_source_currency` | string | Source currency code | +| `pathfind_dest_currency` | string | Destination currency code | +| `pathfind_path_count` | int64 | Number of paths found | +| `pathfind_cache_hit` | bool | RippleLineCache hit | #### TxQ Attributes -```cpp -"txq_queue_depth" = int64 // Current queue depth -"txq_fee_level" = int64 // Fee level of transaction -"txq_eviction_reason" = string // Why transaction was evicted -``` +| Key | Type | Description | +| --------------------- | ------ | --------------------------- | +| `txq_queue_depth` | int64 | Current queue depth | +| `txq_fee_level` | int64 | Fee level of transaction | +| `txq_eviction_reason` | string | Why transaction was evicted | #### Fee Attributes -```cpp -"fee_load_factor" = int64 // Current load factor -"fee_escalation_level" = int64 // Fee escalation multiplier -``` +| Key | Type | Description | +| ---------------------- | ----- | ------------------------- | +| `fee_load_factor` | int64 | Current load factor | +| `fee_escalation_level` | int64 | Fee escalation multiplier | #### Validator Attributes -```cpp -"validator_list_size" = int64 // UNL size -"validator_list_age_sec" = int64 // Seconds since last update -``` +| Key | Type | Description | +| ------------------------ | ----- | ------------------------- | +| `validator_list_size` | int64 | UNL size | +| `validator_list_age_sec` | int64 | Seconds since last update | #### Amendment Attributes -```cpp -"amendment_name" = string // Amendment name -"amendment_status" = string // "enabled", "vetoed", "supported" -``` +| Key | Type | Description | +| ------------------ | ------ | -------------------------------------- | +| `amendment_name` | string | Amendment name | +| `amendment_status` | string | `"enabled"`, `"vetoed"`, `"supported"` | #### SHAMap Attributes -```cpp -"shamap_type" = string // "transaction", "state", "account_state" -"shamap_missing_nodes" = int64 // Number of missing nodes during sync -"shamap_duration_ms" = float64 // Sync duration -``` +| Key | Type | Description | +| ---------------------- | ------- | --------------------------------------------- | +| `shamap_type` | string | `"transaction"`, `"state"`, `"account_state"` | +| `shamap_missing_nodes` | int64 | Number of missing nodes during sync | +| `shamap_duration_ms` | float64 | Sync duration | ### 2.4.3 Data Collection Summary @@ -433,41 +381,19 @@ The following data is explicitly **excluded** from telemetry collection: #### Collector-Level Data Protection -The OpenTelemetry Collector can be configured to hash or redact sensitive attributes before export: - -```yaml -processors: - attributes: - actions: - # Hash account addresses before storage - - key: tx_account - action: hash - # Remove IP addresses entirely - - key: peer_address - action: delete - # Redact specific fields - - key: params - action: delete -``` +The OpenTelemetry Collector can be configured (via an `attributes` processor) +to hash or redact sensitive attributes before export — for example, hashing +`tx_account`, deleting `peer_address` to drop IP addresses, and deleting +`params` to redact request parameters. #### Configuration Options for Privacy -In `xrpld.cfg`, operators can control data collection granularity: - -```ini -[telemetry] -enabled=1 - -# Disable collection of specific components -trace_transactions=1 -trace_consensus=1 -trace_rpc=1 -trace_peer=0 # Disable peer tracing (high volume) - -# Redact specific attributes -redact_account=1 # Hash account addresses before export -redact_peer_address=1 # Remove peer IP addresses -``` +In `xrpld.cfg`, operators control data collection granularity through the +`[telemetry]` section. Besides `enabled`, per-component toggles +(`trace_transactions`, `trace_consensus`, `trace_rpc`, `trace_peer` — the last +often disabled due to high volume) select which spans are emitted, and +redaction flags (`redact_account` to hash account addresses, `redact_peer_address` +to remove peer IP addresses) control SDK-level redaction before export. > **Note**: The `redact_account` configuration in `xrpld.cfg` controls SDK-level redaction before export, while collector-level filtering (see [Collector-Level Data Protection](#collector-level-data-protection) above) provides an additional defense-in-depth layer. Both can operate independently. @@ -540,15 +466,8 @@ xrpld already has two observability mechanisms. OpenTelemetry complements (not r - No parent-child relationships between events - Manual log parsing required -```json -// Example PerfLog entry -{ - "time": "2024-01-15T10:30:00.123Z", - "method": "submit", - "duration_us": 1523, - "result": "tesSUCCESS" -} -``` +A PerfLog entry is a JSON object with fields such as `time`, `method`, +`duration_us`, and `result`. #### Beast Insight (StatsD) @@ -562,12 +481,8 @@ xrpld already has two observability mechanisms. OpenTelemetry complements (not r - No causal relationships - Single-node perspective -```cpp -// Example StatsD usage in xrpld -insight.increment("rpc.submit.count"); -insight.gauge("ledger.age", age); -insight.timing("consensus.round", duration); -``` +In xrpld, Beast Insight is used through `increment` (counters), `gauge` +(point-in-time values), and `timing` (durations) calls. #### OpenTelemetry (NEW) @@ -581,13 +496,9 @@ insight.timing("consensus.round", duration); - Requires collector infrastructure - Higher complexity than logging -```cpp -// Example OpenTelemetry span -auto span = telemetry.startSpan("tx.relay"); -span->SetAttribute("tx_hash", hash); -span->SetAttribute("peer_id", peerId); -// Span automatically linked to parent via context -``` +A span is created via `startSpan` (e.g. `"tx.relay"`), annotated with +attributes such as `tx_hash` and `peer_id`, and is automatically linked to its +parent through the active context. ### 2.6.3 When to Use Each @@ -632,41 +543,13 @@ flowchart TB ### 2.6.5 Correlation with PerfLog -Trace IDs can be correlated with existing PerfLog entries for comprehensive debugging: - -```cpp -// In RPCHandler.cpp - correlate trace with PerfLog -Status doCommand(RPC::JsonContext& context, Json::Value& result) -{ - // Start OpenTelemetry span - auto span = context.app.getTelemetry().startSpan( - "rpc.command." + context.method); - - // Get trace ID for correlation - auto traceId = span->GetContext().trace_id().IsValid() - ? toHex(span->GetContext().trace_id()) - : ""; - - // Use existing PerfLog with trace correlation - auto const curId = context.app.getPerfLog().currentId(); - context.app.getPerfLog().rpcStart(context.method, curId); - - // Future: Add trace ID to PerfLog entry - // context.app.getPerfLog().setTraceId(curId, traceId); - - try { - auto ret = handler(context, result); - context.app.getPerfLog().rpcFinish(context.method, curId); - span->SetStatus(opentelemetry::trace::StatusCode::kOk); - return ret; - } catch (std::exception const& e) { - context.app.getPerfLog().rpcError(context.method, curId); - span->RecordException(e); - span->SetStatus(opentelemetry::trace::StatusCode::kError, e.what()); - throw; - } -} -``` +Trace IDs can be correlated with existing PerfLog entries for comprehensive +debugging. The design is for `RPCHandler.cpp` to start an `rpc.command.` +span alongside the existing PerfLog `rpcStart`/`rpcFinish`/`rpcError` calls, +extract the span's `trace_id` (when valid), and eventually stamp it onto the +PerfLog entry (a planned `setTraceId` hook) so logs and traces share a key. The +span status is set to OK on success or to error (recording the exception) on +failure. --- diff --git a/OpenTelemetryPlan/03-implementation-strategy.md b/OpenTelemetryPlan/03-implementation-strategy.md index 8a3eda6338..697e8af019 100644 --- a/OpenTelemetryPlan/03-implementation-strategy.md +++ b/OpenTelemetryPlan/03-implementation-strategy.md @@ -1,7 +1,7 @@ # Implementation Strategy > **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md) -> **Related**: [Code Samples](./04-code-samples.md) | [Configuration Reference](./05-configuration-reference.md) +> **Related**: [Configuration Reference](./05-configuration-reference.md) --- @@ -314,27 +314,12 @@ flowchart TD ### 3.7.3 Conditional Instrumentation -```cpp -// Compile-time feature flag -#ifndef XRPL_ENABLE_TELEMETRY -// Zero-cost when disabled -#define XRPL_TRACE_SPAN(t, n) ((void)0) -#endif - -// Runtime component filtering -if (telemetry.shouldTracePeer()) -{ - XRPL_TRACE_SPAN(telemetry, "peer.message.receive"); - // ... instrumentation -} -// No overhead when component tracing disabled -``` +Instrumentation is gated on two levels. A compile-time feature flag (`XRPL_ENABLE_TELEMETRY`) reduces the trace macros to no-ops when telemetry is built out, so disabled builds carry zero cost. At runtime, per-component guards (e.g. `shouldTracePeer()`) skip span creation for components whose tracing is turned off, incurring no overhead beyond a single boolean check. --- ## 3.8 Links to Detailed Documentation -- **[Code Samples](./04-code-samples.md)**: Complete implementation code for all components - **[Configuration Reference](./05-configuration-reference.md)**: Configuration options and collector setup - **[Implementation Phases](./06-implementation-phases.md)**: Detailed timeline and milestones @@ -482,47 +467,10 @@ If issues are discovered after deployment: ### 3.9.7 Code Change Examples -**Minimal RPC Instrumentation (Low Intrusiveness):** +**Minimal RPC Instrumentation (Low Intrusiveness):** Instrumenting an RPC handler adds roughly 3-4 lines: one macro to start the span and one or two `setAttribute` calls (command name, status). The span ends automatically via RAII, so the existing control flow — process the request, send the result — is untouched. -```cpp -// Before -void ServerHandler::onRequest(...) { - auto result = processRequest(req); - send(result); -} - -// After (only ~10 lines added) -void ServerHandler::onRequest(...) { - XRPL_TRACE_RPC(app_.getTelemetry(), "rpc.request"); // +1 line - XRPL_TRACE_SET_ATTR("command", command); // +1 line - - auto result = processRequest(req); - - XRPL_TRACE_SET_ATTR("rpc_status", status); // +1 line - send(result); -} -``` - -**Consensus Instrumentation (Medium Intrusiveness):** - -```cpp -// Before -void RCLConsensusAdaptor::startRound(...) { - // ... existing logic -} - -// After (context storage required) -void RCLConsensusAdaptor::startRound(...) { - XRPL_TRACE_CONSENSUS(app_.getTelemetry(), "consensus.round"); - XRPL_TRACE_SET_ATTR("ledger_seq", seq); - - // Store context for child spans in phase transitions - currentRoundContext_ = _xrpl_guard_->context(); // New member variable - - // ... existing logic unchanged -} -``` +**Consensus Instrumentation (Medium Intrusiveness):** Consensus is slightly more intrusive because child spans in later phase transitions need the round's context. Beyond the span-start and attribute macros, this requires storing the active context in a new member variable (`currentRoundContext_`) at round start. The existing round logic itself remains unchanged. --- -_Previous: [Design Decisions](./02-design-decisions.md)_ | _Next: [Code Samples](./04-code-samples.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_ +_Previous: [Design Decisions](./02-design-decisions.md)_ | _Next: [Configuration Reference](./05-configuration-reference.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_ diff --git a/OpenTelemetryPlan/04-code-samples.md b/OpenTelemetryPlan/04-code-samples.md deleted file mode 100644 index 497ff9d5ea..0000000000 --- a/OpenTelemetryPlan/04-code-samples.md +++ /dev/null @@ -1,1129 +0,0 @@ -# Code Samples - -> **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md) -> **Related**: [Implementation Strategy](./03-implementation-strategy.md) | [Configuration Reference](./05-configuration-reference.md) - ---- - -## 4.1 Core Interfaces - -> **OTLP** = OpenTelemetry Protocol - -### 4.1.1 Main Telemetry Interface - -```cpp -// include/xrpl/telemetry/Telemetry.h -#pragma once - -#include -#include -#include -#include - -#include -#include -#include - -namespace xrpl { -namespace telemetry { - -/** - * Main telemetry interface for OpenTelemetry integration. - * - * This class provides the primary API for distributed tracing in xrpld. - * It manages the OpenTelemetry SDK lifecycle and provides convenience - * methods for creating spans and propagating context. - */ -class Telemetry -{ -public: - /** - * Configuration for the telemetry system. - */ - struct Setup - { - bool enabled = false; - std::string serviceName = "xrpld"; - std::string serviceVersion; - std::string serviceInstanceId; // Node public key - - // Exporter configuration - std::string exporterType = "otlp_grpc"; // "otlp_grpc", "otlp_http", "none" - std::string exporterEndpoint = "localhost:4317"; - bool useTls = false; - std::string tlsCertPath; - - // Head sampling: fixed at 1.0 (sample everything), not config-driven. - // Keeps trace keep/drop decisions coherent across nodes; volume - // reduction is delegated to the collector's tail sampling. - double samplingRatio = 1.0; - - // Batch processor settings - std::uint32_t batchSize = 512; - std::chrono::milliseconds batchDelay{5000}; - std::uint32_t maxQueueSize = 2048; - - // Network attributes - std::uint32_t networkId = 0; - std::string networkType = "mainnet"; - - // Component filtering - bool traceTransactions = true; - bool traceConsensus = true; - bool traceRpc = true; - bool tracePeer = true; // High volume, enabled by default - bool traceLedger = true; - bool tracePathfind = true; - bool traceTxQ = true; - bool traceValidator = false; // Low volume, disabled by default - bool traceAmendment = false; // Very low volume, disabled by default - }; - - virtual ~Telemetry() = default; - - // ═══════════════════════════════════════════════════════════════════════ - // LIFECYCLE - // ═══════════════════════════════════════════════════════════════════════ - - /** Start the telemetry system (call after configuration) */ - virtual void start() = 0; - - /** Stop the telemetry system (flushes pending spans) */ - virtual void stop() = 0; - - /** Check if telemetry is enabled */ - virtual bool isEnabled() const = 0; - - // ═══════════════════════════════════════════════════════════════════════ - // TRACER ACCESS - // ═══════════════════════════════════════════════════════════════════════ - - /** Get the tracer for creating spans */ - virtual opentelemetry::nostd::shared_ptr - getTracer(std::string_view name = "xrpld") = 0; - - // ═══════════════════════════════════════════════════════════════════════ - // SPAN CREATION (Convenience Methods) - // ═══════════════════════════════════════════════════════════════════════ - - /** Start a new span with default options */ - virtual opentelemetry::nostd::shared_ptr - startSpan( - std::string_view name, - opentelemetry::trace::SpanKind kind = - opentelemetry::trace::SpanKind::kInternal) = 0; - - /** Start a span as child of given context */ - virtual opentelemetry::nostd::shared_ptr - startSpan( - std::string_view name, - opentelemetry::context::Context const& parentContext, - opentelemetry::trace::SpanKind kind = - opentelemetry::trace::SpanKind::kInternal) = 0; - - // ═══════════════════════════════════════════════════════════════════════ - // CONTEXT PROPAGATION - // ═══════════════════════════════════════════════════════════════════════ - - /** Serialize context for network transmission */ - virtual std::string serializeContext( - opentelemetry::context::Context const& ctx) = 0; - - /** Deserialize context from network data */ - virtual opentelemetry::context::Context deserializeContext( - std::string const& serialized) = 0; - - // ═══════════════════════════════════════════════════════════════════════ - // COMPONENT FILTERING - // ═══════════════════════════════════════════════════════════════════════ - - /** Check if transaction tracing is enabled */ - virtual bool shouldTraceTransactions() const = 0; - - /** Check if consensus tracing is enabled */ - virtual bool shouldTraceConsensus() const = 0; - - /** Check if RPC tracing is enabled */ - virtual bool shouldTraceRpc() const = 0; - - /** Check if peer message tracing is enabled */ - virtual bool shouldTracePeer() const = 0; - - /** Check if ledger tracing is enabled */ - virtual bool shouldTraceLedger() const = 0; - - /** Check if path finding tracing is enabled */ - virtual bool shouldTracePathfind() const = 0; - - /** Check if transaction queue tracing is enabled */ - virtual bool shouldTraceTxQ() const = 0; - - /** Check if validator list/manifest tracing is enabled */ - virtual bool shouldTraceValidator() const = 0; - - /** Check if amendment voting tracing is enabled */ - virtual bool shouldTraceAmendment() const = 0; -}; - -// Factory functions -std::unique_ptr -make_Telemetry( - Telemetry::Setup const& setup, - beast::Journal journal); - -Telemetry::Setup -setup_Telemetry( - Section const& section, - std::string const& nodePublicKey, - std::string const& version); - -} // namespace telemetry -} // namespace xrpl -``` - ---- - -## 4.2 RAII Span Guard - -```cpp -// include/xrpl/telemetry/SpanGuard.h -#pragma once - -#include -#include -#include - -#include -#include - -namespace xrpl { -namespace telemetry { - -/** - * RAII guard for OpenTelemetry spans. - * - * Automatically ends the span on destruction and makes it the current - * span in the thread-local context. - */ -class SpanGuard -{ - opentelemetry::nostd::shared_ptr span_; - opentelemetry::trace::Scope scope_; - -public: - /** - * Construct guard with span. - * The span becomes the current span in thread-local context. - * - * @note If span is nullptr (e.g., telemetry disabled), the guard - * becomes a no-op. All methods safely check for null before access. - */ - explicit SpanGuard( - opentelemetry::nostd::shared_ptr span) - : span_(span ? std::move(span) : nullptr) - , scope_(span_ ? opentelemetry::trace::Scope(span_) - : opentelemetry::trace::Scope( - opentelemetry::nostd::shared_ptr< - opentelemetry::trace::Span>(nullptr))) - { - } - - // Non-copyable, non-movable - SpanGuard(SpanGuard const&) = delete; - SpanGuard& operator=(SpanGuard const&) = delete; - SpanGuard(SpanGuard&&) = delete; - SpanGuard& operator=(SpanGuard&&) = delete; - - ~SpanGuard() - { - if (span_) - span_->End(); - } - - /** Access the underlying span */ - opentelemetry::trace::Span& span() { return *span_; } - opentelemetry::trace::Span const& span() const { return *span_; } - - /** Set span status to OK */ - void setOk() - { - span_->SetStatus(opentelemetry::trace::StatusCode::kOk); - } - - /** Set span status with code and description */ - void setStatus( - opentelemetry::trace::StatusCode code, - std::string_view description = "") - { - span_->SetStatus(code, std::string(description)); - } - - /** Set an attribute on the span */ - template - void setAttribute(std::string_view key, T&& value) - { - span_->SetAttribute( - opentelemetry::nostd::string_view(key.data(), key.size()), - std::forward(value)); - } - - /** Add an event to the span */ - void addEvent(std::string_view name) - { - span_->AddEvent(std::string(name)); - } - - /** Record an exception on the span */ - void recordException(std::exception const& e) - { - span_->RecordException(e); - span_->SetStatus( - opentelemetry::trace::StatusCode::kError, - e.what()); - } - - /** Get the current trace context */ - opentelemetry::context::Context context() const - { - return opentelemetry::context::RuntimeContext::GetCurrent(); - } -}; - -/** - * No-op span guard for when tracing is disabled. - * Provides the same interface but does nothing. - */ -class NullSpanGuard -{ -public: - NullSpanGuard() = default; - - void setOk() {} - void setStatus(opentelemetry::trace::StatusCode, std::string_view = "") {} - - template - void setAttribute(std::string_view, T&&) {} - - void addEvent(std::string_view) {} - void recordException(std::exception const&) {} - - /** Return a default empty context (matches SpanGuard interface) */ - opentelemetry::context::Context context() const - { - return opentelemetry::context::Context{}; - } -}; - -} // namespace telemetry -} // namespace xrpl -``` - ---- - -## 4.3 Instrumentation Macros - -```cpp -// src/xrpld/telemetry/TracingInstrumentation.h -#pragma once - -#include -#include - -namespace xrpl { -namespace telemetry { - -// ═══════════════════════════════════════════════════════════════════════════ -// INSTRUMENTATION MACROS -// ═══════════════════════════════════════════════════════════════════════════ - -#ifdef XRPL_ENABLE_TELEMETRY - -// Start a span that is automatically ended when guard goes out of scope -#define XRPL_TRACE_SPAN(telemetry, name) \ - auto _xrpl_span_ = (telemetry).startSpan(name); \ - ::xrpl::telemetry::SpanGuard _xrpl_guard_(_xrpl_span_) - -// Start a span with specific kind -#define XRPL_TRACE_SPAN_KIND(telemetry, name, kind) \ - auto _xrpl_span_ = (telemetry).startSpan(name, kind); \ - ::xrpl::telemetry::SpanGuard _xrpl_guard_(_xrpl_span_) - -// Conditional span based on component -#define XRPL_TRACE_TX(telemetry, name) \ - std::optional<::xrpl::telemetry::SpanGuard> _xrpl_guard_; \ - if ((telemetry).shouldTraceTransactions()) { \ - _xrpl_guard_.emplace((telemetry).startSpan(name)); \ - } - -#define XRPL_TRACE_CONSENSUS(telemetry, name) \ - std::optional<::xrpl::telemetry::SpanGuard> _xrpl_guard_; \ - if ((telemetry).shouldTraceConsensus()) { \ - _xrpl_guard_.emplace((telemetry).startSpan(name)); \ - } - -#define XRPL_TRACE_RPC(telemetry, name) \ - std::optional<::xrpl::telemetry::SpanGuard> _xrpl_guard_; \ - if ((telemetry).shouldTraceRpc()) { \ - _xrpl_guard_.emplace((telemetry).startSpan(name)); \ - } - -#define XRPL_TRACE_PEER(telemetry, name) \ - std::optional<::xrpl::telemetry::SpanGuard> _xrpl_guard_; \ - if ((telemetry).shouldTracePeer()) { \ - _xrpl_guard_.emplace((telemetry).startSpan(name)); \ - } - -#define XRPL_TRACE_LEDGER(telemetry, name) \ - std::optional<::xrpl::telemetry::SpanGuard> _xrpl_guard_; \ - if ((telemetry).shouldTraceLedger()) { \ - _xrpl_guard_.emplace((telemetry).startSpan(name)); \ - } - -#define XRPL_TRACE_PATHFIND(telemetry, name) \ - std::optional<::xrpl::telemetry::SpanGuard> _xrpl_guard_; \ - if ((telemetry).shouldTracePathfind()) { \ - _xrpl_guard_.emplace((telemetry).startSpan(name)); \ - } - -#define XRPL_TRACE_TXQ(telemetry, name) \ - std::optional<::xrpl::telemetry::SpanGuard> _xrpl_guard_; \ - if ((telemetry).shouldTraceTxQ()) { \ - _xrpl_guard_.emplace((telemetry).startSpan(name)); \ - } - -#define XRPL_TRACE_VALIDATOR(telemetry, name) \ - std::optional<::xrpl::telemetry::SpanGuard> _xrpl_guard_; \ - if ((telemetry).shouldTraceValidator()) { \ - _xrpl_guard_.emplace((telemetry).startSpan(name)); \ - } - -#define XRPL_TRACE_AMENDMENT(telemetry, name) \ - std::optional<::xrpl::telemetry::SpanGuard> _xrpl_guard_; \ - if ((telemetry).shouldTraceAmendment()) { \ - _xrpl_guard_.emplace((telemetry).startSpan(name)); \ - } - -// Set attribute on current span (if exists). -// Works with both std::optional (from conditional macros) -// and bare SpanGuard (from XRPL_TRACE_SPAN). Uses 'if constexpr'-like -// dispatch via a helper that checks for .has_value(). -#define XRPL_TRACE_SET_ATTR(key, value) \ - do { \ - if constexpr (requires { _xrpl_guard_.has_value(); }) { \ - if (_xrpl_guard_.has_value()) \ - _xrpl_guard_->setAttribute(key, value); \ - } else { \ - _xrpl_guard_.setAttribute(key, value); \ - } \ - } while(0) - -// Record exception on current span -#define XRPL_TRACE_EXCEPTION(e) \ - do { \ - if constexpr (requires { _xrpl_guard_.has_value(); }) { \ - if (_xrpl_guard_.has_value()) \ - _xrpl_guard_->recordException(e); \ - } else { \ - _xrpl_guard_.recordException(e); \ - } \ - } while(0) - -#else // XRPL_ENABLE_TELEMETRY not defined - -#define XRPL_TRACE_SPAN(telemetry, name) ((void)0) -#define XRPL_TRACE_SPAN_KIND(telemetry, name, kind) ((void)0) -#define XRPL_TRACE_TX(telemetry, name) ((void)0) -#define XRPL_TRACE_CONSENSUS(telemetry, name) ((void)0) -#define XRPL_TRACE_RPC(telemetry, name) ((void)0) -#define XRPL_TRACE_PEER(telemetry, name) ((void)0) -#define XRPL_TRACE_LEDGER(telemetry, name) ((void)0) -#define XRPL_TRACE_PATHFIND(telemetry, name) ((void)0) -#define XRPL_TRACE_TXQ(telemetry, name) ((void)0) -#define XRPL_TRACE_VALIDATOR(telemetry, name) ((void)0) -#define XRPL_TRACE_AMENDMENT(telemetry, name) ((void)0) -#define XRPL_TRACE_SET_ATTR(key, value) ((void)0) -#define XRPL_TRACE_EXCEPTION(e) ((void)0) - -#endif // XRPL_ENABLE_TELEMETRY - -} // namespace telemetry -} // namespace xrpl -``` - ---- - -## 4.4 Protocol Buffer Extensions - -### 4.4.1 TraceContext Message Definition - -Add to `src/xrpld/overlay/detail/ripple.proto`: - -```protobuf -// Note: xrpld uses proto2 syntax. The 'optional' keyword below is valid -// in proto2 (it is the default field rule) and is included for clarity. - -// Trace context for distributed tracing across nodes -// Uses W3C Trace Context format internally -message TraceContext { - // 16-byte trace identifier (required for valid context) - bytes trace_id = 1; - - // 8-byte span identifier of parent span - bytes span_id = 2; - - // Trace flags (bit 0 = sampled) - uint32 trace_flags = 3; - - // W3C tracestate header value for vendor-specific data - string trace_state = 4; -} - -// Extend existing messages with optional trace context -// High field numbers (1000+) to avoid conflicts - -message TMTransaction { - // ... existing fields ... - - // Optional trace context for distributed tracing - optional TraceContext trace_context = 1001; -} - -message TMProposeSet { - // ... existing fields ... - optional TraceContext trace_context = 1001; -} - -message TMValidation { - // ... existing fields ... - optional TraceContext trace_context = 1001; -} - -message TMGetLedger { - // ... existing fields ... - optional TraceContext trace_context = 1001; -} - -message TMLedgerData { - // ... existing fields ... - optional TraceContext trace_context = 1001; -} -``` - -### 4.4.2 Context Serialization/Deserialization - -```cpp -// include/xrpl/telemetry/TraceContext.h -#pragma once - -#include -#include -#include -#include -#include // Generated protobuf - -#include -#include - -namespace xrpl { -namespace telemetry { - -/** - * Utilities for trace context serialization and propagation. - */ -class TraceContextPropagator -{ -public: - /** - * Extract trace context from Protocol Buffer message. - * Returns empty context if no trace info present. - */ - static opentelemetry::context::Context - extract(protocol::TraceContext const& proto); - - /** - * Inject current trace context into Protocol Buffer message. - */ - static void - inject( - opentelemetry::context::Context const& ctx, - protocol::TraceContext& proto); - - /** - * Extract trace context from HTTP headers (for RPC). - * Supports W3C Trace Context (traceparent, tracestate). - */ - static opentelemetry::context::Context - extractFromHeaders( - std::function(std::string_view)> headerGetter); - - /** - * Inject trace context into HTTP headers (for RPC responses). - */ - static void - injectToHeaders( - opentelemetry::context::Context const& ctx, - std::function headerSetter); -}; - -// ═══════════════════════════════════════════════════════════════════════════ -// IMPLEMENTATION -// ═══════════════════════════════════════════════════════════════════════════ - -inline opentelemetry::context::Context -TraceContextPropagator::extract(protocol::TraceContext const& proto) -{ - using namespace opentelemetry::trace; - - if (proto.trace_id().size() != 16 || proto.span_id().size() != 8) - { - // Log malformed trace context for debugging. Silent failures in - // context extraction make distributed tracing issues hard to diagnose. - JLOG(j_.warn()) << "Malformed trace context: trace_id size=" - << proto.trace_id().size() - << " span_id size=" << proto.span_id().size(); - return opentelemetry::context::Context{}; - } - - // Construct TraceId and SpanId from bytes - TraceId traceId(reinterpret_cast(proto.trace_id().data())); - SpanId spanId(reinterpret_cast(proto.span_id().data())); - TraceFlags flags(static_cast(proto.trace_flags())); - - // Create SpanContext from extracted data - SpanContext spanContext(traceId, spanId, flags, /* remote = */ true); - - // DefaultSpan wraps SpanContext for use as a non-recording parent. - // This is the standard OTel C++ pattern for remote context propagation. - // DefaultSpan carries the remote SpanContext without recording any data. - auto parentCtx = opentelemetry::trace::SetSpan( - opentelemetry::context::Context{}, - opentelemetry::nostd::shared_ptr( - new DefaultSpan(spanContext))); - - return parentCtx; -} - -inline void -TraceContextPropagator::inject( - opentelemetry::context::Context const& ctx, - protocol::TraceContext& proto) -{ - using namespace opentelemetry::trace; - - // Get current span from context - auto span = GetSpan(ctx); - if (!span) - return; - - auto const& spanCtx = span->GetContext(); - if (!spanCtx.IsValid()) - return; - - // Serialize trace_id (16 bytes) - auto const& traceId = spanCtx.trace_id(); - proto.set_trace_id(traceId.Id().data(), TraceId::kSize); - - // Serialize span_id (8 bytes) - auto const& spanId = spanCtx.span_id(); - proto.set_span_id(spanId.Id().data(), SpanId::kSize); - - // Serialize flags - proto.set_trace_flags(spanCtx.trace_flags().flags()); - - // Note: tracestate not implemented yet -} - -} // namespace telemetry -} // namespace xrpl -``` - ---- - -## 4.5 Module-Specific Instrumentation - -### 4.5.1 Transaction Relay Instrumentation - -```cpp -// src/xrpld/overlay/detail/PeerImp.cpp (modified) - -#include - -void -PeerImp::handleTransaction( - std::shared_ptr const& m) -{ - // Extract trace context from incoming message - opentelemetry::context::Context parentCtx; - if (m->has_trace_context()) - { - parentCtx = telemetry::TraceContextPropagator::extract( - m->trace_context()); - } - - // Start span as child of remote span (cross-node link) - auto span = app_.getTelemetry().startSpan( - "tx.receive", - parentCtx, - opentelemetry::trace::SpanKind::kServer); - telemetry::SpanGuard guard(span); - - try - { - // Parse and validate transaction - SerialIter sit(makeSlice(m->rawtransaction())); - auto stx = std::make_shared(sit); - - // Add transaction attributes - guard.setAttribute("tx_hash", to_string(stx->getTransactionID())); - guard.setAttribute("tx_type", stx->getTxnType()); - guard.setAttribute("peer_id", remote_address_.to_string()); - - // Check if we've seen this transaction (HashRouter) - auto const [flags, suppressed] = - app_.getHashRouter().addSuppressionPeer( - stx->getTransactionID(), - id_); - - if (suppressed) - { - guard.setAttribute("suppressed", true); - guard.addEvent("tx.duplicate"); - return; // Already processing this transaction - } - - // Create child span for validation - { - auto validateSpan = app_.getTelemetry().startSpan("tx.validate"); - telemetry::SpanGuard validateGuard(validateSpan); - - auto [validity, reason] = checkTransaction(stx); - validateGuard.setAttribute("validity", - validity == Validity::Valid ? "valid" : "invalid"); - - if (validity != Validity::Valid) - { - validateGuard.setStatus( - opentelemetry::trace::StatusCode::kError, - reason); - return; - } - } - - // Relay to other peers (capture context for propagation) - auto ctx = guard.context(); - - // Create child span for relay - auto relaySpan = app_.getTelemetry().startSpan( - "tx.relay", - ctx, - opentelemetry::trace::SpanKind::kClient); - { - telemetry::SpanGuard relayGuard(relaySpan); - - // Inject context into outgoing message - protocol::TraceContext protoCtx; - telemetry::TraceContextPropagator::inject( - relayGuard.context(), protoCtx); - - // Relay to other peers - app_.getOverlay().relay( - stx->getTransactionID(), - *m, - protoCtx, // Pass trace context - exclusions); - - relayGuard.setAttribute("relay_count", - static_cast(relayCount)); - } - - guard.setOk(); - } - catch (std::exception const& e) - { - guard.recordException(e); - JLOG(journal_.warn()) << "Transaction handling failed: " << e.what(); - } -} -``` - -### 4.5.2 Consensus Instrumentation - -```cpp -// src/xrpld/app/consensus/RCLConsensus.cpp (modified) - -#include - -void -RCLConsensusAdaptor::startRound( - NetClock::time_point const& now, - RCLCxLedger::ID const& prevLedgerHash, - RCLCxLedger const& prevLedger, - hash_set const& peers, - bool proposing) -{ - XRPL_TRACE_CONSENSUS(app_.getTelemetry(), "consensus.round"); - - XRPL_TRACE_SET_ATTR("prev_ledger_prefix", to_string(prevLedgerHash)); - XRPL_TRACE_SET_ATTR("ledger_seq", - static_cast(prevLedger.seq() + 1)); - XRPL_TRACE_SET_ATTR("proposers", - static_cast(peers.size())); - XRPL_TRACE_SET_ATTR("consensus_mode", - proposing ? "proposing" : "observing"); - - // Store trace context for use in phase transitions - currentRoundContext_ = _xrpl_guard_.has_value() - ? _xrpl_guard_->context() - : opentelemetry::context::Context{}; - - // ... existing implementation ... -} - -ConsensusPhase -RCLConsensusAdaptor::phaseTransition(ConsensusPhase newPhase) -{ - // Create span for phase transition - auto span = app_.getTelemetry().startSpan( - "consensus.phase." + to_string(newPhase), - currentRoundContext_); - telemetry::SpanGuard guard(span); - - guard.setAttribute("consensus_phase", to_string(newPhase)); - guard.addEvent("phase.enter"); - - auto const startTime = std::chrono::steady_clock::now(); - - try - { - auto result = doPhaseTransition(newPhase); - - auto const duration = std::chrono::steady_clock::now() - startTime; - guard.setAttribute("consensus_phase_duration_ms", - std::chrono::duration(duration).count()); - - guard.setOk(); - return result; - } - catch (std::exception const& e) - { - guard.recordException(e); - throw; - } -} - -void -RCLConsensusAdaptor::peerProposal( - NetClock::time_point const& now, - RCLCxPeerPos const& proposal) -{ - // Extract trace context from proposal message - opentelemetry::context::Context parentCtx; - if (proposal.hasTraceContext()) - { - parentCtx = telemetry::TraceContextPropagator::extract( - proposal.traceContext()); - } - - auto span = app_.getTelemetry().startSpan( - "consensus.proposal.receive", - parentCtx, - opentelemetry::trace::SpanKind::kServer); - telemetry::SpanGuard guard(span); - - guard.setAttribute("proposer", - toBase58(TokenType::NodePublic, proposal.nodeId())); - guard.setAttribute("consensus_round", - static_cast(proposal.proposal().proposeSeq())); - - // ... existing implementation ... - - guard.setOk(); -} -``` - -### 4.5.3 RPC Handler Instrumentation - -```cpp -// src/xrpld/rpc/detail/ServerHandler.cpp (modified) - -#include - -void -ServerHandler::onRequest( - http_request_type&& req, - std::function&& send) -{ - // Extract trace context from HTTP headers (W3C Trace Context) - auto parentCtx = telemetry::TraceContextPropagator::extractFromHeaders( - [&req](std::string_view name) -> std::optional { - // Beast's find() accepts a string_view for custom header lookup - auto it = req.find(name); - if (it != req.end()) - return std::string(it->value()); - return std::nullopt; - }); - - // Start request span - auto span = app_.getTelemetry().startSpan( - "rpc.request", - parentCtx, - opentelemetry::trace::SpanKind::kServer); - telemetry::SpanGuard guard(span); - - // Add HTTP attributes - guard.setAttribute("http.method", std::string(req.method_string())); - guard.setAttribute("http.target", std::string(req.target())); - guard.setAttribute("http.user_agent", - std::string(req[boost::beast::http::field::user_agent])); - - auto const startTime = std::chrono::steady_clock::now(); - - try - { - // Parse and process request - auto const& body = req.body(); - Json::Value jv; - Json::Reader reader; - - if (!reader.parse(body, jv)) - { - guard.setStatus( - opentelemetry::trace::StatusCode::kError, - "Invalid JSON"); - sendError(send, "Invalid JSON"); - return; - } - - // Extract command name - std::string command = jv.isMember("command") - ? jv["command"].asString() - : jv.isMember("method") - ? jv["method"].asString() - : "unknown"; - - guard.setAttribute("command", command); - - // Create child span for command execution - auto cmdSpan = app_.getTelemetry().startSpan( - "rpc.command." + command); - { - telemetry::SpanGuard cmdGuard(cmdSpan); - - // Execute RPC command - auto result = processRequest(jv); - - // Record result attributes - if (result.isMember("status")) - { - cmdGuard.setAttribute("rpc_status", - result["status"].asString()); - } - - if (result["status"].asString() == "error") - { - cmdGuard.setStatus( - opentelemetry::trace::StatusCode::kError, - result.isMember("error_message") - ? result["error_message"].asString() - : "RPC error"); - } - else - { - cmdGuard.setOk(); - } - } - - auto const duration = std::chrono::steady_clock::now() - startTime; - guard.setAttribute("http.duration_ms", - std::chrono::duration(duration).count()); - - // Inject trace context into response headers - http_response_type resp; - telemetry::TraceContextPropagator::injectToHeaders( - guard.context(), - [&resp](std::string_view name, std::string_view value) { - resp.set(std::string(name), std::string(value)); - }); - - guard.setOk(); - send(std::move(resp)); - } - catch (std::exception const& e) - { - guard.recordException(e); - JLOG(journal_.error()) << "RPC request failed: " << e.what(); - sendError(send, e.what()); - } -} -``` - -### 4.5.4 JobQueue Context Propagation - -> **Architecture note**: `JobQueue` and its inner `Workers` class do not -> hold an `Application&` or `ServiceRegistry&`. They receive a -> `perf::PerfLog*` at construction. To instrument job execution, a -> `telemetry::Telemetry&` must be threaded into `JobQueue`'s constructor -> alongside the existing `PerfLog&`, or the trace context can be -> captured/restored without starting new spans inside the worker itself. -> -> The approach below captures trace context at job-creation time and -> restores it when the job executes, so that any spans created _inside_ -> the job body automatically become children of the original caller's -> trace. This requires adding a `telemetry::Telemetry&` to `JobQueue`. - -```cpp -// include/xrpl/core/JobQueue.h (modified) - -#ifdef XRPL_ENABLE_TELEMETRY -#include -#endif - -class JobQueue : private Workers::Callback -{ - // ... existing members ... - - // Telemetry reference for job execution spans (added alongside - // the existing perf::PerfLog& member). - telemetry::Telemetry& telemetry_; - -#ifdef XRPL_ENABLE_TELEMETRY - // Per-job trace context captured at addJob() time and restored - // on the worker thread when the job runs. - struct JobContext - { - opentelemetry::context::Context traceCtx; - }; -#endif - -public: - JobQueue( - int threadCount, - beast::insight::Collector::ptr const& collector, - beast::Journal journal, - Logs& logs, - perf::PerfLog& perfLog, - telemetry::Telemetry& telemetry); // New parameter - // ... -}; -``` - -```cpp -// src/libxrpl/core/detail/JobQueue.cpp (modified — processTask) - -void -JobQueue::processTask(int instance) -{ - // ... existing job dequeue logic ... - -#ifdef XRPL_ENABLE_TELEMETRY - // Restore the trace context that was captured when the job was - // enqueued. Any spans created inside the job body will become - // children of the original caller's trace. - auto token = opentelemetry::context::RuntimeContext::Attach( - job.traceContext()); - - // Start an execution span if telemetry is enabled at runtime - std::optional guard; - if (telemetry_.isEnabled()) - { - guard.emplace(telemetry_.startSpan("job.execute")); - guard->setAttribute("job_type", to_string(job.type())); - guard->setAttribute("job_worker", - static_cast(instance)); - } -#endif - - try - { - job.execute(); -#ifdef XRPL_ENABLE_TELEMETRY - if (guard) - guard->setOk(); -#endif - } - catch (std::exception const& e) - { -#ifdef XRPL_ENABLE_TELEMETRY - if (guard) - guard->recordException(e); -#endif - JLOG(journal_.error()) << "Job execution failed: " << e.what(); - } -} -``` - ---- - -## 4.6 Span Flow Visualization - -
- -```mermaid -flowchart TB - subgraph Client["External Client"] - submit["Submit TX"] - end - - subgraph NodeA["xrpld Node A"] - rpcA["rpc.request"] - cmdA["rpc.command.submit"] - txRecvA["tx.receive"] - txValA["tx.validate"] - txRelayA["tx.relay"] - end - - subgraph NodeB["xrpld Node B"] - txRecvB["tx.receive"] - txValB["tx.validate"] - txRelayB["tx.relay"] - end - - subgraph NodeC["xrpld Node C"] - txRecvC["tx.receive"] - consensusC["consensus.round"] - phaseC["consensus.phase.establish"] - end - - submit --> rpcA - rpcA --> cmdA - cmdA --> txRecvA - txRecvA --> txValA - txValA --> txRelayA - txRelayA -.->|"TraceContext"| txRecvB - txRecvB --> txValB - txValB --> txRelayB - txRelayB -.->|"TraceContext"| txRecvC - txRecvC --> consensusC - consensusC --> phaseC - - style Client fill:#334155,stroke:#1e293b,color:#fff - style NodeA fill:#1e3a8a,stroke:#172554,color:#fff - style NodeB fill:#064e3b,stroke:#022c22,color:#fff - style NodeC fill:#78350f,stroke:#451a03,color:#fff - style submit fill:#e2e8f0,stroke:#cbd5e1,color:#1e293b - style rpcA fill:#1d4ed8,stroke:#1e40af,color:#fff - style cmdA fill:#1d4ed8,stroke:#1e40af,color:#fff - style txRecvA fill:#047857,stroke:#064e3b,color:#fff - style txValA fill:#047857,stroke:#064e3b,color:#fff - style txRelayA fill:#047857,stroke:#064e3b,color:#fff - style txRecvB fill:#047857,stroke:#064e3b,color:#fff - style txValB fill:#047857,stroke:#064e3b,color:#fff - style txRelayB fill:#047857,stroke:#064e3b,color:#fff - style txRecvC fill:#047857,stroke:#064e3b,color:#fff - style consensusC fill:#fef3c7,stroke:#fde68a,color:#1e293b - style phaseC fill:#fef3c7,stroke:#fde68a,color:#1e293b -``` - -
- -**Reading the diagram:** - -- **Client / Submit TX**: An external client submits a transaction, creating the root span that initiates the trace. -- **Node A (RPC layer)**: The receiving node processes the submission through `rpc.request` and `rpc.command.submit`, then hands off to the transaction pipeline (`tx.receive` → `tx.validate` → `tx.relay`). -- **Dashed arrows (TraceContext)**: Cross-node boundaries where trace context is propagated via the protobuf protocol extension, linking spans across independent processes. -- **Node B (relay hop)**: A peer node that receives, validates, and relays the transaction further, demonstrating multi-hop propagation. -- **Node C (consensus)**: The final node where the transaction enters consensus (`consensus.round` → `consensus.phase.establish`), showing how a single client action produces an end-to-end distributed trace. - ---- - -_Previous: [Implementation Strategy](./03-implementation-strategy.md)_ | _Next: [Configuration Reference](./05-configuration-reference.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_ diff --git a/OpenTelemetryPlan/05-configuration-reference.md b/OpenTelemetryPlan/05-configuration-reference.md index 9702b1af6c..2eeca72dcb 100644 --- a/OpenTelemetryPlan/05-configuration-reference.md +++ b/OpenTelemetryPlan/05-configuration-reference.md @@ -1,7 +1,7 @@ # Configuration Reference > **Parent Document**: [OpenTelemetryPlan.md](./OpenTelemetryPlan.md) -> **Related**: [Code Samples](./04-code-samples.md) | [Implementation Phases](./06-implementation-phases.md) +> **Related**: [Implementation Phases](./06-implementation-phases.md) --- @@ -11,63 +11,7 @@ ### 5.1.1 Configuration File Section -Add to `cfg/xrpld-example.cfg`: - -```ini -# ═══════════════════════════════════════════════════════════════════════════════ -# TELEMETRY (OpenTelemetry Distributed Tracing) -# ═══════════════════════════════════════════════════════════════════════════════ -# -# Enables distributed tracing for transaction flow, consensus, and RPC calls. -# Traces are exported to an OpenTelemetry Collector using OTLP protocol. -# -# [telemetry] -# -# # Enable/disable telemetry (default: 0 = disabled) -# enabled=1 -# -# # OTLP endpoint (default: http://localhost:4318/v1/traces - OTLP/HTTP) -# # Note: only OTLP/HTTP is shipped in Phase 1b. OTLP/gRPC support is -# # planned as future work and is not yet parsed by TelemetryConfig.cpp. -# endpoint=http://localhost:4318/v1/traces -# -# # Use TLS for exporter connection (default: 0) -# use_tls=0 -# -# # Path to CA certificate for TLS (optional) -# # tls_ca_cert=/path/to/ca.crt -# -# # Head sampling is intentionally fixed at 1.0 (sample everything) and is -# # NOT configurable. A per-node head-sampling ratio would let nodes make -# # divergent keep/drop decisions for the same distributed trace, producing -# # broken/partial traces across the network. Volume reduction is delegated -# # to the collector's tail sampling instead. See Section 7.4.2. -# -# # Batch processor settings -# batch_size=512 # Spans per batch (default: 512) -# batch_delay_ms=5000 # Max delay before sending batch (default: 5000) -# max_queue_size=2048 # Max queued spans (default: 2048) -# -# # Component-specific tracing (default: all enabled except peer) -# trace_transactions=1 # Transaction relay and processing -# trace_consensus=1 # Consensus rounds and proposals -# trace_rpc=1 # RPC request handling -# trace_peer=1 # Peer messages (high volume, enabled by default) -# trace_ledger=1 # Ledger acquisition and building -# -# # Planned (not yet parsed by TelemetryConfig.cpp): -# # trace_pathfind=1 # Path computation (Phase 2) -# # trace_txq=1 # Transaction queue (Phase 3) -# # trace_validator=0 # Validator list / manifest (future) -# # trace_amendment=0 # Amendment voting (future) -# -# # Service identification (automatically detected if not specified) -# # service_name=xrpld -# # service_instance_id= - -[telemetry] -enabled=0 -``` +The authoritative `[telemetry]` example lives in `cfg/xrpld-example.cfg`. Telemetry is disabled by default (`enabled=0`); enabling it turns on distributed tracing for transaction flow, consensus, and RPC calls, with traces exported to an OpenTelemetry Collector over OTLP. Head sampling is intentionally fixed at 1.0 (sample everything) and is not configurable — per-node head-sampling would produce broken/partial distributed traces, so volume reduction is delegated to the collector's tail sampling (see Section 7.4.2). The full option reference follows. ### 5.1.2 Configuration Options Summary @@ -106,67 +50,7 @@ phases. They will be added as the corresponding subsystems are instrumented: > **TxQ** = Transaction Queue -```cpp -// src/libxrpl/telemetry/TelemetryConfig.cpp - -#include -#include - -namespace xrpl { -namespace telemetry { - -Telemetry::Setup -setup_Telemetry( - Section const& section, - std::string const& nodePublicKey, - std::string const& version) -{ - Telemetry::Setup setup; - - // Basic settings - setup.enabled = section.value_or("enabled", false); - setup.serviceName = section.value_or("service_name", "xrpld"); - setup.serviceVersion = version; - setup.serviceInstanceId = section.value_or( - "service_instance_id", nodePublicKey); - - // Exporter settings - setup.exporterType = section.value_or("exporter", "otlp_grpc"); - - if (setup.exporterType == "otlp_grpc") - setup.exporterEndpoint = section.value_or("endpoint", "localhost:4317"); - else if (setup.exporterType == "otlp_http") - setup.exporterEndpoint = section.value_or("endpoint", "localhost:4318"); - - setup.useTls = section.value_or("use_tls", false); - setup.tlsCertPath = section.value_or("tls_ca_cert", ""); - - // Head sampling is fixed at 1.0 (sample everything) and is not read from - // config — see Section 7.4.2. setup.samplingRatio stays at its 1.0 default. - - // Batch processor - setup.batchSize = section.value_or("batch_size", 512u); - setup.batchDelay = std::chrono::milliseconds{ - section.value_or("batch_delay_ms", 5000u)}; - setup.maxQueueSize = section.value_or("max_queue_size", 2048u); - - // Component filtering - setup.traceTransactions = section.value_or("trace_transactions", true); - setup.traceConsensus = section.value_or("trace_consensus", true); - setup.traceRpc = section.value_or("trace_rpc", true); - setup.tracePeer = section.value_or("trace_peer", true); - setup.traceLedger = section.value_or("trace_ledger", true); - setup.tracePathfind = section.value_or("trace_pathfind", true); - setup.traceTxQ = section.value_or("trace_txq", true); - setup.traceValidator = section.value_or("trace_validator", false); - setup.traceAmendment = section.value_or("trace_amendment", false); - - return setup; -} - -} // namespace telemetry -} // namespace xrpl -``` +The parser `setup_Telemetry()` in `src/libxrpl/telemetry/TelemetryConfig.cpp` reads the `[telemetry]` `Section` and populates a `Telemetry::Setup` struct, applying the defaults listed in Section 5.1.2 via `section.value_or(...)`. It derives `serviceInstanceId` from the node public key when not overridden, selects the exporter endpoint default by exporter type, and leaves the sampling ratio at its fixed 1.0 default (not read from config — see Section 7.4.2). --- @@ -180,84 +64,11 @@ setup_Telemetry( > constructed with an empty `serviceInstanceId` and patched via > `setServiceInstanceId()` once `setup()` has called `getNodeIdentity()`. -```cpp -// src/xrpld/app/main/Application.cpp (modified) - -#include - -class ApplicationImp : public Application, public BasicApp -{ - // ... existing members (perfLog_, etc.) ... - - // Telemetry — constructed in the member initializer list with - // an empty serviceInstanceId, patched in setup(). - std::unique_ptr telemetry_; - - // Member initializer list (excerpt): - // ... - // , telemetry_( - // telemetry::make_Telemetry( - // telemetry::setup_Telemetry( - // config_->section("telemetry"), - // "", // Updated later via setServiceInstanceId() - // BuildInfo::getVersionString()), - // logs_->journal("Telemetry"))) - // ... - - bool setup(...) override - { - // ... existing setup code ... - - nodeIdentity_ = getNodeIdentity(*this, cmdline); - - // Inject node identity into telemetry resource attributes, - // unless the user already set a custom service_instance_id. - if (!config_->section("telemetry").exists("service_instance_id")) - telemetry_->setServiceInstanceId( - toBase58(TokenType::NodePublic, nodeIdentity_->first)); - - // ... rest of setup ... - } - - void start(bool withTimers) override - { - // ... existing start code ... - telemetry_->start(); - } - - void run() override - { - // ... existing run/shutdown code ... - telemetry_->stop(); - } - - telemetry::Telemetry& - getTelemetry() override - { - return *telemetry_; - } -}; -``` +`ApplicationImp` (in `src/xrpld/app/main/Application.cpp`) owns a `std::unique_ptr telemetry_`. It is built in the member initializer list via `make_Telemetry(setup_Telemetry(...))` with an empty `serviceInstanceId`, then patched in `setup()` by calling `setServiceInstanceId()` with the Base58 node public key (unless the user supplied a custom `service_instance_id`). `start()` and `run()` forward to `telemetry_->start()` / `telemetry_->stop()`, and `getTelemetry()` returns the owned instance. ### 5.3.2 ServiceRegistry Interface Addition -```cpp -// include/xrpl/core/ServiceRegistry.h (modified) - -namespace telemetry { -class Telemetry; -} // namespace telemetry - -class ServiceRegistry -{ -public: - // ... existing virtual methods ... - - /** Get the telemetry system for distributed tracing. */ - virtual telemetry::Telemetry& - getTelemetry() = 0; -}; -``` +`include/xrpl/core/ServiceRegistry.h` gains a pure-virtual `telemetry::Telemetry& getTelemetry()` (with a forward declaration of `telemetry::Telemetry`), giving every component a uniform accessor for the tracing subsystem. > **Note:** `Application` extends `ServiceRegistry`, so `getTelemetry()` is > available on both. Components that hold a `ServiceRegistry&` (e.g. @@ -273,114 +84,11 @@ public: ### 5.4.1 Find OpenTelemetry Module -```cmake -# cmake/FindOpenTelemetry.cmake - -# Find OpenTelemetry C++ SDK -# -# This module defines: -# OpenTelemetry_FOUND - System has OpenTelemetry -# OpenTelemetry::api - API library target -# OpenTelemetry::sdk - SDK library target -# OpenTelemetry::otlp_grpc_exporter - OTLP gRPC exporter target -# OpenTelemetry::otlp_http_exporter - OTLP HTTP exporter target - -find_package(opentelemetry-cpp CONFIG QUIET) - -if(opentelemetry-cpp_FOUND) - set(OpenTelemetry_FOUND TRUE) - - # Create imported targets if not already created by config - if(NOT TARGET OpenTelemetry::api) - add_library(OpenTelemetry::api ALIAS opentelemetry-cpp::api) - endif() - if(NOT TARGET OpenTelemetry::sdk) - add_library(OpenTelemetry::sdk ALIAS opentelemetry-cpp::sdk) - endif() - if(NOT TARGET OpenTelemetry::otlp_grpc_exporter) - add_library(OpenTelemetry::otlp_grpc_exporter ALIAS - opentelemetry-cpp::otlp_grpc_exporter) - endif() -else() - # Try pkg-config fallback - find_package(PkgConfig QUIET) - if(PKG_CONFIG_FOUND) - pkg_check_modules(OTEL opentelemetry-cpp QUIET) - if(OTEL_FOUND) - set(OpenTelemetry_FOUND TRUE) - # Create imported targets from pkg-config - add_library(OpenTelemetry::api INTERFACE IMPORTED) - target_include_directories(OpenTelemetry::api INTERFACE - ${OTEL_INCLUDE_DIRS}) - endif() - endif() -endif() - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(OpenTelemetry - REQUIRED_VARS OpenTelemetry_FOUND) -``` +A `cmake/FindOpenTelemetry.cmake` module locates the OpenTelemetry C++ SDK. It first tries `find_package(opentelemetry-cpp CONFIG)`, aliasing the imported targets `OpenTelemetry::api`, `OpenTelemetry::sdk`, and `OpenTelemetry::otlp_grpc_exporter`, and falls back to `pkg-config` when no CMake config package is present. ### 5.4.2 CMakeLists.txt Changes -```cmake -# CMakeLists.txt (additions) - -# ═══════════════════════════════════════════════════════════════════════════════ -# TELEMETRY OPTIONS -# ═══════════════════════════════════════════════════════════════════════════════ - -option(XRPL_ENABLE_TELEMETRY - "Enable OpenTelemetry distributed tracing support" OFF) - -if(XRPL_ENABLE_TELEMETRY) - find_package(OpenTelemetry REQUIRED) - - # Define compile-time flag - add_compile_definitions(XRPL_ENABLE_TELEMETRY) - - message(STATUS "OpenTelemetry tracing: ENABLED") -else() - message(STATUS "OpenTelemetry tracing: DISABLED") -endif() - -# ═══════════════════════════════════════════════════════════════════════════════ -# TELEMETRY LIBRARY -# ═══════════════════════════════════════════════════════════════════════════════ - -if(XRPL_ENABLE_TELEMETRY) - add_library(xrpl_telemetry - src/libxrpl/telemetry/Telemetry.cpp - src/libxrpl/telemetry/TelemetryConfig.cpp - src/libxrpl/telemetry/TraceContext.cpp - ) - - target_include_directories(xrpl_telemetry - PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/include - ) - - target_link_libraries(xrpl_telemetry - PUBLIC - OpenTelemetry::api - OpenTelemetry::sdk - OpenTelemetry::otlp_grpc_exporter - PRIVATE - xrpl_basics - ) - - # Add to main library dependencies - target_link_libraries(xrpld PRIVATE xrpl_telemetry) -else() - # Create null implementation library - add_library(xrpl_telemetry - src/libxrpl/telemetry/NullTelemetry.cpp - ) - target_include_directories(xrpl_telemetry - PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include - ) -endif() -``` +The top-level `CMakeLists.txt` adds an `XRPL_ENABLE_TELEMETRY` option (default `OFF`). When enabled, it runs `find_package(OpenTelemetry REQUIRED)`, defines the `XRPL_ENABLE_TELEMETRY` compile flag, and builds the `xrpl_telemetry` library from the real telemetry sources linked against the OpenTelemetry targets; when disabled, it builds the same target from a no-op `NullTelemetry.cpp` so call sites compile unchanged. --- @@ -388,151 +96,15 @@ endif() > **OTLP** = OpenTelemetry Protocol | **APM** = Application Performance Monitoring +The authoritative collector config lives in the repo at `docker/telemetry/otel-collector-config.yaml` (with Tempo backend config in `docker/telemetry/tempo.yaml`). The sections below summarize the development and production shapes of that pipeline. + ### 5.5.1 Development Configuration -```yaml -# otel-collector-dev.yaml -# Minimal configuration for local development - -receivers: - otlp: - protocols: - grpc: - endpoint: 0.0.0.0:4317 - http: - endpoint: 0.0.0.0:4318 - -processors: - batch: - timeout: 1s - send_batch_size: 100 - -exporters: - # Console output for debugging - logging: - verbosity: detailed - sampling_initial: 5 - sampling_thereafter: 200 - - # Tempo for trace visualization - otlp/tempo: - endpoint: tempo:4317 - tls: - insecure: true - -service: - pipelines: - traces: - receivers: [otlp] - processors: [batch] - exporters: [logging, otlp/tempo] -``` +The development collector enables an OTLP receiver on both gRPC (`0.0.0.0:4317`) and HTTP (`0.0.0.0:4318`), a single `batch` processor (1s timeout, batch size 100), and two exporters: a `logging` exporter for console debugging and `otlp/tempo` (insecure) for trace visualization. The single `traces` pipeline wires receiver → batch → both exporters. ### 5.5.2 Production Configuration -```yaml -# otel-collector-prod.yaml -# Production configuration with filtering, sampling, and multiple backends - -receivers: - otlp: - protocols: - grpc: - endpoint: 0.0.0.0:4317 - tls: - cert_file: /etc/otel/server.crt - key_file: /etc/otel/server.key - ca_file: /etc/otel/ca.crt - -processors: - # Memory limiter to prevent OOM - memory_limiter: - check_interval: 1s - limit_mib: 1000 - spike_limit_mib: 200 - - # Batch processing for efficiency - batch: - timeout: 5s - send_batch_size: 512 - send_batch_max_size: 1024 - - # Tail-based sampling (keep errors and slow traces) - tail_sampling: - decision_wait: 10s - num_traces: 100000 - expected_new_traces_per_sec: 1000 - policies: - # Always keep error traces - - name: errors - type: status_code - status_code: - status_codes: [ERROR] - # Keep slow consensus rounds (>5s) - - name: slow-consensus - type: latency - latency: - threshold_ms: 5000 - # Keep slow RPC requests (>1s) - - name: slow-rpc - type: and - and: - and_sub_policy: - - name: rpc-spans - type: string_attribute - string_attribute: - key: command - values: [".*"] - enabled_regex_matching: true - - name: latency - type: latency - latency: - threshold_ms: 1000 - # Probabilistic sampling for the rest - - name: probabilistic - type: probabilistic - probabilistic: - sampling_percentage: 10 - - # Attribute processing - attributes: - actions: - # Hash sensitive data - - key: tx_account - action: hash - # Add deployment info - - key: deployment.environment - value: production - action: upsert - -exporters: - # Grafana Tempo for long-term storage - otlp/tempo: - endpoint: tempo.monitoring:4317 - tls: - insecure: false - ca_file: /etc/otel/tempo-ca.crt - - # Elastic APM for correlation with logs - otlp/elastic: - endpoint: apm.elastic:8200 - headers: - Authorization: "Bearer ${ELASTIC_APM_TOKEN}" - -extensions: - health_check: - endpoint: 0.0.0.0:13133 - zpages: - endpoint: 0.0.0.0:55679 - -service: - extensions: [health_check, zpages] - pipelines: - traces: - receivers: [otlp] - processors: [memory_limiter, tail_sampling, attributes, batch] - exporters: [otlp/tempo, otlp/elastic] -``` +The production collector adds TLS on the OTLP gRPC receiver and a richer processor chain: a `memory_limiter` (OOM guard), `batch` (5s timeout, size 512), `tail_sampling`, and an `attributes` processor that hashes sensitive fields (e.g. `tx_account`) and stamps `deployment.environment`. Tail sampling keeps all `ERROR` traces, slow consensus rounds (>5s) and slow RPC requests (>1s), and probabilistically samples the remainder at 10%. Exporters target Grafana Tempo (TLS) and Elastic APM; `health_check` and `zpages` extensions are enabled for operability. --- @@ -540,61 +112,7 @@ service: > **OTLP** = OpenTelemetry Protocol -```yaml -# docker-compose-telemetry.yaml -version: "3.8" - -services: - # OpenTelemetry Collector - otel-collector: - image: otel/opentelemetry-collector-contrib:0.92.0 - container_name: otel-collector - command: ["--config=/etc/otel-collector-config.yaml"] - volumes: - - ./otel-collector-dev.yaml:/etc/otel-collector-config.yaml:ro - ports: - - "4317:4317" # OTLP gRPC - - "4318:4318" # OTLP HTTP - - "13133:13133" # Health check - depends_on: - - tempo - - # Tempo for trace visualization - tempo: - image: grafana/tempo:2.6.1 - container_name: tempo - ports: - - "3200:3200" # Tempo HTTP API - - "4317" # OTLP gRPC (internal) - - # Grafana for dashboards - grafana: - image: grafana/grafana:10.2.3 - container_name: grafana - environment: - - GF_AUTH_ANONYMOUS_ENABLED=true - - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin - volumes: - - ./grafana/provisioning:/etc/grafana/provisioning:ro - - ./grafana/dashboards:/var/lib/grafana/dashboards:ro - ports: - - "3000:3000" - depends_on: - - tempo - - # Prometheus for metrics (optional, for correlation) - prometheus: - image: prom/prometheus:v2.48.1 - container_name: prometheus - volumes: - - ./prometheus.yaml:/etc/prometheus/prometheus.yml:ro - ports: - - "9090:9090" - -networks: - default: - name: xrpld-telemetry -``` +The authoritative development stack lives in the repo at `docker/telemetry/docker-compose.yml`. It brings up four services on a shared `xrpld-telemetry` network: an `otel-collector` (otel/opentelemetry-collector-contrib) exposing OTLP gRPC `4317`, OTLP HTTP `4318`, and health check `13133`; `tempo` for trace storage/visualization; `grafana` with provisioned datasources and dashboards (anonymous admin enabled); and an optional `prometheus` for metric correlation. --- @@ -661,175 +179,23 @@ Step-by-step instructions for integrating xrpld traces with Grafana. #### Tempo (Recommended) -```yaml -# grafana/provisioning/datasources/tempo.yaml -apiVersion: 1 - -datasources: - - name: Tempo - type: tempo - access: proxy - url: http://tempo:3200 - jsonData: - httpMethod: GET - tracesToLogs: - datasourceUid: loki - tags: ["service.name", "tx_hash"] - mappedTags: [{ key: "trace_id", value: "traceID" }] - mapTagNamesEnabled: true - filterByTraceID: true - serviceMap: - datasourceUid: prometheus - nodeGraph: - enabled: true - search: - hide: false - lokiSearch: - datasourceUid: loki -``` +A Tempo datasource (`grafana/provisioning/datasources/tempo.yaml`, provisioned from `docker/telemetry/grafana/`) points at `http://tempo:3200` and enables `tracesToLogs` (linking to Loki on `service.name`/`tx_hash` and mapping `trace_id` → `traceID`), `serviceMap` against Prometheus, the node graph, and Loki search. #### Elastic APM -```yaml -# grafana/provisioning/datasources/elastic-apm.yaml -apiVersion: 1 - -datasources: - - name: Elasticsearch-APM - type: elasticsearch - access: proxy - url: http://elasticsearch:9200 - database: "apm-*" - jsonData: - esVersion: "8.0.0" - timeField: "@timestamp" - logMessageField: message - logLevelField: log.level -``` +Alternatively, an Elasticsearch datasource (`grafana/provisioning/datasources/elastic-apm.yaml`) of type `elasticsearch` points at `http://elasticsearch:9200` against the `apm-*` index, using `@timestamp` as the time field and mapping the log message/level fields. ### 5.8.2 Dashboard Provisioning -```yaml -# grafana/provisioning/dashboards/dashboards.yaml -apiVersion: 1 - -providers: - - name: "xrpld-dashboards" - orgId: 1 - folder: "xrpld" - folderUid: "xrpld" - type: file - disableDeletion: false - updateIntervalSeconds: 30 - options: - path: /var/lib/grafana/dashboards/rippled -``` +A dashboard provider (`grafana/provisioning/dashboards/dashboards.yaml`) loads the `xrpld` dashboard folder from disk (`/var/lib/grafana/dashboards/rippled`), polling for changes every 30s with deletion disabled. ### 5.8.3 Example Dashboard: RPC Performance -```json -{ - "title": "xrpld RPC Performance", - "uid": "xrpld-rpc-performance", - "panels": [ - { - "title": "RPC Latency by Command", - "type": "heatmap", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && span.command != \"\"} | histogram_over_time(duration) by (span.command)" - } - ], - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 } - }, - { - "title": "RPC Error Rate", - "type": "timeseries", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && status.code=error} | rate() by (span.command)" - } - ], - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 } - }, - { - "title": "Top 10 Slowest RPC Commands", - "type": "table", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && span.command != \"\"} | avg(duration) by (span.command) | topk(10)" - } - ], - "gridPos": { "h": 8, "w": 24, "x": 0, "y": 8 } - }, - { - "title": "Recent Traces", - "type": "table", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\"}" - } - ], - "gridPos": { "h": 8, "w": 24, "x": 0, "y": 16 } - } - ] -} -``` +An example `xrpld RPC Performance` dashboard (uid `xrpld-rpc-performance`) sourced from Tempo via TraceQL provides four panels: RPC latency by command (heatmap), RPC error rate by command (timeseries), the top 10 slowest RPC commands by average duration (table), and a recent-traces table. ### 5.8.4 Example Dashboard: Transaction Tracing -```json -{ - "title": "xrpld Transaction Tracing", - "uid": "xrpld-tx-tracing", - "panels": [ - { - "title": "Transaction Throughput", - "type": "stat", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"tx.receive\"} | rate()" - } - ], - "gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 } - }, - { - "title": "Cross-Node Relay Count", - "type": "timeseries", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"tx.relay\"} | avg(span.relay_count)" - } - ], - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 } - }, - { - "title": "Transaction Validation Errors", - "type": "table", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"tx.validate\" && status.code=error}" - } - ], - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 } - } - ] -} -``` +An example `xrpld Transaction Tracing` dashboard (uid `xrpld-tx-tracing`) over Tempo provides three panels: transaction throughput (`tx.receive` rate, stat), cross-node relay count (average `span.relay_count` on `tx.relay`, timeseries), and a table of transaction validation errors (`tx.validate` with `status.code=error`). ### 5.8.5 TraceQL Query Examples @@ -861,58 +227,15 @@ To correlate OpenTelemetry traces with existing PerfLog data: **Step 1: Configure Loki to ingest PerfLog** -```yaml -# promtail-config.yaml -scrape_configs: - - job_name: xrpld-perflog - static_configs: - - targets: - - localhost - labels: - job: xrpld - __path__: /var/log/rippled/perf*.log - pipeline_stages: - - json: - expressions: - trace_id: trace_id - ledger_seq: ledger_seq - tx_hash: tx_hash - - labels: - trace_id: - ledger_seq: - tx_hash: -``` +Configure a Promtail scrape job (`promtail-config.yaml`) that tails `/var/log/rippled/perf*.log`, parses each JSON line, and promotes `trace_id`, `ledger_seq`, and `tx_hash` to Loki labels. **Step 2: Add trace_id to PerfLog entries** -Modify PerfLog to include trace_id when available: - -```cpp -// In PerfLog output, add trace_id from current span context -void logPerf(Json::Value& entry) { - auto span = opentelemetry::trace::GetSpan( - opentelemetry::context::RuntimeContext::GetCurrent()); - if (span && span->GetContext().IsValid()) { - char traceIdHex[33]; - span->GetContext().trace_id().ToLowerBase16(traceIdHex); - entry["trace_id"] = std::string(traceIdHex, 32); - } - // ... existing logging -} -``` +Modify PerfLog so its JSON output includes a `trace_id` field whenever a valid span is active: fetch the current span from the OpenTelemetry runtime context, and if its context is valid, render the trace ID as a 32-character lowercase hex string into the log entry. **Step 3: Configure Grafana trace-to-logs link** -In Tempo data source configuration, set up the derived field: - -```yaml -jsonData: - tracesToLogs: - datasourceUid: loki - tags: ["trace_id", "tx_hash"] - filterByTraceID: true - filterBySpanID: false -``` +In the Tempo datasource, set the `tracesToLogs` derived field to link to Loki on the `trace_id` and `tx_hash` tags, with `filterByTraceID: true`. ### 5.8.7 Correlation with Insight/StatsD Metrics @@ -920,46 +243,22 @@ To correlate traces with existing Beast Insight metrics: **Step 1: Export Insight metrics to Prometheus** -```yaml -# prometheus.yaml -scrape_configs: - - job_name: "xrpld-statsd" - static_configs: - - targets: ["statsd-exporter:9102"] -``` +Add a Prometheus scrape job (`prometheus.yaml`) named `xrpld-statsd` targeting the StatsD exporter at `statsd-exporter:9102`. **Step 2: Add exemplars to metrics** -OpenTelemetry SDK automatically adds exemplars (trace IDs) to metrics when using the Prometheus exporter. This links metrics spikes to specific traces. +The OpenTelemetry SDK automatically adds exemplars (trace IDs) to metrics when using the Prometheus exporter, linking metric spikes to specific traces. **Step 3: Configure Grafana metric-to-trace link** -```yaml -# In Prometheus data source -jsonData: - exemplarTraceIdDestinations: - - name: trace_id - datasourceUid: tempo -``` +In the Prometheus datasource, set `exemplarTraceIdDestinations` to map the `trace_id` exemplar to the Tempo datasource. **Step 4: Dashboard panel with exemplars** -```json -{ - "title": "RPC Latency with Trace Links", - "type": "timeseries", - "datasource": "Prometheus", - "targets": [ - { - "expr": "histogram_quantile(0.99, rate(xrpld_rpc_duration_seconds_bucket[5m]))", - "exemplar": true - } - ] -} -``` +Add a timeseries panel over Prometheus (e.g. `histogram_quantile(0.99, rate(xrpld_rpc_duration_seconds_bucket[5m]))`) with `exemplar: true` enabled. This allows clicking on metric data points to jump directly to the related trace. --- -_Previous: [Code Samples](./04-code-samples.md)_ | _Next: [Implementation Phases](./06-implementation-phases.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_ +_Previous: [Implementation Strategy](./03-implementation-strategy.md)_ | _Next: [Implementation Phases](./06-implementation-phases.md)_ | _Back to: [Overview](./OpenTelemetryPlan.md)_ diff --git a/OpenTelemetryPlan/07-observability-backends.md b/OpenTelemetryPlan/07-observability-backends.md index b8e35d91f5..a8bd897723 100644 --- a/OpenTelemetryPlan/07-observability-backends.md +++ b/OpenTelemetryPlan/07-observability-backends.md @@ -230,205 +230,47 @@ Pre-built dashboards for xrpld observability. ### 7.6.1 Consensus Health Dashboard -```json -{ - "title": "xrpld Consensus Health", - "uid": "xrpld-consensus-health", - "tags": ["xrpld", "consensus", "tracing"], - "panels": [ - { - "title": "Consensus Round Duration", - "type": "timeseries", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"consensus.round\"} | avg(duration) by (resource.service.instance.id)" - } - ], - "fieldConfig": { - "defaults": { - "unit": "ms", - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 4000 }, - { "color": "red", "value": 5000 } - ] - } - } - }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 } - }, - { - "title": "Phase Duration Breakdown", - "type": "barchart", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=~\"consensus.phase.*\"} | avg(duration) by (name)" - } - ], - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 } - }, - { - "title": "Proposers per Round", - "type": "stat", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"consensus.round\"} | avg(span.proposers)" - } - ], - "gridPos": { "h": 4, "w": 6, "x": 0, "y": 8 } - }, - { - "title": "Recent Slow Rounds (>5s)", - "type": "table", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"consensus.round\"} | duration > 5s" - } - ], - "gridPos": { "h": 8, "w": 24, "x": 0, "y": 12 } - } - ] -} -``` +A Tempo-backed dashboard (uid `xrpld-consensus-health`) with four panels, all driven by TraceQL: + +- **Consensus Round Duration** (timeseries, ms): average `consensus.round` span duration per node instance, with yellow/red thresholds at 4s/5s. +- **Phase Duration Breakdown** (barchart): average duration of `consensus.phase.*` spans grouped by span name. +- **Proposers per Round** (stat): average of the `span.proposers` attribute on `consensus.round` spans. +- **Recent Slow Rounds (>5s)** (table): `consensus.round` spans filtered to `duration > 5s`. + +The underlying TraceQL queries are listed in section 7.7.3 and used throughout this doc. ### 7.6.2 Node Overview Dashboard -```json -{ - "title": "xrpld Node Overview", - "uid": "xrpld-node-overview", - "panels": [ - { - "title": "Active Nodes", - "type": "stat", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\"} | count_over_time() by (resource.service.instance.id) | count()" - } - ], - "gridPos": { "h": 4, "w": 4, "x": 0, "y": 0 } - }, - { - "title": "Total Transactions (1h)", - "type": "stat", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"tx.receive\"} | count()" - } - ], - "gridPos": { "h": 4, "w": 4, "x": 4, "y": 0 } - }, - { - "title": "Error Rate", - "type": "gauge", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && status.code=error} | rate() / {resource.service.name=\"xrpld\"} | rate() * 100" - } - ], - "fieldConfig": { - "defaults": { - "unit": "percent", - "max": 10, - "thresholds": { - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 1 }, - { "color": "red", "value": 5 } - ] - } - } - }, - "gridPos": { "h": 4, "w": 4, "x": 8, "y": 0 } - }, - { - "title": "Service Map", - "type": "nodeGraph", - "datasource": "Tempo", - "gridPos": { "h": 12, "w": 12, "x": 12, "y": 0 } - } - ] -} -``` +A Tempo-backed dashboard (uid `xrpld-node-overview`) with four panels: + +- **Active Nodes** (stat): count of distinct `resource.service.instance.id` values seen for the `xrpld` service. +- **Total Transactions (1h)** (stat): count of `tx.receive` spans. +- **Error Rate** (gauge, percent): ratio of `status.code=error` spans to all spans, with yellow/red thresholds at 1%/5%. +- **Service Map** (nodeGraph): Tempo-generated service dependency graph. ### 7.6.3 Alert Rules -```yaml -# grafana/provisioning/alerting/rippled-alerts.yaml -apiVersion: 1 +Grafana provisions three TraceQL-based alert rules (group `xrpld-tracing-alerts`, evaluated every 1m) against the Tempo datasource: -groups: - - name: xrpld-tracing-alerts - folder: xrpld - interval: 1m - rules: - - uid: consensus-slow - title: Consensus Round Slow - condition: A - data: - - refId: A - datasourceUid: tempo - model: - queryType: traceql - query: '{resource.service.name="xrpld" && name="consensus.round"} | avg(duration) > 5s' - # Note: Verify TraceQL aggregate queries are supported by your - # Tempo version. Aggregate alerting (e.g., avg(duration)) requires - # Tempo 2.3+ with TraceQL metrics enabled. - for: 5m - annotations: - summary: Consensus rounds taking >5 seconds - description: "Consensus duration: {{ $value }}ms" - labels: - severity: warning +- **Consensus Round Slow** (warning, `for: 5m`): fires when average `consensus.round` duration exceeds 5s. - - uid: rpc-error-spike - title: RPC Error Rate Spike - condition: B - data: - - refId: B - datasourceUid: tempo - model: - queryType: traceql - query: '{resource.service.name="xrpld" && name=~"rpc.command.*" && status.code=error} | rate() > 0.05' - # Note: Verify TraceQL aggregate queries are supported by your - # Tempo version. Aggregate alerting (e.g., rate()) requires - # Tempo 2.3+ with TraceQL metrics enabled. - for: 2m - annotations: - summary: RPC error rate >5% - labels: - severity: critical + ``` + {resource.service.name="xrpld" && name="consensus.round"} | avg(duration) > 5s + ``` - - uid: tx-throughput-drop - title: Transaction Throughput Drop - condition: C - data: - - refId: C - datasourceUid: tempo - model: - queryType: traceql - query: '{resource.service.name="xrpld" && name="tx.receive"} | rate() < 10' - for: 10m - annotations: - summary: Transaction throughput below threshold - labels: - severity: warning -``` +- **RPC Error Rate Spike** (critical, `for: 2m`): fires when the error rate across `rpc.command.*` spans exceeds 5%. + + ``` + {resource.service.name="xrpld" && name=~"rpc.command.*" && status.code=error} | rate() > 0.05 + ``` + +- **Transaction Throughput Drop** (warning, `for: 10m`): fires when the `tx.receive` span rate falls below 10/s. + + ``` + {resource.service.name="xrpld" && name="tx.receive"} | rate() < 10 + ``` + +> **Note**: The first two rules use TraceQL aggregates (`avg(duration)`, `rate()`), which require Tempo 2.3+ with TraceQL metrics enabled. Verify aggregate query support in your Tempo version before provisioning. --- @@ -548,93 +390,14 @@ rate(xrpld_tx_applied_total[1m]) ### 7.7.4 Unified Dashboard Example -```json -{ - "title": "xrpld Unified Observability", - "uid": "xrpld-unified", - "panels": [ - { - "title": "Transaction Latency (Traces)", - "type": "timeseries", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"tx.receive\"} | histogram_over_time(duration)" - } - ], - "gridPos": { "h": 6, "w": 8, "x": 0, "y": 0 } - }, - { - "title": "Transaction Rate (Metrics)", - "type": "timeseries", - "datasource": "Prometheus", - "targets": [ - { - "expr": "rate(xrpld_tx_received_total[5m])", - "legendFormat": "{{ instance }}" - } - ], - "fieldConfig": { - "defaults": { - "links": [ - { - "title": "View traces", - "url": "/explore?left={\"datasource\":\"Tempo\",\"query\":\"{resource.service.name=\\\"xrpld\\\" && name=\\\"tx.receive\\\"}\"}" - } - ] - } - }, - "gridPos": { "h": 6, "w": 8, "x": 8, "y": 0 } - }, - { - "title": "Recent Logs", - "type": "logs", - "datasource": "Loki", - "targets": [ - { - "expr": "{job=\"xrpld\"} | json" - } - ], - "gridPos": { "h": 6, "w": 8, "x": 16, "y": 0 } - }, - { - "title": "Trace Search", - "type": "table", - "datasource": "Tempo", - "targets": [ - { - "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\"}" - } - ], - "fieldConfig": { - "overrides": [ - { - "matcher": { "id": "byName", "options": "traceID" }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "View trace", - "url": "/explore?left={\"datasource\":\"Tempo\",\"query\":\"${__value.raw}\"}" - }, - { - "title": "View logs", - "url": "/explore?left={\"datasource\":\"Loki\",\"query\":\"{job=\\\"xrpld\\\"} |= \\\"${__value.raw}\\\"\"}" - } - ] - } - ] - } - ] - }, - "gridPos": { "h": 12, "w": 24, "x": 0, "y": 6 } - } - ] -} -``` +A single dashboard (uid `xrpld-unified`) that ties traces, metrics, and logs together across the Tempo, Prometheus, and Loki datasources: + +- **Transaction Latency (Traces)** (timeseries, Tempo): `histogram_over_time(duration)` of `tx.receive` spans. +- **Transaction Rate (Metrics)** (timeseries, Prometheus): `rate(xrpld_tx_received_total[5m])` per instance, with a data link that opens the matching `tx.receive` traces in Tempo. +- **Recent Logs** (logs, Loki): `{job="xrpld"} | json`. +- **Trace Search** (table, Tempo): all `xrpld` traces, with per-row data links on `traceID` that jump to the trace in Tempo and to the correlated logs in Loki (`{job="xrpld"} |= ""`). + +The cross-datasource data links are what make this a single-pane debugging view; the correlation fields they rely on are listed in section 7.7.2. --- diff --git a/OpenTelemetryPlan/08-appendix.md b/OpenTelemetryPlan/08-appendix.md index 33ec8d3e02..ddff360695 100644 --- a/OpenTelemetryPlan/08-appendix.md +++ b/OpenTelemetryPlan/08-appendix.md @@ -177,7 +177,6 @@ flowchart TB | [01-architecture-analysis.md](./01-architecture-analysis.md) | xrpld architecture and trace points | | [02-design-decisions.md](./02-design-decisions.md) | SDK selection, exporters, span conventions | | [03-implementation-strategy.md](./03-implementation-strategy.md) | Directory structure, performance analysis | -| [04-code-samples.md](./04-code-samples.md) | C++ code examples for all components | | [05-configuration-reference.md](./05-configuration-reference.md) | xrpld config, CMake, Collector configs | | [06-implementation-phases.md](./06-implementation-phases.md) | Timeline, tasks, risks, success metrics | | [07-observability-backends.md](./07-observability-backends.md) | Backend selection and architecture | @@ -187,7 +186,6 @@ flowchart TB | Document | Description | | ------------------------------------ | --------------------------------------------------- | -| [POC_taskList.md](./POC_taskList.md) | Proof-of-concept telemetry integration | | [presentation.md](./presentation.md) | Presentation slides for OpenTelemetry plan overview | --- diff --git a/OpenTelemetryPlan/OpenTelemetryPlan.md b/OpenTelemetryPlan/OpenTelemetryPlan.md index 3974d79481..2836070485 100644 --- a/OpenTelemetryPlan/OpenTelemetryPlan.md +++ b/OpenTelemetryPlan/OpenTelemetryPlan.md @@ -46,7 +46,6 @@ flowchart TB subgraph impl["Implementation"] strategy["03-implementation-strategy.md"] - code["04-code-samples.md"] config["05-configuration-reference.md"] end @@ -54,7 +53,6 @@ flowchart TB phases["06-implementation-phases.md"] backends["07-observability-backends.md"] appendix["08-appendix.md"] - poc["POC_taskList.md"] end overview --> fundamentals @@ -65,12 +63,10 @@ flowchart TB fund --> arch arch --> design design --> strategy - strategy --> code - code --> config + strategy --> config config --> phases phases --> backends backends --> appendix - phases --> poc style overview fill:#1b5e20,stroke:#0d3d14,color:#fff,stroke-width:2px style fundamentals fill:#00695c,stroke:#004d40,color:#fff @@ -81,12 +77,10 @@ flowchart TB style arch fill:#0d47a1,stroke:#082f6a,color:#fff style design fill:#0d47a1,stroke:#082f6a,color:#fff style strategy fill:#bf360c,stroke:#8c2809,color:#fff - style code fill:#bf360c,stroke:#8c2809,color:#fff style config fill:#bf360c,stroke:#8c2809,color:#fff style phases fill:#4a148c,stroke:#2e0d57,color:#fff style backends fill:#4a148c,stroke:#2e0d57,color:#fff style appendix fill:#4a148c,stroke:#2e0d57,color:#fff - style poc fill:#4a148c,stroke:#2e0d57,color:#fff ``` @@ -101,12 +95,10 @@ flowchart TB | **1** | [Architecture Analysis](./01-architecture-analysis.md) | xrpld component analysis, trace points, instrumentation priorities | | **2** | [Design Decisions](./02-design-decisions.md) | SDK selection, exporters, span naming, attributes, context propagation | | **3** | [Implementation Strategy](./03-implementation-strategy.md) | Directory structure, key principles, performance optimization | -| **4** | [Code Samples](./04-code-samples.md) | C++ implementation examples for core infrastructure and key modules | | **5** | [Configuration Reference](./05-configuration-reference.md) | xrpld config, CMake integration, Collector configurations | | **6** | [Implementation Phases](./06-implementation-phases.md) | 5-phase timeline, tasks, risks, success metrics | | **7** | [Observability Backends](./07-observability-backends.md) | Backend selection guide and production architecture | | **8** | [Appendix](./08-appendix.md) | Glossary, references, version history | -| **POC** | [POC Task List](./POC_taskList.md) | Proof of concept tasks for RPC tracing end-to-end demo | --- @@ -154,21 +146,6 @@ Performance optimization strategies include head sampling fixed at 100% (intenti --- -## 4. Code Samples - -C++ implementation examples are provided for the core telemetry infrastructure and key modules: - -- `Telemetry.h` - Core interface for tracer access and span creation -- `SpanGuard.h` - RAII wrapper for automatic span lifecycle management -- `TracingInstrumentation.h` - Macros for conditional instrumentation -- Protocol Buffer extensions for trace context propagation -- Module-specific instrumentation (RPC, Consensus, P2P, JobQueue) -- Remaining modules (PathFinding, TxQ, Validator, etc.) follow the same patterns - -➡️ **[View all Code Samples](./04-code-samples.md)** - ---- - ## 5. Configuration Reference > **OTLP** = OpenTelemetry Protocol | **APM** = Application Performance Monitoring @@ -219,12 +196,4 @@ The appendix contains a glossary of OpenTelemetry and xrpld-specific terms, refe --- -## POC Task List - -A step-by-step task list for building a minimal end-to-end proof of concept that demonstrates distributed tracing in xrpld. The POC scope is limited to RPC tracing — showing request traces flowing from xrpld through an OpenTelemetry Collector into Tempo, viewable in Grafana. - -➡️ **[View POC Task List](./POC_taskList.md)** - ---- - _This document provides a comprehensive implementation plan for integrating OpenTelemetry distributed tracing into the xrpld XRP Ledger node software. For detailed information on any section, follow the links to the corresponding sub-documents._ diff --git a/OpenTelemetryPlan/POC_taskList.md b/OpenTelemetryPlan/POC_taskList.md deleted file mode 100644 index 3e2b96d86a..0000000000 --- a/OpenTelemetryPlan/POC_taskList.md +++ /dev/null @@ -1,636 +0,0 @@ -# OpenTelemetry POC Task List - -> **Goal**: Build a minimal end-to-end proof of concept that demonstrates distributed tracing in xrpld. A successful POC will show RPC request traces flowing from xrpld through an OTel Collector into Tempo, viewable in Grafana. -> -> **Scope**: RPC tracing only (highest value, lowest risk per the [CRAWL phase](./06-implementation-phases.md#6102-quick-wins-immediate-value) in the implementation phases). No cross-node P2P context propagation or consensus tracing in the POC. - -### Related Plan Documents - -| Document | Relevance to POC | -| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [00-tracing-fundamentals.md](./00-tracing-fundamentals.md) | Core concepts: traces, spans, context propagation, sampling | -| [01-architecture-analysis.md](./01-architecture-analysis.md) | RPC request flow (§1.5), key trace points (§1.6), instrumentation priority (§1.7) | -| [02-design-decisions.md](./02-design-decisions.md) | SDK selection (§2.1), exporter config (§2.2), span naming (§2.3), attribute schema (§2.4), coexistence with PerfLog/Insight (§2.6) | -| [03-implementation-strategy.md](./03-implementation-strategy.md) | Directory structure (§3.1), key principles (§3.2), performance overhead (§3.3-3.6), conditional compilation (§3.7.3), code intrusiveness (§3.9) | -| [04-code-samples.md](./04-code-samples.md) | Telemetry interface (§4.1), SpanGuard (§4.2), macros (§4.3), RPC instrumentation (§4.5.3) | -| [05-configuration-reference.md](./05-configuration-reference.md) | xrpld config (§5.1), config parser (§5.2), Application integration (§5.3), CMake (§5.4), Collector config (§5.5), Docker Compose (§5.6), Grafana (§5.8) | -| [06-implementation-phases.md](./06-implementation-phases.md) | Phase 1 core tasks (§6.2), Phase 2 RPC tasks (§6.3), quick wins (§6.10), definition of done (§6.11) | -| [07-observability-backends.md](./07-observability-backends.md) | Tempo dev setup (§7.1), Grafana dashboards (§7.6), alert rules (§7.6.3) | - ---- - -## Task 0: Docker Observability Stack Setup - -> **OTLP** = OpenTelemetry Protocol - -**Objective**: Stand up the backend infrastructure to receive, store, and display traces. - -**What to do**: - -- Create `docker/telemetry/docker-compose.yml` in the repo with three services: - 1. **OpenTelemetry Collector** (`otel/opentelemetry-collector-contrib:0.92.0`) - - Expose ports `4317` (OTLP gRPC) and `4318` (OTLP HTTP) - - Expose port `13133` (health check) - - Mount a config file `docker/telemetry/otel-collector-config.yaml` - 2. **Tempo** (`grafana/tempo:2.6.1`) - - Expose port `3200` (HTTP API) and `4317` (OTLP gRPC, internal) - 3. **Grafana** (`grafana/grafana:latest`) — optional but useful - - Expose port `3000` - - Enable anonymous admin access for local dev (`GF_AUTH_ANONYMOUS_ENABLED=true`, `GF_AUTH_ANONYMOUS_ORG_ROLE=Admin`) - - Provision Tempo as a data source via `docker/telemetry/grafana/provisioning/datasources/tempo.yaml` - -- Create `docker/telemetry/otel-collector-config.yaml`: - - ```yaml - receivers: - otlp: - protocols: - grpc: - endpoint: 0.0.0.0:4317 - http: - endpoint: 0.0.0.0:4318 - - processors: - batch: - timeout: 1s - send_batch_size: 100 - - exporters: - logging: - verbosity: detailed - otlp/tempo: - endpoint: tempo:4317 - tls: - insecure: true - - service: - pipelines: - traces: - receivers: [otlp] - processors: [batch] - exporters: [logging, otlp/tempo] - ``` - -- Create Grafana Tempo datasource provisioning file at `docker/telemetry/grafana/provisioning/datasources/tempo.yaml`: - ```yaml - apiVersion: 1 - datasources: - - name: Tempo - type: tempo - access: proxy - url: http://tempo:3200 - ``` - -**Verification**: Run `docker compose -f docker/telemetry/docker-compose.yml up -d`, then: - -- `curl http://localhost:13133` returns healthy (Collector) -- `http://localhost:3000` opens Grafana (Tempo datasource available, no traces yet) - -**Reference**: - -- [05-configuration-reference.md §5.5](./05-configuration-reference.md) — Collector config (dev YAML with Tempo exporter) -- [05-configuration-reference.md §5.6](./05-configuration-reference.md) — Docker Compose development environment -- [07-observability-backends.md §7.1](./07-observability-backends.md) — Tempo quick start and backend selection -- [05-configuration-reference.md §5.8](./05-configuration-reference.md) — Grafana datasource provisioning and dashboards - ---- - -## Task 1: Add OpenTelemetry C++ SDK Dependency - -**Objective**: Make `opentelemetry-cpp` available to the build system. - -**What to do**: - -- Edit `conanfile.py` to add `opentelemetry-cpp` as an **optional** dependency. The gRPC otel plugin flag (`"grpc/*:otel_plugin": False`) in the existing conanfile may need to remain false — we pull the OTel SDK separately. - - Add a Conan option: `with_telemetry = [True, False]` defaulting to `False` - - When `with_telemetry` is `True`, add `opentelemetry-cpp` to `self.requires()` - - Required OTel Conan components: `opentelemetry-cpp` (which bundles api, sdk, and exporters). If the package isn't in Conan Center, consider using `FetchContent` in CMake or building from source as a fallback. -- Edit `CMakeLists.txt`: - - Add option: `option(XRPL_ENABLE_TELEMETRY "Enable OpenTelemetry tracing" OFF)` - - When ON, `find_package(opentelemetry-cpp CONFIG REQUIRED)` and add compile definition `XRPL_ENABLE_TELEMETRY` - - When OFF, do nothing (zero build impact) -- Verify the build succeeds with `-DXRPL_ENABLE_TELEMETRY=OFF` (no regressions) and with `-DXRPL_ENABLE_TELEMETRY=ON` (SDK links successfully). - -**Key files**: - -- `conanfile.py` -- `CMakeLists.txt` - -**Reference**: - -- [05-configuration-reference.md §5.4](./05-configuration-reference.md) — CMake integration, `FindOpenTelemetry.cmake`, `XRPL_ENABLE_TELEMETRY` option -- [03-implementation-strategy.md §3.2](./03-implementation-strategy.md) — Key principle: zero-cost when disabled via compile-time flags -- [02-design-decisions.md §2.1](./02-design-decisions.md) — SDK selection rationale and required OTel components - ---- - -## Task 2: Create Core Telemetry Interface and NullTelemetry - -**Objective**: Define the `Telemetry` abstract interface and a no-op implementation so the rest of the codebase can reference telemetry without hard-depending on the OTel SDK. - -**What to do**: - -- Create `include/xrpl/telemetry/Telemetry.h`: - - Define `namespace xrpl::telemetry` - - Define `struct Telemetry::Setup` holding: `enabled`, `exporterEndpoint`, `samplingRatio`, `serviceName`, `serviceVersion`, `serviceInstanceId`, `traceRpc`, `traceTransactions`, `traceConsensus`, `tracePeer` - - Define abstract `class Telemetry` with: - - `virtual void start() = 0;` - - `virtual void stop() = 0;` - - `virtual bool isEnabled() const = 0;` - - `virtual nostd::shared_ptr getTracer(string_view name = "xrpld") = 0;` - - `virtual nostd::shared_ptr startSpan(string_view name, SpanKind kind = kInternal) = 0;` - - `virtual nostd::shared_ptr startSpan(string_view name, Context const& parentContext, SpanKind kind = kInternal) = 0;` - - `virtual bool shouldTraceRpc() const = 0;` - - `virtual bool shouldTraceTransactions() const = 0;` - - `virtual bool shouldTraceConsensus() const = 0;` - - Factory: `std::unique_ptr make_Telemetry(Setup const&, beast::Journal);` - - Config parser: `Telemetry::Setup setup_Telemetry(Section const&, std::string const& nodePublicKey, std::string const& version);` - -- Create `include/xrpl/telemetry/SpanGuard.h`: - - RAII guard that takes an `nostd::shared_ptr`, creates a `Scope`, and calls `span->End()` in destructor. - - Convenience: `setAttribute()`, `setOk()`, `setStatus()`, `addEvent()`, `recordException()`, `context()` - - See [04-code-samples.md](./04-code-samples.md) §4.2 for the full implementation. - -- Create `src/libxrpl/telemetry/NullTelemetry.cpp`: - - Implements `Telemetry` with all no-ops. - - `isEnabled()` returns `false`, `startSpan()` returns a noop span. - - This is used when `XRPL_ENABLE_TELEMETRY` is OFF or `enabled=0` in config. - -- Guard all OTel SDK headers behind `#ifdef XRPL_ENABLE_TELEMETRY`. The `NullTelemetry` implementation should compile without the OTel SDK present. - -**Key new files**: - -- `include/xrpl/telemetry/Telemetry.h` -- `include/xrpl/telemetry/SpanGuard.h` -- `src/libxrpl/telemetry/NullTelemetry.cpp` - -**Reference**: - -- [04-code-samples.md §4.1](./04-code-samples.md) — Full `Telemetry` interface with `Setup` struct, lifecycle, tracer access, span creation, and component filtering methods -- [04-code-samples.md §4.2](./04-code-samples.md) — Full `SpanGuard` RAII implementation and `NullSpanGuard` no-op class -- [03-implementation-strategy.md §3.1](./03-implementation-strategy.md) — Directory structure: `include/xrpl/telemetry/` for headers, `src/libxrpl/telemetry/` for implementation -- [03-implementation-strategy.md §3.7.3](./03-implementation-strategy.md) — Conditional instrumentation and zero-cost compile-time disabled pattern - ---- - -## Task 3: Implement OTel-Backed Telemetry - -> **OTLP** = OpenTelemetry Protocol - -**Objective**: Implement the real `Telemetry` class that initializes the OTel SDK, configures the OTLP exporter and batch processor, and creates tracers/spans. - -**What to do**: - -- Create `src/libxrpl/telemetry/Telemetry.cpp` (compiled only when `XRPL_ENABLE_TELEMETRY=ON`): - - `class TelemetryImpl : public Telemetry` that: - - In `start()`: creates a `TracerProvider` with: - - Resource attributes: `service.name`, `service.version`, `service.instance.id` - - An `OtlpHttpExporter` pointed at `setup.exporterEndpoint` (default `localhost:4318`) - - A `BatchSpanProcessor` with configurable batch size and delay - - A `TraceIdRatioBasedSampler` using `setup.samplingRatio` - - Sets the global `TracerProvider` - - In `stop()`: calls `ForceFlush()` then shuts down the provider - - In `startSpan()`: delegates to `getTracer()->StartSpan(name, ...)` - - `shouldTraceRpc()` etc. read from `Setup` fields - -- Create `src/libxrpl/telemetry/TelemetryConfig.cpp`: - - `setup_Telemetry()` parses the `[telemetry]` config section from `xrpld.cfg` - - Maps config keys: `enabled`, `exporter`, `endpoint`, `sampling_ratio`, `trace_rpc`, `trace_transactions`, `trace_consensus`, `trace_peer` - -- Wire `make_Telemetry()` factory: - - If `setup.enabled` is true AND `XRPL_ENABLE_TELEMETRY` is defined: return `TelemetryImpl` - - Otherwise: return `NullTelemetry` - -- Add telemetry source files to CMake. When `XRPL_ENABLE_TELEMETRY=ON`, compile `Telemetry.cpp` and `TelemetryConfig.cpp` and link against `opentelemetry-cpp::api`, `opentelemetry-cpp::sdk`, `opentelemetry-cpp::otlp_grpc_exporter`. When OFF, compile only `NullTelemetry.cpp`. - -**Key new files**: - -- `src/libxrpl/telemetry/Telemetry.cpp` -- `src/libxrpl/telemetry/TelemetryConfig.cpp` - -**Key modified files**: - -- `CMakeLists.txt` (add telemetry library target) - -**Reference**: - -- [04-code-samples.md §4.1](./04-code-samples.md) — `Telemetry` interface that `TelemetryImpl` must implement -- [05-configuration-reference.md §5.2](./05-configuration-reference.md) — `setup_Telemetry()` config parser implementation -- [02-design-decisions.md §2.2](./02-design-decisions.md) — OTLP/gRPC exporter config (endpoint, TLS options) -- [02-design-decisions.md §2.4.1](./02-design-decisions.md) — Resource attributes: `service.name`, `service.version`, `service.instance.id`, `xrpl.network.id` -- [03-implementation-strategy.md §3.4](./03-implementation-strategy.md) — Per-operation CPU costs and overhead budget for span creation -- [03-implementation-strategy.md §3.5](./03-implementation-strategy.md) — Memory overhead: static (~456 KB) and dynamic (~1.2 MB) budgets - ---- - -## Task 4: Integrate Telemetry into Application Lifecycle - -**Objective**: Wire the `Telemetry` object into the `ServiceRegistry` / `Application` so all components can access it. - -**What to do**: - -- Edit `include/xrpl/core/ServiceRegistry.h`: - - Forward-declare `namespace telemetry { class Telemetry; }` inside `namespace xrpl` - - Add pure virtual method: `virtual telemetry::Telemetry& getTelemetry() = 0;` - - (`Application` extends `ServiceRegistry`, so this is automatically available on `Application` too) - -- Edit `src/xrpld/app/main/Application.cpp` (the `ApplicationImp` class): - - Add member: `std::unique_ptr telemetry_;` - - In the member initializer list, construct telemetry with an empty - `serviceInstanceId` (node identity is not yet known): - ```cpp - , telemetry_( - telemetry::make_Telemetry( - telemetry::setup_Telemetry( - config_->section("telemetry"), - "", // Updated later via setServiceInstanceId() - BuildInfo::getVersionString()), - logs_->journal("Telemetry"))) - ``` - - In `setup()`, after `nodeIdentity_` is resolved, inject the node - public key as the service instance ID: - ```cpp - if (!config_->section("telemetry").exists("service_instance_id")) - telemetry_->setServiceInstanceId( - toBase58(TokenType::NodePublic, nodeIdentity_->first)); - ``` - - In `start()`: call `telemetry_->start()` - - In `run()` (shutdown path): call `telemetry_->stop()` (to flush pending spans) - - Implement `getTelemetry()` override: return `*telemetry_` - -- Add `[telemetry]` section to the example config `cfg/xrpld-example.cfg`: - ```ini - # [telemetry] - # enabled=1 - # endpoint=http://localhost:4318/v1/traces - # sampling_ratio=1.0 - # trace_rpc=1 - ``` - -> **Access patterns**: Components holding `ServiceRegistry&` (e.g. -> `NetworkOPsImp`) call `registry_.get().getTelemetry()`. Components -> holding `Application&` (e.g. `ServerHandler`, `PeerImp`, -> `RCLConsensusAdaptor`) call `app_.getTelemetry()` directly. Both -> resolve to the same `Telemetry` instance. - -**Key modified files**: - -- `include/xrpl/core/ServiceRegistry.h` -- `src/xrpld/app/main/Application.cpp` -- `cfg/xrpld-example.cfg` (example config) - -**Reference**: - -- [05-configuration-reference.md §5.3](./05-configuration-reference.md) — `ApplicationImp` changes: member declaration, constructor init, `start()`/`stop()` wiring, `getTelemetry()` override -- [05-configuration-reference.md §5.1](./05-configuration-reference.md) — `[telemetry]` config section format and all option defaults -- [03-implementation-strategy.md §3.9.2](./03-implementation-strategy.md) — File impact assessment: `Application.cpp` ~15 lines added, ~3 changed (Low risk) - ---- - -## Task 5: Create Instrumentation Macros - -**Objective**: Define convenience macros that make instrumenting code one-liners, and that compile to zero-cost no-ops when telemetry is disabled. - -**What to do**: - -- Create `src/xrpld/telemetry/TracingInstrumentation.h`: - - When `XRPL_ENABLE_TELEMETRY` is defined: - - ```cpp - #define XRPL_TRACE_SPAN(telemetry, name) \ - auto _xrpl_span_ = (telemetry).startSpan(name); \ - ::xrpl::telemetry::SpanGuard _xrpl_guard_(_xrpl_span_) - - #define XRPL_TRACE_RPC(telemetry, name) \ - std::optional<::xrpl::telemetry::SpanGuard> _xrpl_guard_; \ - if ((telemetry).shouldTraceRpc()) { \ - _xrpl_guard_.emplace((telemetry).startSpan(name)); \ - } - - #define XRPL_TRACE_SET_ATTR(key, value) \ - if (_xrpl_guard_.has_value()) { \ - _xrpl_guard_->setAttribute(key, value); \ - } - - #define XRPL_TRACE_EXCEPTION(e) \ - if (_xrpl_guard_.has_value()) { \ - _xrpl_guard_->recordException(e); \ - } - ``` - - - When `XRPL_ENABLE_TELEMETRY` is NOT defined, all macros expand to `((void)0)` - -**Key new file**: - -- `src/xrpld/telemetry/TracingInstrumentation.h` - -**Reference**: - -- [04-code-samples.md §4.3](./04-code-samples.md) — Full macro definitions for `XRPL_TRACE_SPAN`, `XRPL_TRACE_RPC`, `XRPL_TRACE_CONSENSUS`, `XRPL_TRACE_SET_ATTR`, `XRPL_TRACE_EXCEPTION` with both enabled and disabled branches -- [03-implementation-strategy.md §3.7.3](./03-implementation-strategy.md) — Conditional instrumentation pattern: compile-time `#ifndef` and runtime `shouldTrace*()` checks -- [03-implementation-strategy.md §3.9.7](./03-implementation-strategy.md) — Before/after code examples showing minimal intrusiveness (~1-3 lines per instrumentation point) - ---- - -## Task 6: Instrument RPC ServerHandler - -> **WS** = WebSocket - -**Objective**: Add tracing to the HTTP RPC entry point so every incoming RPC request creates a span. - -**What to do**: - -- Edit `src/xrpld/rpc/detail/ServerHandler.cpp`: - - `#include` the `TracingInstrumentation.h` header - - In `ServerHandler::onRequest(Session& session)`: - - At the top of the method, add: `XRPL_TRACE_RPC(app_.getTelemetry(), "rpc.request");` - - After the RPC command name is extracted, set attribute: `XRPL_TRACE_SET_ATTR("command", command);` - - After the response status is known, set: `XRPL_TRACE_SET_ATTR("http.status_code", static_cast(statusCode));` - - Wrap error paths with: `XRPL_TRACE_EXCEPTION(e);` - - In `ServerHandler::processRequest(...)`: - - Add a child span: `XRPL_TRACE_RPC(app_.getTelemetry(), "rpc.process");` - - Set method attribute: `XRPL_TRACE_SET_ATTR("rpc_method", request_method);` - - In `ServerHandler::onWSMessage(...)` (WebSocket path): - - Add: `XRPL_TRACE_RPC(app_.getTelemetry(), "rpc.ws.message");` - -- The goal is to see spans like: - ``` - rpc.request - └── rpc.process - ``` - in Tempo/Grafana for every HTTP RPC call. - -**Key modified file**: - -- `src/xrpld/rpc/detail/ServerHandler.cpp` (~15-25 lines added) - -**Reference**: - -- [04-code-samples.md §4.5.3](./04-code-samples.md) — Complete `ServerHandler::onRequest()` instrumented code sample with W3C header extraction, span creation, attribute setting, and error handling -- [01-architecture-analysis.md §1.5](./01-architecture-analysis.md) — RPC request flow diagram: HTTP request -> attributes -> jobqueue.enqueue -> rpc.command -> response -- [01-architecture-analysis.md §1.6](./01-architecture-analysis.md) — Key trace points table: `rpc.request` in `ServerHandler.cpp::onRequest()` (Priority: High) -- [02-design-decisions.md §2.3](./02-design-decisions.md) — Span naming convention: `rpc.request`, `rpc.command.*` -- [02-design-decisions.md §2.4.2](./02-design-decisions.md) — RPC span attributes: `command`, `version`, `rpc_role`, `params` -- [03-implementation-strategy.md §3.9.2](./03-implementation-strategy.md) — File impact: `ServerHandler.cpp` ~40 lines added, ~10 changed (Low risk) - ---- - -## Task 7: Instrument RPC Command Execution - -**Objective**: Add per-command tracing inside the RPC handler so each command (e.g., `submit`, `account_info`, `server_info`) gets its own child span. - -**What to do**: - -- Edit `src/xrpld/rpc/detail/RPCHandler.cpp`: - - `#include` the `TracingInstrumentation.h` header - - In `doCommand(RPC::JsonContext& context, Json::Value& result)`: - - At the top: `XRPL_TRACE_RPC(context.app.getTelemetry(), "rpc.command." + context.method);` - - Set attributes: - - `XRPL_TRACE_SET_ATTR("command", context.method);` - - `XRPL_TRACE_SET_ATTR("version", static_cast(context.apiVersion));` - - `XRPL_TRACE_SET_ATTR("rpc_role", (context.role == Role::ADMIN) ? "admin" : "user");` - - On success: `XRPL_TRACE_SET_ATTR("rpc_status", "success");` - - On error: `XRPL_TRACE_SET_ATTR("rpc_status", "error");` and set the error message - -- After this, traces in Tempo/Grafana should look like: - ``` - rpc.request (command=account_info) - └── rpc.process - └── rpc.command.account_info (version=2, rpc_role=user, rpc_status=success) - ``` - -**Key modified file**: - -- `src/xrpld/rpc/detail/RPCHandler.cpp` (~15-20 lines added) - -**Reference**: - -- [04-code-samples.md §4.5.3](./04-code-samples.md) — `ServerHandler::onRequest()` code sample (includes child span pattern for `rpc.command.*`) -- [02-design-decisions.md §2.3](./02-design-decisions.md) — Span naming: `rpc.command.*` pattern with dynamic command name (e.g., `rpc.command.server_info`) -- [02-design-decisions.md §2.4.2](./02-design-decisions.md) — RPC attribute schema: `command`, `version`, `rpc_role`, `rpc_status` -- [01-architecture-analysis.md §1.6](./01-architecture-analysis.md) — Key trace points table: `rpc.command.*` in `RPCHandler.cpp::doCommand()` (Priority: High) -- [02-design-decisions.md §2.6.5](./02-design-decisions.md) — Correlation with PerfLog: how `doCommand()` can link trace_id with existing PerfLog entries -- [03-implementation-strategy.md §3.4.4](./03-implementation-strategy.md) — RPC request overhead budget: ~1.75 μs total per request - ---- - -## Task 8: Build, Run, and Verify End-to-End - -> **OTLP** = OpenTelemetry Protocol - -**Objective**: Prove the full pipeline works: xrpld emits traces -> OTel Collector receives them -> Tempo stores them for Grafana visualization. - -**What to do**: - -1. **Start the Docker stack**: - - ```bash - docker compose -f docker/telemetry/docker-compose.yml up -d - ``` - - Verify Collector health: `curl http://localhost:13133` - -2. **Build xrpld with telemetry**: - - ```bash - # Adjust for your actual build workflow - conan install . --build=missing -o with_telemetry=True - cmake --preset default -DXRPL_ENABLE_TELEMETRY=ON - cmake --build --preset default - ``` - -3. **Configure xrpld**: - Add to `xrpld.cfg` (or your local test config): - - ```ini - [telemetry] - enabled=1 - endpoint=localhost:4317 - sampling_ratio=1.0 - trace_rpc=1 - ``` - -4. **Start xrpld** in standalone mode: - - ```bash - ./rippled --conf xrpld.cfg -a --start - ``` - -5. **Generate RPC traffic**: - - ```bash - # server_info - curl -s -X POST http://localhost:5005 \ - -H "Content-Type: application/json" \ - -d '{"method":"server_info","params":[{}]}' - - # ledger - curl -s -X POST http://localhost:5005 \ - -H "Content-Type: application/json" \ - -d '{"method":"ledger","params":[{"ledger_index":"current"}]}' - - # account_info (will error in standalone, that's fine — we trace errors too) - curl -s -X POST http://localhost:5005 \ - -H "Content-Type: application/json" \ - -d '{"method":"account_info","params":[{"account":"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"}]}' - ``` - -6. **Verify in Grafana (Tempo)**: - - Open `http://localhost:3000` - - Navigate to Explore → select Tempo datasource - - Search for service `xrpld` - - Confirm you see traces with spans: `rpc.request` -> `rpc.process` -> `rpc.command.server_info` - - Click into a trace and verify attributes: `command`, `rpc_status`, `version` - -7. **Verify zero-overhead when disabled**: - - Rebuild with `XRPL_ENABLE_TELEMETRY=OFF`, or set `enabled=0` in config - - Run the same RPC calls - - Confirm no new traces appear and no errors in xrpld logs - -**Verification Checklist**: - -- [ ] Docker stack starts without errors -- [ ] xrpld builds with `-DXRPL_ENABLE_TELEMETRY=ON` -- [ ] xrpld starts and connects to OTel Collector (check xrpld logs for telemetry messages) -- [ ] Traces appear in Grafana/Tempo under service "xrpld" -- [ ] Span hierarchy is correct (parent-child relationships) -- [ ] Span attributes are populated (`command`, `rpc_status`, etc.) -- [ ] Error spans show error status and message -- [ ] Building with `XRPL_ENABLE_TELEMETRY=OFF` produces no regressions -- [ ] Setting `enabled=0` at runtime produces no traces and no errors - -**Reference**: - -- [06-implementation-phases.md §6.11.1](./06-implementation-phases.md) — Phase 1 definition of done: SDK compiles, runtime toggle works, span creation verified in Tempo, config validation passes -- [06-implementation-phases.md §6.11.2](./06-implementation-phases.md#6112-phase-2-rpc-tracing) — Phase 2 definition of done: 100% RPC coverage, traceparent propagation, <1ms p99 overhead, dashboard deployed -- [06-implementation-phases.md §6.8](./06-implementation-phases.md) — Success metrics: trace coverage >95%, CPU overhead <3%, memory <5 MB, latency impact <2% -- [03-implementation-strategy.md §3.9.5](./03-implementation-strategy.md) — Backward compatibility: config optional, protocol unchanged, `XRPL_ENABLE_TELEMETRY=OFF` produces identical binary -- [01-architecture-analysis.md §1.8](./01-architecture-analysis.md) — Observable outcomes: what traces, metrics, and dashboards to expect - ---- - -## Task 9: Document POC Results and Next Steps - -> **OTLP** = OpenTelemetry Protocol | **WS** = WebSocket - -**Objective**: Capture findings, screenshots, and remaining work for the team. - -**What to do**: - -- Take screenshots of Grafana/Tempo showing: - - The service list with "xrpld" - - A trace with the full span tree - - Span detail view showing attributes -- Document any issues encountered (build issues, SDK quirks, missing attributes) -- Note performance observations (build time impact, any noticeable runtime overhead) -- Write a short summary of what the POC proves and what it doesn't cover yet: - - **Proves**: OTel SDK integrates with xrpld, OTLP export works, RPC traces visible - - **Doesn't cover**: Cross-node P2P context propagation, consensus tracing, protobuf trace context, W3C traceparent header extraction, tail-based sampling, production deployment -- Outline next steps (mapping to the full plan phases): - - [Phase 2](./06-implementation-phases.md) completion: [W3C header extraction](./02-design-decisions.md) (§2.5), WebSocket tracing, all [RPC handlers](./01-architecture-analysis.md) (§1.6) - - [Phase 3](./06-implementation-phases.md): [Protobuf `TraceContext` message](./04-code-samples.md) (§4.4), [transaction relay tracing](./04-code-samples.md) (§4.5.1) across nodes - - [Phase 4](./06-implementation-phases.md): [Consensus round and phase tracing](./04-code-samples.md) (§4.5.2) - - [Phase 5](./06-implementation-phases.md): [Production collector config](./05-configuration-reference.md) (§5.5.2), [Grafana dashboards](./07-observability-backends.md) (§7.6), [alerting](./07-observability-backends.md) (§7.6.3) - -**Reference**: - -- [06-implementation-phases.md §6.1](./06-implementation-phases.md) — Full 5-phase timeline overview and Gantt chart -- [06-implementation-phases.md §6.10](./06-implementation-phases.md) — Crawl-Walk-Run strategy: POC is the CRAWL phase, next steps are WALK and RUN -- [06-implementation-phases.md §6.12](./06-implementation-phases.md) — Recommended implementation order (14 steps across 9 weeks) -- [03-implementation-strategy.md §3.9](./03-implementation-strategy.md) — Code intrusiveness assessment and risk matrix for each remaining component -- [07-observability-backends.md §7.2](./07-observability-backends.md) — Production backend selection (Tempo, Elastic APM, Honeycomb, Datadog) -- [02-design-decisions.md §2.5](./02-design-decisions.md) — Context propagation design: W3C HTTP headers, protobuf P2P, JobQueue internal -- [00-tracing-fundamentals.md](./00-tracing-fundamentals.md) — Reference for team onboarding on distributed tracing concepts - ---- - -## Summary - -| Task | Description | New Files | Modified Files | Depends On | -| ---- | ------------------------------------ | --------- | -------------- | ---------- | -| 0 | Docker observability stack | 4 | 0 | — | -| 1 | OTel C++ SDK dependency | 0 | 2 | — | -| 2 | Core Telemetry interface + NullImpl | 3 | 0 | 1 | -| 3 | OTel-backed Telemetry implementation | 2 | 1 | 1, 2 | -| 4 | Application lifecycle integration | 0 | 3 | 2, 3 | -| 5 | Instrumentation macros | 1 | 0 | 2 | -| 6 | Instrument RPC ServerHandler | 0 | 1 | 4, 5 | -| 7 | Instrument RPC command execution | 0 | 1 | 4, 5 | -| 8 | End-to-end verification | 0 | 0 | 0-7 | -| 9 | Document results and next steps | 1 | 0 | 8 | - -**Parallel work**: Tasks 0 and 1 can run in parallel. Tasks 2 and 5 have no dependency on each other. Tasks 6 and 7 can be done in parallel once Tasks 4 and 5 are complete. - ---- - -## Next Steps (Post-POC) - -> **OTLP** = OpenTelemetry Protocol | **WS** = WebSocket - -### Metrics Pipeline for Grafana Dashboards - -The current POC exports **traces only**. Grafana's Explore view can query Tempo for individual traces, but time-series charts (latency histograms, request throughput, error rates) require a **metrics pipeline**. To enable this: - -1. **Add a `spanmetrics` connector** to the OTel Collector config that derives RED metrics (Rate, Errors, Duration) from trace spans automatically: - - ```yaml - connectors: - spanmetrics: - histogram: - explicit: - buckets: [1ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 5s] - dimensions: - - name: command - - name: rpc_status - - exporters: - prometheus: - endpoint: 0.0.0.0:8889 - - service: - pipelines: - traces: - receivers: [otlp] - processors: [batch] - exporters: [debug, otlp/tempo, spanmetrics] - metrics: - receivers: [spanmetrics] - exporters: [prometheus] - ``` - -2. **Add Prometheus** to the Docker Compose stack to scrape the collector's metrics endpoint. - -3. **Add Prometheus as a Grafana datasource** and build dashboards for: - - RPC request latency (p50/p95/p99) by command - - RPC throughput (requests/sec) by command - - Error rate by command - - Span duration distribution - -### Additional Instrumentation - -- **W3C `traceparent` header extraction** in `ServerHandler` to support cross-service context propagation from external callers -- **WebSocket RPC tracing** in `ServerHandler::onWSMessage()` -- **Transaction relay tracing** across nodes using protobuf `TraceContext` messages -- **Consensus round and phase tracing** for validator coordination visibility -- **Ledger close tracing** to measure close-to-validated latency - -### Production Hardening - -- **Tail-based sampling** in the OTel Collector to reduce volume while retaining error/slow traces -- **TLS configuration** for the OTLP exporter in production deployments -- **Resource limits** on the batch processor queue to prevent unbounded memory growth -- **Health monitoring** for the telemetry pipeline itself (collector lag, export failures) - -### POC Lessons Learned - -Issues encountered during POC implementation that inform future work: - -| Issue | Resolution | Impact on Future Work | -| -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------- | -| Conan lockfile rejected `opentelemetry-cpp/1.18.0` | Used `--lockfile=""` to bypass | Lockfile must be regenerated when adding new dependencies | -| Conan package only builds OTLP HTTP exporter, not gRPC | Switched from gRPC to HTTP exporter (`localhost:4318/v1/traces`) | HTTP exporter is the default; gRPC requires custom Conan profile | -| CMake target `opentelemetry-cpp::api` etc. don't exist in Conan package | Use umbrella target `opentelemetry-cpp::opentelemetry-cpp` | Conan targets differ from upstream CMake targets | -| OTel Collector `logging` exporter deprecated | Renamed to `debug` exporter | Use `debug` in all collector configs going forward | -| Macro parameter `telemetry` collided with `::xrpl::telemetry::` namespace | Renamed macro params to `_tel_obj_`, `_span_name_` | Avoid common words as macro parameter names | -| `opentelemetry::trace::Scope` creates new context on move | Store scope as member, create once in constructor | SpanGuard move semantics need care with Scope lifecycle | -| `TracerProviderFactory::Create` returns `unique_ptr`, not `nostd::shared_ptr` | Use `std::shared_ptr` member, wrap in `nostd::shared_ptr` for global provider | OTel SDK factory return types don't match API provider types | From 134a24d5bc885c25f83f21fbd0ea31758f026d16 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:26:38 +0100 Subject: [PATCH 21/30] ci: Add OpenTelemetry span-attribute naming check (phase 1c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add check_otel_naming.py and wire it into on-pr.yml so every PR validates that span-attribute names stay consistent across the code, collector, Tempo, dashboards, and docs. - The valid key set is derived dynamically from the *SpanNames.h constants and the resource attributes the code registers in Telemetry.cpp — no hardcoded allowlist to drift. - Each rule is presence-gated: it runs only when the file it needs is in the tree, so the check is correct whether telemetry changes land in one PR or several (the collector/Tempo/dashboard/runbook layers arrive in later phases). - Rule A flags dotted span-attribute keys; Rule F flags string-literal attribute keys and span-name arguments (values may be runtime data). - stdlib-only, mirroring the levelization check (bare `python`, no pip step). - Telemetry.h / SpanGuard.h @code examples now use *SpanNames.h constants so the strict literal check passes. - CONTRIBUTING.md documents the check and how to run it locally. Co-Authored-By: Claude Opus 4.8 --- .github/scripts/otel-naming/README.md | 53 ++ .../scripts/otel-naming/check_otel_naming.py | 568 ++++++++++++++++++ .github/workflows/on-pr.yml | 8 + .../workflows/reusable-check-otel-naming.yml | 28 + CONTRIBUTING.md | 20 +- include/xrpl/telemetry/SpanGuard.h | 6 +- include/xrpl/telemetry/Telemetry.h | 17 +- 7 files changed, 688 insertions(+), 12 deletions(-) create mode 100644 .github/scripts/otel-naming/README.md create mode 100644 .github/scripts/otel-naming/check_otel_naming.py create mode 100644 .github/workflows/reusable-check-otel-naming.yml diff --git a/.github/scripts/otel-naming/README.md b/.github/scripts/otel-naming/README.md new file mode 100644 index 0000000000..a510f90dfb --- /dev/null +++ b/.github/scripts/otel-naming/README.md @@ -0,0 +1,53 @@ +# OTel naming-consistency check + +`check_otel_naming.py` enforces the OpenTelemetry span-attribute naming +convention documented in +[CONTRIBUTING.md](../../../CONTRIBUTING.md#telemetry-span-attribute-naming) +across every layer of the telemetry pipeline. The `*SpanNames.h` constants are +the single source of truth (L1); every other layer must agree with them. + +## Running locally + +``` +python .github/scripts/otel-naming/check_otel_naming.py +``` + +It takes no arguments, can be run from any directory inside the repo, and uses +only the Python standard library (no `pip install`, matching the levelization +check). A non-zero exit code means a violation was found; the output lists each +violation as `RULE | location | token | expected`. + +## What it checks + +The valid key set is **derived dynamically from the OTel code** — there is no +hardcoded allowlist: + +- **L1 keys** come from the `namespace attr { ... }` blocks of every + `*SpanNames.h`, resolving the `makeStr("x")` / `join(seg::a, seg::b)` DSL + (cross-file, so `join(seg::rpc, ...)` resolves `seg::rpc` from the base + `SpanNames.h`). +- **Legitimate dotted keys** = the resource attrs declared in the base + `SpanNames.h` (`xrpl.network.*`) plus the `semconv::service::*` keys the code + passes to `Resource::Create()` in `Telemetry.cpp` (`service.*`). A dotted key + declared in any other header is a violation. + +### Rules (each fails the build, when its inputs are present) + +| Rule | Check | +| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A | No stray dotted span-attribute key (only the derived resource keys may be dotted). | +| F | No string literals as attribute keys or span-name arguments in `setAttribute`/`addEvent`/`span`/`childSpan`. Attribute _values_ are exempt (runtime data). `*SpanNames.h` definitions are exempt. | +| B | Every collector `spanmetrics.dimensions` name exists in the L1 key set. | +| C | Every Tempo span-filter tag exists in the L1 key set. | +| D | Every dashboard PromQL label (non-builtin) exists in the L1 key set. | +| E | Every runbook attribute reference exists in the L1 key set. | + +## Presence-gated + +Every rule runs **only when the source files it needs are present** in the tree +and is otherwise skipped (printed as `SKIP: `), never failed. +This keeps the check correct no matter how telemetry work is split across PRs — +a stacked chain, one large PR, or independent per-stage PRs where (for example) +the collector config lands before the dashboards. The collector/Tempo/dashboard/ +runbook layers are introduced in later phases; on a branch without them, only +the L1-intrinsic rules (A, F) run. diff --git a/.github/scripts/otel-naming/check_otel_naming.py b/.github/scripts/otel-naming/check_otel_naming.py new file mode 100644 index 0000000000..5c8b647986 --- /dev/null +++ b/.github/scripts/otel-naming/check_otel_naming.py @@ -0,0 +1,568 @@ +#!/usr/bin/env python3 + +""" +Usage: check_naming.py +This script takes no parameters and can be called from any directory inside the +repository (it locates the repo root via `git rev-parse`). + +Enforces the OpenTelemetry span-attribute naming convention documented in +CONTRIBUTING.md ("Telemetry span attribute naming") across every layer of the +telemetry pipeline. The `*SpanNames.h` constants are the single source of truth +(L1); every other layer must agree with them. + +Design principles +----------------- +1. No hardcoded allowlist. The set of valid attribute keys — including which + dotted keys are legitimate resource attributes — is derived dynamically by + parsing the repository's own OTel code: + * `*SpanNames.h` `namespace attr { ... }` blocks (the underscore/bare keys + and the `join(seg::..., ...)` dotted resource compositions), and + * the keys the code passes to `Resource::Create({ ... })` in Telemetry.cpp + (the standard `semconv::service::*` keys -> service.name/version/...). + +2. Presence-gated enforcement. Every rule runs ONLY when the source files it + needs are present in the tree, and is otherwise skipped (never failed). This + keeps the check correct no matter how work is split across PRs: a stacked + chain, one large PR, or independent per-stage PRs where (for example) the + collector config lands in a different PR than the dashboards. The check never + assumes a file from another phase/PR exists. + +Layers +------ + L1 code : src/**/*SpanNames.h, include/**/*SpanNames.h (ground truth) + L1 resource : src/libxrpl/telemetry/Telemetry.cpp (dotted allowlist) + L1 callsites : setAttribute/addEvent/span/childSpan in src/**, include/** + L2 collector : docker/telemetry/otel-collector-config.yaml (spanmetrics dims) + L3 tempo : docker/telemetry/tempo.yaml (span filter tags) + L4 dashboards: docker/telemetry/grafana/dashboards/*.json (PromQL labels) + L5 runbook : docs/telemetry-runbook.md (attr tables) + +Rules (each FAILS the build, when its inputs are present) +--------------------------------------------------------- + A No stray dotted span-attribute key. A dotted `.` used as a span + attribute that is not in the derived resource-key set is a violation. + F No string literals as attribute keys/values or span-name arguments. Every + setAttribute/addEvent/span/childSpan argument must reference a *SpanNames.h + constant, never a "literal". Definitions inside *SpanNames.h are exempt. + B Every collector spanmetrics dimension exists in the L1 key set. + C Every tempo span-filter tag exists in the L1 key set. + D Every dashboard PromQL label (non-builtin) exists in the L1 key set. + E Every attribute named in the runbook tables exists in the L1 key set. + +Exit code is non-zero if any present-and-enforced rule finds a violation. +""" + +import json +import re +import subprocess +import sys +from pathlib import Path +from typing import Dict, List, Optional, Set, Tuple + +# --------------------------------------------------------------------------- +# Repo location +# --------------------------------------------------------------------------- + + +def repo_root() -> Path: + """Return the repository root, so the script works from any CWD.""" + out = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=True, + ) + return Path(out.stdout.strip()) + + +# --------------------------------------------------------------------------- +# Regexes (compiled once) +# --------------------------------------------------------------------------- + +# A segment/string constant definition: `inline constexpr auto NAME = ;` +CONST_DEF = re.compile(r"inline\s+constexpr\s+auto\s+(\w+)\s*=\s*(.+?);", re.DOTALL) +MAKESTR = re.compile(r'makeStr\(\s*"([^"]*)"\s*\)') +# A `namespace {` opener, to track which namespace a constant lives in. +NS_OPEN = re.compile(r"namespace\s+([\w:]+)\s*\{") +# Telemetry call-sites whose string arguments must be constants, not literals. +CALLSITE = re.compile(r"\b(setAttribute|addEvent|span|childSpan)\s*\(") +# A C++ string literal (used to flag literals inside call-site argument lists). +STRING_LITERAL = re.compile(r'"((?:[^"\\]|\\.)*)"') +# Dotted token that looks like `a.b` or `a.b.c` (lowercase + underscore words). +DOTTED_TOKEN = re.compile(r"\b([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+)\b") + + +def strip_block_comments(text: str) -> str: + """Remove /* ... */ comments but KEEP /// and // line comments. + + Doxygen @code examples live in /** ... */ blocks, and Rule F must still see + the call-sites inside them, so we do NOT strip /** */ wholesale. Instead we + only strip C-style comments that are clearly not doc blocks is unsafe, so we + keep all comments and rely on call-site detection. This function currently + returns text unchanged and exists as a single, documented control point. + """ + return text + + +# --------------------------------------------------------------------------- +# L1: parse *SpanNames.h into the authoritative key set +# --------------------------------------------------------------------------- + + +def find_spanname_headers(root: Path) -> List[Path]: + return sorted( + p + for p in list((root / "src").rglob("*SpanNames.h")) + + list((root / "include").rglob("*SpanNames.h")) + if p.is_file() + ) + + +def resolve_constants( + text: str, symbols: Optional[Dict[str, str]] = None +) -> Dict[str, str]: + """Resolve `inline constexpr auto NAME = ` to strings. + + Supports the small constexpr DSL used by SpanNames.h: + makeStr("x") -> "x" + join(a, b) -> resolve(a) + "." + resolve(b) + seg::xrpl / attr::foo -> looked up in the symbol table + The optional `symbols` argument seeds (and is updated in place with) the + table, so a global pass over ALL *SpanNames.h headers can resolve + cross-file references such as `join(seg::rpc, ...)` where `seg::rpc` is + defined in the base SpanNames.h. Keys are stored by their bare name + (last `::` component), so `seg::rpc` and `rpc` both resolve. + """ + if symbols is None: + symbols = {} + + def resolve_expr(expr: str) -> Optional[str]: + expr = expr.strip() + m = MAKESTR.fullmatch(expr) + if m: + return m.group(1) + if expr.startswith("join(") and expr.endswith(")"): + args = split_top_level_args(expr[len("join(") : -1]) + parts = [resolve_expr(a) for a in args] + if any(p is None for p in parts): + return None + return ".".join(p for p in parts if p is not None) + # Bare or qualified symbol reference, e.g. `seg::xrpl` or `networkId`. + key = expr.split("::")[-1] + return symbols.get(key, symbols.get(expr)) + + # Iterate definitions in source order so earlier symbols are available. + for m in CONST_DEF.finditer(text): + name, expr = m.group(1), m.group(2) + val = resolve_expr(expr) + if val is not None: + symbols[name] = val + return symbols + + +def build_global_symbols(headers: List[Path]) -> Dict[str, str]: + """Resolve constants across ALL headers so cross-file `seg::`/`join` + references (e.g. `join(seg::rpc, ...)` in RpcSpanNames.h, where `seg::rpc` + lives in the base SpanNames.h) resolve. Base SpanNames.h is processed + first so its `seg::` segments seed the table.""" + symbols: Dict[str, str] = {} + ordered = sorted(headers, key=lambda p: (p.name != "SpanNames.h", str(p))) + # Two passes: the first seeds segments, the second resolves dependents. + for _ in range(2): + for h in ordered: + resolve_constants(h.read_text(), symbols) + return symbols + + +def split_top_level_args(s: str) -> List[str]: + """Split a comma-separated arg list, respecting nested parentheses.""" + args, depth, cur = [], 0, "" + for ch in s: + if ch == "(": + depth += 1 + cur += ch + elif ch == ")": + depth -= 1 + cur += ch + elif ch == "," and depth == 0: + args.append(cur) + cur = "" + else: + cur += ch + if cur.strip(): + args.append(cur) + return args + + +def attr_keys_from_header(path: Path, symbols: Dict[str, str]) -> Set[str]: + """Return the set of attribute-key strings declared in a header's + `namespace attr { ... }` block(s). `symbols` is the global cross-file + table so `join(seg::rpc, ...)` style dotted attrs resolve correctly.""" + text = path.read_text() + keys: Set[str] = set() + # Walk the file tracking namespace nesting to find `namespace attr` blocks. + depth = 0 + attr_depth: Optional[int] = None + for line in text.splitlines(): + opener = NS_OPEN.search(line) + if opener: + if opener.group(1).split("::")[-1] == "attr": + attr_depth = depth + depth += 1 + continue + # Count brace nesting crudely for namespace close detection. + depth += line.count("{") - line.count("}") + if attr_depth is not None and depth <= attr_depth: + attr_depth = None + continue + if attr_depth is not None: + md = CONST_DEF.search(line) + if md: + # Resolve the named constant against the global symbol table; + # this captures both makeStr("x") and join(seg::y, ...) forms. + val = symbols.get(md.group(1)) + if val is not None: + keys.add(val) + else: + for ms in MAKESTR.finditer(line): + keys.add(ms.group(1)) + return keys + + +# --------------------------------------------------------------------------- +# Reporting +# --------------------------------------------------------------------------- + + +class Report: + def __init__(self) -> None: + self.violations: List[Tuple[str, str, str, str]] = [] + self.skips: List[str] = [] + self.checked: List[str] = [] + + def violation(self, rule: str, loc: str, token: str, expected: str) -> None: + self.violations.append((rule, loc, token, expected)) + + def skip(self, rule: str, reason: str) -> None: + self.skips.append(f"SKIP: {rule} — {reason}") + + def ok(self, msg: str) -> None: + self.checked.append(f"OK: {msg}") + + def render_and_exit(self) -> None: + for line in self.skips: + print(line) + for line in self.checked: + print(line) + if self.violations: + print("\nNaming-convention violations:\n") + print(f" {'RULE':<5} {'LOCATION':<48} {'TOKEN':<28} EXPECTED") + print(f" {'-' * 5} {'-' * 48} {'-' * 28} {'-' * 30}") + for rule, loc, token, expected in self.violations: + print(f" {rule:<5} {loc:<48} {token:<28} {expected}") + print( + "\nSee CONTRIBUTING.md -> 'Telemetry span attribute naming'. " + "The *SpanNames.h constants are the single source of truth." + ) + sys.exit(1) + print("\nAll present telemetry naming layers are consistent.") + sys.exit(0) + + +def main() -> None: + root = repo_root() + report = Report() + + # --- Build the L1 ground-truth key set (presence-gated) ---------------- + headers = find_spanname_headers(root) + l1_keys: Set[str] = set() + if headers: + symbols = build_global_symbols(headers) + # Map each key to the header(s) that declare it, so Rule A can tell a + # legitimate resource attr (declared in the base SpanNames.h) from a + # stray dotted key declared in a domain header. + keys_by_header: Dict[Path, Set[str]] = {} + for h in headers: + hk = attr_keys_from_header(h, symbols) + keys_by_header[h] = hk + l1_keys |= hk + report.ok( + f"L1: {len(l1_keys)} attribute keys from {len(headers)} " + f"*SpanNames.h header(s)" + ) + else: + report.skip("L1", "no *SpanNames.h present (not a naming-relevant tree)") + keys_by_header = {} + + # --- Derive the legitimate dotted (resource) keys dynamically ---------- + # ONLY the base SpanNames.h resource attrs + the semconv keys Telemetry.cpp + # passes to Resource::Create(). A dotted key from any other header is a + # violation, not an allowlist entry. + dotted_allow = derive_dotted_resource_keys(root, keys_by_header, report) + + # --- Rule A: no stray dotted span-attribute keys ----------------------- + if l1_keys: + run_rule_a(keys_by_header, dotted_allow, report) + # --- Rule F: no string literals at telemetry call-sites ---------------- + if headers: + run_rule_f(root, report) + else: + report.skip("F", "no *SpanNames.h present") + + # --- Cross-layer rules B/C/D/E (each presence-gated) ------------------- + run_rule_b_collector(root, l1_keys, report) + run_rule_c_tempo(root, l1_keys, report) + run_rule_d_dashboards(root, l1_keys, report) + run_rule_e_runbook(root, l1_keys, report) + + report.render_and_exit() + + +def derive_dotted_resource_keys( + root: Path, keys_by_header: Dict[Path, Set[str]], report: Report +) -> Set[str]: + """Legitimate dotted keys = dotted attrs declared ONLY in the base + SpanNames.h (xrpl.network.*) plus the standard semconv keys the code + passes to Resource::Create() in Telemetry.cpp (service.*). Dotted keys + declared in any OTHER header are NOT allowlisted — they are Rule-A + violations.""" + allow: Set[str] = set() + for h, keys in keys_by_header.items(): + if h.name == "SpanNames.h": # the base header holds resource attrs + allow |= {k for k in keys if "." in k} + tele = root / "src" / "libxrpl" / "telemetry" / "Telemetry.cpp" + if tele.is_file(): + text = tele.read_text() + # semconv::service::kServiceName -> service.name, etc. + for m in re.finditer(r"semconv::(\w+)::k(\w+)", text): + group = m.group(1) # e.g. "service" + field = camel_to_snake(m.group(2)).replace("service_", "", 1) + allow.add(f"{group}.{field.replace('_', '.')}") + report.ok(f"resource dotted-key allowlist derived: {sorted(allow)}") + else: + report.skip("resource-derive", "Telemetry.cpp not present") + return allow + + +def camel_to_snake(s: str) -> str: + return re.sub(r"(? None: + """Any dotted attribute key that is not an allowed resource key is a + violation, reported against the header that declares it.""" + found = False + for h in sorted(keys_by_header): + for key in sorted(keys_by_header[h]): + if "." in key and key not in dotted_allow: + found = True + report.violation("A", h.name, key, "underscore form, not dotted") + if not found: + report.ok("A: no stray dotted span-attribute keys") + + +# Which argument positions of each call must be a constant (0-based). The +# attribute VALUE position is intentionally absent: values are runtime data +# (command names, hashes, counts), not naming-convention surface. +# setAttribute(key, value) -> check arg 0 (key); value (arg 1) exempt +# addEvent(name[, attrs]) -> check arg 0 (event name) +# span(category, prefix, name) -> check args 1,2 (prefix + span-name leaf) +# childSpan(name[, parentCtx]) -> check arg 0 (span-name leaf) +CONSTANT_ARG_POSITIONS: Dict[str, Set[int]] = { + "setAttribute": {0}, + "addEvent": {0}, + "span": {1, 2}, + "childSpan": {0}, +} + + +def run_rule_f(root: Path, report: Report) -> None: + """Flag string literals in the constant-only argument positions of + setAttribute/addEvent/span/childSpan. Attribute VALUES are exempt (runtime + data). *SpanNames.h definitions are exempt (constants live there).""" + found = False + sources = [ + p + for base in ("src", "include") + for ext in ("*.h", "*.cpp") + for p in (root / base).rglob(ext) + if p.is_file() + ] + for path in sorted(sources): + if path.name.endswith("SpanNames.h"): + continue + text = path.read_text(errors="ignore") + for call, arglist, lineno in iter_calls(text): + positions = CONSTANT_ARG_POSITIONS.get(call, set()) + args = split_top_level_args(arglist) + for idx in positions: + if idx >= len(args): + continue + lit = STRING_LITERAL.search(args[idx]) + if lit: + found = True + report.violation( + "F", + f"{path.relative_to(root)}:{lineno}", + f'{call} arg{idx} "{lit.group(1)}"', + "use a *SpanNames.h constant", + ) + if not found: + report.ok("F: no string-literal keys/names at telemetry call-sites") + + +def iter_calls(text: str): + """Yield (call_name, raw_arglist, lineno) for each setAttribute/addEvent/ + span/childSpan invocation, spanning multiple physical lines if needed.""" + # Precompute line-number lookup by character offset. + line_starts = [0] + for ch in text: + line_starts.append(line_starts[-1] + 1) + for m in CALLSITE.finditer(text): + name = m.group(1) + # Walk from the opening paren, balancing nesting to find the close. + i = m.end() # one char past the '(' + depth = 1 + while i < len(text) and depth > 0: + c = text[i] + if c == "(": + depth += 1 + elif c == ")": + depth -= 1 + i += 1 + arglist = text[m.end() : i - 1] + lineno = text.count("\n", 0, m.start()) + 1 + yield name, arglist, lineno + + +def run_rule_b_collector(root: Path, l1_keys: Set[str], report: Report) -> None: + path = root / "docker" / "telemetry" / "otel-collector-config.yaml" + if not path.is_file(): + report.skip("B", "collector config not present") + return + text = path.read_text() + if "spanmetrics" not in text: + report.skip("B", "no spanmetrics block in collector config") + return + dims = extract_spanmetrics_dimensions(text) + if not l1_keys: + report.skip("B", "no L1 key set to validate against") + return + miss = [d for d in dims if d not in l1_keys] + for d in miss: + report.violation("B", str(path.relative_to(root)), d, "must exist in L1") + if not miss: + report.ok(f"B: {len(dims)} collector dimension(s) all in L1") + + +def extract_spanmetrics_dimensions(text: str) -> List[str]: + dims: List[str] = [] + in_dims = False + for line in text.splitlines(): + if re.search(r"\bdimensions\s*:", line): + in_dims = True + continue + if in_dims: + m = re.search(r"-\s*name\s*:\s*([A-Za-z0-9_.]+)", line) + if m: + dims.append(m.group(1)) + elif line.strip() and not line.lstrip().startswith("-") and ":" in line: + in_dims = False + return dims + + +def run_rule_c_tempo(root: Path, l1_keys: Set[str], report: Report) -> None: + path = root / "docker" / "telemetry" / "tempo.yaml" + if not path.is_file(): + report.skip("C", "tempo.yaml not present") + return + text = path.read_text() + tags = re.findall(r"(?:tag|attribute)\s*:\s*([A-Za-z0-9_.]+)", text) + span_tags = [t for t in tags if not t.startswith(("service.", "span."))] + if not span_tags: + report.skip("C", "no span-attribute tags in tempo.yaml") + return + if not l1_keys: + report.skip("C", "no L1 key set to validate against") + return + miss = [t for t in span_tags if t not in l1_keys] + for t in miss: + report.violation("C", str(path.relative_to(root)), t, "must exist in L1") + if not miss: + report.ok(f"C: {len(span_tags)} tempo tag(s) all in L1") + + +def run_rule_d_dashboards(root: Path, l1_keys: Set[str], report: Report) -> None: + dash_dir = root / "docker" / "telemetry" / "grafana" / "dashboards" + files = sorted(dash_dir.glob("*.json")) if dash_dir.is_dir() else [] + if not files: + report.skip("D", "no dashboard JSON present") + return + if not l1_keys: + report.skip("D", "no L1 key set to validate against") + return + builtins = { + "le", + "exported_instance", + "span_name", + "status_code", + "service_name", + "service_version", + "service_instance_id", + "job", + "instance", + } + found = False + for f in files: + try: + text = f.read_text() + except OSError: + continue + # PromQL `sum by (a, b)` and `{label="..."}` references. + labels: Set[str] = set() + for m in re.finditer(r"by\s*\(([^)]*)\)", text): + labels |= {x.strip() for x in m.group(1).split(",") if x.strip()} + for m in re.finditer(r"\b([a-z_][a-z0-9_]*)\s*[=!]~?\s*\"", text): + labels.add(m.group(1)) + for lbl in sorted(labels): + if lbl in builtins or lbl in l1_keys: + continue + found = True + report.violation( + "D", str(f.relative_to(root)), lbl, "must exist in L1 or builtin" + ) + if not found: + report.ok(f"D: dashboard PromQL labels all in L1 ({len(files)} file(s))") + + +def run_rule_e_runbook(root: Path, l1_keys: Set[str], report: Report) -> None: + path = root / "docs" / "telemetry-runbook.md" + if not path.is_file(): + report.skip("E", "runbook not present") + return + if not l1_keys: + report.skip("E", "no L1 key set to validate against") + return + text = path.read_text() + found = False + # Attribute names appear as `code` spans in the runbook's reference tables. + for m in re.finditer(r"`([a-z][a-z0-9_]*(?:\.[a-z0-9_]+)*)`", text): + token = m.group(1) + # Only consider tokens that look like attribute keys (underscore form or + # a dotted form), and skip obvious span names / prose words. + if "_" in token or "." in token: + if token in l1_keys: + continue + if "." in token: # dotted: must be a resource key in L1 + found = True + report.violation( + "E", str(path.relative_to(root)), token, "underscore, not dotted" + ) + if not found: + report.ok("E: runbook attribute references consistent with L1") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index 4b2edeb93d..59e78354dd 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -51,8 +51,10 @@ jobs: files: | # These paths are unique to `on-pr.yml`. .github/scripts/levelization/** + .github/scripts/otel-naming/** .github/scripts/rename/** .github/workflows/reusable-check-levelization.yml + .github/workflows/reusable-check-otel-naming.yml .github/workflows/reusable-check-rename.yml .github/workflows/on-pr.yml @@ -108,6 +110,11 @@ jobs: if: ${{ needs.should-run.outputs.go == 'true' }} uses: ./.github/workflows/reusable-check-levelization.yml + check-otel-naming: + needs: should-run + if: ${{ needs.should-run.outputs.go == 'true' }} + uses: ./.github/workflows/reusable-check-otel-naming.yml + check-rename: needs: should-run if: ${{ needs.should-run.outputs.go == 'true' }} @@ -176,6 +183,7 @@ jobs: if: failure() || cancelled() needs: - check-levelization + - check-otel-naming - check-rename - clang-tidy - build-test diff --git a/.github/workflows/reusable-check-otel-naming.yml b/.github/workflows/reusable-check-otel-naming.yml new file mode 100644 index 0000000000..a7af2da8cd --- /dev/null +++ b/.github/workflows/reusable-check-otel-naming.yml @@ -0,0 +1,28 @@ +# This workflow checks that OpenTelemetry span-attribute names stay consistent +# across the code (*SpanNames.h), collector, Tempo, dashboards, and docs. +# See .github/scripts/otel-naming/check_otel_naming.py and the +# "Telemetry span attribute naming" section in CONTRIBUTING.md. +name: Check OTel naming + +# This workflow can only be triggered by other workflows. +on: workflow_call + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-otel-naming + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + otel-naming: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Check OTel naming + # The script is stdlib-only and reads only files already in the tree; + # it enforces each rule only when the layer it needs is present, so it + # works whether telemetry changes land in one PR or several. + run: python .github/scripts/otel-naming/check_otel_naming.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc56d5a6bb..7ba59f1f6e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -326,8 +326,24 @@ form (e.g. `service.*` resource attributes, `http.*` span attributes); the "no dotted form" rule above applies to xrpl-custom keys, not to OTel-standard conventions. -Always reference the `*SpanNames.h` constants — never pass string literals as -attribute keys or values to `setAttribute`/`addEvent`. +Always reference the `*SpanNames.h` constants for attribute keys and span +names — never pass a string literal as a key or as a `span`/`childSpan` name +argument. (Attribute _values_ may be runtime data.) + +These rules are enforced by `.github/scripts/otel-naming/check_otel_naming.py`, +run in CI on every pull request. The check derives the set of valid keys +directly from the `*SpanNames.h` constants and the resource attributes the code +registers, so there is no separate list to keep in sync. It cross-validates the +collector, Tempo, dashboards, and docs against those keys, and each rule runs +only when the file it needs is present — so it works whether telemetry changes +land in one pull request or several. Run it locally with: + +``` +python .github/scripts/otel-naming/check_otel_naming.py +``` + +See [.github/scripts/otel-naming/README.md](.github/scripts/otel-naming/README.md) +for the full rule list. ## Contracts and instrumentation diff --git a/include/xrpl/telemetry/SpanGuard.h b/include/xrpl/telemetry/SpanGuard.h index d5e3a5781f..5b2313f0e7 100644 --- a/include/xrpl/telemetry/SpanGuard.h +++ b/include/xrpl/telemetry/SpanGuard.h @@ -53,8 +53,8 @@ using namespace xrpl::telemetry; auto span = SpanGuard::span( - TraceCategory::Rpc, rpc_span::prefix::command, "submit"); - span.setAttribute(rpc_span::attr::command, "submit"); + TraceCategory::Rpc, rpc_span::prefix::command, commandName); + span.setAttribute(rpc_span::attr::command, commandName); span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::success); // span ended automatically on scope exit @endcode @@ -62,7 +62,7 @@ 2. Error recording: @code auto span = SpanGuard::span( - TraceCategory::Rpc, rpc_span::prefix::command, "submit"); + TraceCategory::Rpc, rpc_span::prefix::command, commandName); try { doWork(); span.setOk(); diff --git a/include/xrpl/telemetry/Telemetry.h b/include/xrpl/telemetry/Telemetry.h index a354136d70..f4d3bff6ce 100644 --- a/include/xrpl/telemetry/Telemetry.h +++ b/include/xrpl/telemetry/Telemetry.h @@ -52,19 +52,22 @@ 2. Child span for a sub-operation (scoped child): @code - auto parent = SpanGuard::span(TraceCategory::Transactions, "tx", "process"); + auto parent = SpanGuard::span( + TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::process); { - auto child = parent.childSpan("tx.apply"); - child.setAttribute("tx_type", txType); + auto child = parent.childSpan(rpc_span::op::process); + child.setAttribute(rpc_span::attr::version, apiVersion); // child ends here } @endcode 3. Unrelated span (cross-scope, same thread): @code - // Transactions and RPC can be active simultaneously - auto txSpan = SpanGuard::span(TraceCategory::Transactions, "tx", "process"); - auto rpcSpan = SpanGuard::span(TraceCategory::Rpc, "rpc", "info"); + // gRPC and RPC handlers can be active simultaneously + auto grpcSpan = SpanGuard::span( + TraceCategory::Rpc, grpc_span::prefix::grpc, grpc_span::attr::method); + auto rpcSpan = SpanGuard::span( + TraceCategory::Rpc, rpc_span::prefix::command, commandName); // both spans end on scope exit @endcode @@ -74,7 +77,7 @@ auto ctx = parentGuard.captureContext(); // Thread B: create child span with explicit parent - auto child = SpanGuard::childSpan("async.work", ctx); + auto child = SpanGuard::childSpan(rpc_span::op::process, ctx); @endcode @note Thread safety: The Telemetry interface is safe for concurrent reads From ca7282479f166d6ac8446b4651d90a73de6d9765 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:33:08 +0100 Subject: [PATCH 22/30] ci: Enforce lower_snake_case attribute keys in OTel naming check Add Rule G to check_otel_naming.py: every span-attribute key must be lower_snake_case (^[a-z][a-z0-9_]*$ per dot-separated segment). This catches camelCase, UPPERCASE, and spaces in keys, which the structural (dotted) and source (literal) rules did not. Document it in the script README and CONTRIBUTING.md. Co-Authored-By: Claude Opus 4.8 --- .github/scripts/otel-naming/README.md | 1 + .../scripts/otel-naming/check_otel_naming.py | 27 +++++++++++++++++++ CONTRIBUTING.md | 4 +++ 3 files changed, 32 insertions(+) diff --git a/.github/scripts/otel-naming/README.md b/.github/scripts/otel-naming/README.md index a510f90dfb..2a7708d512 100644 --- a/.github/scripts/otel-naming/README.md +++ b/.github/scripts/otel-naming/README.md @@ -36,6 +36,7 @@ hardcoded allowlist: | Rule | Check | | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | A | No stray dotted span-attribute key (only the derived resource keys may be dotted). | +| G | Attribute keys are `lower_snake_case` (`^[a-z][a-z0-9_]*$` per dot-segment) — no camelCase, UPPERCASE, or spaces. | | F | No string literals as attribute keys or span-name arguments in `setAttribute`/`addEvent`/`span`/`childSpan`. Attribute _values_ are exempt (runtime data). `*SpanNames.h` definitions are exempt. | | B | Every collector `spanmetrics.dimensions` name exists in the L1 key set. | | C | Every Tempo span-filter tag exists in the L1 key set. | diff --git a/.github/scripts/otel-naming/check_otel_naming.py b/.github/scripts/otel-naming/check_otel_naming.py index 5c8b647986..3a5e157444 100644 --- a/.github/scripts/otel-naming/check_otel_naming.py +++ b/.github/scripts/otel-naming/check_otel_naming.py @@ -41,6 +41,8 @@ Rules (each FAILS the build, when its inputs are present) --------------------------------------------------------- A No stray dotted span-attribute key. A dotted `.` used as a span attribute that is not in the derived resource-key set is a violation. + G Attribute keys must be lower_snake_case (^[a-z][a-z0-9_]*$ per segment). + Flags camelCase, UPPERCASE, spaces, and other stray characters. F No string literals as attribute keys/values or span-name arguments. Every setAttribute/addEvent/span/childSpan argument must reference a *SpanNames.h constant, never a "literal". Definitions inside *SpanNames.h are exempt. @@ -303,6 +305,9 @@ def main() -> None: # --- Rule A: no stray dotted span-attribute keys ----------------------- if l1_keys: run_rule_a(keys_by_header, dotted_allow, report) + # --- Rule G: keys must be lower_snake_case ----------------------------- + if l1_keys: + run_rule_g(keys_by_header, report) # --- Rule F: no string literals at telemetry call-sites ---------------- if headers: run_rule_f(root, report) @@ -363,6 +368,28 @@ def run_rule_a( report.ok("A: no stray dotted span-attribute keys") +# A lower_snake_case identifier segment: starts lowercase, then lowercase / +# digits / underscores. No uppercase, no spaces, no camelCase. +SNAKE_SEGMENT = re.compile(r"^[a-z][a-z0-9_]*$") + + +def run_rule_g(keys_by_header: Dict[Path, Set[str]], report: Report) -> None: + """Every attribute key must be lower_snake_case. Bare/underscore keys must + match ^[a-z][a-z0-9_]*$; dotted resource keys must be lowercase + dot-separated segments (each segment lower_snake_case). Flags camelCase, + UPPERCASE, spaces, and other stray characters.""" + found = False + for h in sorted(keys_by_header): + for key in sorted(keys_by_header[h]): + segments = key.split(".") + if all(SNAKE_SEGMENT.match(seg) for seg in segments): + continue + found = True + report.violation("G", h.name, key, "must be lower_snake_case") + if not found: + report.ok("G: all attribute keys are lower_snake_case") + + # Which argument positions of each call must be a constant (0-based). The # attribute VALUE position is intentionally absent: values are runtime data # (command names, hashes, counts), not naming-convention surface. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ba59f1f6e..d09b5f504e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -321,6 +321,10 @@ other layer must match them. A CI check enforces this end to end. 5. Span names use `[.]` (dotted). Only attribute _keys_ follow rules 1–4. +All attribute keys are `lower_snake_case` (lowercase letters, digits, and +underscores; each dot-separated segment of a resource key likewise). No +camelCase, uppercase, or spaces. + Standard OpenTelemetry semantic-convention keys keep their canonical dotted form (e.g. `service.*` resource attributes, `http.*` span attributes); the "no dotted form" rule above applies to xrpl-custom keys, not to OTel-standard From d8d6142fbec3898344b0d138c75c5f0bcb877725 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:01:57 +0100 Subject: [PATCH 23/30] ci: Revert phase-2-local OTel naming-check edits The script and its README live on phase-1c (where check_otel_naming.py was introduced). The test-file Rule-F exemption was mistakenly applied here on phase-2; revert to phase-1c's version verbatim. The exemption and further script improvements will land on phase-1c and merge forward, keeping the script's logic on the branch that owns it. Co-Authored-By: Claude Opus 4.8 --- .github/scripts/otel-naming/README.md | 18 ++++++++-------- .../scripts/otel-naming/check_otel_naming.py | 21 +++++-------------- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/.github/scripts/otel-naming/README.md b/.github/scripts/otel-naming/README.md index 6907470bab..2a7708d512 100644 --- a/.github/scripts/otel-naming/README.md +++ b/.github/scripts/otel-naming/README.md @@ -33,15 +33,15 @@ hardcoded allowlist: ### Rules (each fails the build, when its inputs are present) -| Rule | Check | -| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A | No stray dotted span-attribute key (only the derived resource keys may be dotted). | -| G | Attribute keys are `lower_snake_case` (`^[a-z][a-z0-9_]*$` per dot-segment) — no camelCase, UPPERCASE, or spaces. | -| F | No string literals as attribute keys or span-name arguments in `setAttribute`/`addEvent`/`span`/`childSpan`. Attribute _values_ are exempt (runtime data); `*SpanNames.h` definitions and test files are exempt. | -| B | Every collector `spanmetrics.dimensions` name exists in the L1 key set. | -| C | Every Tempo span-filter tag exists in the L1 key set. | -| D | Every dashboard PromQL label (non-builtin) exists in the L1 key set. | -| E | Every runbook attribute reference exists in the L1 key set. | +| Rule | Check | +| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A | No stray dotted span-attribute key (only the derived resource keys may be dotted). | +| G | Attribute keys are `lower_snake_case` (`^[a-z][a-z0-9_]*$` per dot-segment) — no camelCase, UPPERCASE, or spaces. | +| F | No string literals as attribute keys or span-name arguments in `setAttribute`/`addEvent`/`span`/`childSpan`. Attribute _values_ are exempt (runtime data). `*SpanNames.h` definitions are exempt. | +| B | Every collector `spanmetrics.dimensions` name exists in the L1 key set. | +| C | Every Tempo span-filter tag exists in the L1 key set. | +| D | Every dashboard PromQL label (non-builtin) exists in the L1 key set. | +| E | Every runbook attribute reference exists in the L1 key set. | ## Presence-gated diff --git a/.github/scripts/otel-naming/check_otel_naming.py b/.github/scripts/otel-naming/check_otel_naming.py index 5a1948e884..3a5e157444 100644 --- a/.github/scripts/otel-naming/check_otel_naming.py +++ b/.github/scripts/otel-naming/check_otel_naming.py @@ -43,11 +43,9 @@ Rules (each FAILS the build, when its inputs are present) attribute that is not in the derived resource-key set is a violation. G Attribute keys must be lower_snake_case (^[a-z][a-z0-9_]*$ per segment). Flags camelCase, UPPERCASE, spaces, and other stray characters. - F No string literals as attribute keys or span-name arguments. The - setAttribute/addEvent key and the span/childSpan prefix/name args must - reference a *SpanNames.h constant, never a "literal". Attribute VALUES are - exempt (runtime data). Definitions inside *SpanNames.h are exempt, and - test files are exempt (they pass arbitrary literals to exercise the API). + F No string literals as attribute keys/values or span-name arguments. Every + setAttribute/addEvent/span/childSpan argument must reference a *SpanNames.h + constant, never a "literal". Definitions inside *SpanNames.h are exempt. B Every collector spanmetrics dimension exists in the L1 key set. C Every tempo span-filter tag exists in the L1 key set. D Every dashboard PromQL label (non-builtin) exists in the L1 key set. @@ -407,19 +405,10 @@ CONSTANT_ARG_POSITIONS: Dict[str, Set[int]] = { } -def is_test_path(path: Path) -> bool: - """True if the path is test code. Tests legitimately pass arbitrary literal - keys/names to exercise the API mechanics, so Rule F does not apply to them. - Matches a `test`/`tests` directory anywhere in the path (e.g. src/test/, - src/tests/, .../detail/tests/).""" - return any(part in ("test", "tests") for part in path.parts) - - def run_rule_f(root: Path, report: Report) -> None: """Flag string literals in the constant-only argument positions of setAttribute/addEvent/span/childSpan. Attribute VALUES are exempt (runtime - data). *SpanNames.h definitions are exempt (constants live there). Test - files are exempt (they pass arbitrary literals to exercise the API).""" + data). *SpanNames.h definitions are exempt (constants live there).""" found = False sources = [ p @@ -429,7 +418,7 @@ def run_rule_f(root: Path, report: Report) -> None: if p.is_file() ] for path in sorted(sources): - if path.name.endswith("SpanNames.h") or is_test_path(path): + if path.name.endswith("SpanNames.h"): continue text = path.read_text(errors="ignore") for call, arglist, lineno in iter_calls(text): From 4d044e6254809cc689f88845a91cd73022155912 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:10:08 +0100 Subject: [PATCH 24/30] =?UTF-8?q?ci:=20Harden=20OTel=20naming=20check=20?= =?UTF-8?q?=E2=80=94=20unconditional=20Rule=20F,=20test=20exemption,=20Rul?= =?UTF-8?q?e=20H?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three robustness fixes to check_otel_naming.py, all on phase-1c where the script lives: - Rule F now runs UNCONDITIONALLY. It is a purely syntactic check on the call-sites and does not need the L1 key set, so code that calls SpanGuard::span/setAttribute directly without ever defining a *SpanNames.h is still caught (previously it was silently skipped when no header existed). - Exempt test files from Rule F (tests pass arbitrary literal keys to exercise the API). The call-site matcher now requires a SpanGuard/`.`/`->` receiver, so std::span and bare declarations no longer false-positive. - Add Rule H (warning, non-fatal): a namespace-qualified constant used at a telemetry call-site but not defined in any *SpanNames.h is flagged, catching constants defined in-place instead of in the proper header. Bare locals and std:: names are not warned to avoid noise. SpanGuard.h / Telemetry.h @code examples updated to reference constants that exist on this branch. README documents the new behavior. Co-Authored-By: Claude Opus 4.8 --- .github/scripts/otel-naming/README.md | 29 ++-- .../scripts/otel-naming/check_otel_naming.py | 134 +++++++++++++++--- include/xrpl/telemetry/SpanGuard.h | 6 +- 3 files changed, 134 insertions(+), 35 deletions(-) diff --git a/.github/scripts/otel-naming/README.md b/.github/scripts/otel-naming/README.md index 2a7708d512..421a108931 100644 --- a/.github/scripts/otel-naming/README.md +++ b/.github/scripts/otel-naming/README.md @@ -33,15 +33,26 @@ hardcoded allowlist: ### Rules (each fails the build, when its inputs are present) -| Rule | Check | -| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A | No stray dotted span-attribute key (only the derived resource keys may be dotted). | -| G | Attribute keys are `lower_snake_case` (`^[a-z][a-z0-9_]*$` per dot-segment) — no camelCase, UPPERCASE, or spaces. | -| F | No string literals as attribute keys or span-name arguments in `setAttribute`/`addEvent`/`span`/`childSpan`. Attribute _values_ are exempt (runtime data). `*SpanNames.h` definitions are exempt. | -| B | Every collector `spanmetrics.dimensions` name exists in the L1 key set. | -| C | Every Tempo span-filter tag exists in the L1 key set. | -| D | Every dashboard PromQL label (non-builtin) exists in the L1 key set. | -| E | Every runbook attribute reference exists in the L1 key set. | +| Rule | Check | +| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A | No stray dotted span-attribute key (only the derived resource keys may be dotted). | +| G | Attribute keys are `lower_snake_case` (`^[a-z][a-z0-9_]*$` per dot-segment) — no camelCase, UPPERCASE, or spaces. | +| F | No string literals as attribute keys or span-name arguments in `setAttribute`/`addEvent`/`span`/`childSpan`. Attribute _values_ are exempt (runtime data); `*SpanNames.h` definitions and test files are exempt. | +| B | Every collector `spanmetrics.dimensions` name exists in the L1 key set. | +| C | Every Tempo span-filter tag exists in the L1 key set. | +| D | Every dashboard PromQL label (non-builtin) exists in the L1 key set. | +| E | Every runbook attribute reference exists in the L1 key set. | + +Rule F runs **unconditionally** (it is a purely syntactic check on the +call-sites and needs no `*SpanNames.h`), so a code path that calls +`SpanGuard::span`/`setAttribute` directly without ever defining a header is +still caught. + +### Warnings (printed, never fail the build) + +| Rule | Check | +| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| H | A namespace-qualified constant (e.g. `foo::bar::myKey`) used at a telemetry call-site is not defined in any `*SpanNames.h`. The constant should live in the proper header; defining it in-place bypasses rules A/G/F. Warns rather than fails — the argument may be a legitimately dynamic value, and the header may live on a later branch. Bare locals and `std::` names are not warned. | ## Presence-gated diff --git a/.github/scripts/otel-naming/check_otel_naming.py b/.github/scripts/otel-naming/check_otel_naming.py index 3a5e157444..13b79237dc 100644 --- a/.github/scripts/otel-naming/check_otel_naming.py +++ b/.github/scripts/otel-naming/check_otel_naming.py @@ -43,15 +43,26 @@ Rules (each FAILS the build, when its inputs are present) attribute that is not in the derived resource-key set is a violation. G Attribute keys must be lower_snake_case (^[a-z][a-z0-9_]*$ per segment). Flags camelCase, UPPERCASE, spaces, and other stray characters. - F No string literals as attribute keys/values or span-name arguments. Every - setAttribute/addEvent/span/childSpan argument must reference a *SpanNames.h - constant, never a "literal". Definitions inside *SpanNames.h are exempt. + F No string literals as attribute keys or span-name arguments. The + setAttribute/addEvent key and the span/childSpan prefix/name args must + reference a *SpanNames.h constant, never a "literal". Attribute VALUES are + exempt (runtime data). Definitions inside *SpanNames.h are exempt, and + test files are exempt (they pass arbitrary literals to exercise the API). B Every collector spanmetrics dimension exists in the L1 key set. C Every tempo span-filter tag exists in the L1 key set. D Every dashboard PromQL label (non-builtin) exists in the L1 key set. E Every attribute named in the runbook tables exists in the L1 key set. +Warnings (printed, but do NOT fail the build) +---------------------------------------------- + H A constant referenced at a telemetry call-site is not defined in any + *SpanNames.h. Span constants should live in the corresponding + *SpanNames.h (single source of truth); defining one in-place bypasses the + naming rules. A warning (not a failure) because the argument may instead + be a legitimately dynamic local (e.g. a computed span-name leaf). + Exit code is non-zero if any present-and-enforced rule finds a violation. +Warnings never change the exit code. """ import json @@ -87,7 +98,15 @@ MAKESTR = re.compile(r'makeStr\(\s*"([^"]*)"\s*\)') # A `namespace {` opener, to track which namespace a constant lives in. NS_OPEN = re.compile(r"namespace\s+([\w:]+)\s*\{") # Telemetry call-sites whose string arguments must be constants, not literals. -CALLSITE = re.compile(r"\b(setAttribute|addEvent|span|childSpan)\s*\(") +# Require a receiver so we match real SpanGuard calls, not std::span / a math +# `span(...)` / a bare method declaration: +# - `SpanGuard::span(` / `SpanGuard::childSpan(` (static factory) +# - `.span(` / `->setAttribute(` etc. (member call) +# `span`/`childSpan` additionally require the `SpanGuard`/`.`/`->` receiver; +# `setAttribute`/`addEvent` only ever exist on a guard, so a `.`/`->` suffices. +CALLSITE = re.compile( + r"(?:SpanGuard::|\.|->)\s*(setAttribute|addEvent|span|childSpan)\s*\(" +) # A C++ string literal (used to flag literals inside call-site argument lists). STRING_LITERAL = re.compile(r'"((?:[^"\\]|\\.)*)"') # Dotted token that looks like `a.b` or `a.b.c` (lowercase + underscore words). @@ -239,12 +258,20 @@ def attr_keys_from_header(path: Path, symbols: Dict[str, str]) -> Set[str]: class Report: def __init__(self) -> None: self.violations: List[Tuple[str, str, str, str]] = [] + self.warnings: List[Tuple[str, str, str, str]] = [] self.skips: List[str] = [] self.checked: List[str] = [] def violation(self, rule: str, loc: str, token: str, expected: str) -> None: self.violations.append((rule, loc, token, expected)) + def warning(self, rule: str, loc: str, token: str, note: str) -> None: + """A non-fatal finding: printed, but does not fail the build. Used where + the script cannot be certain a finding is wrong (e.g. a constant used at + a call-site that is not defined in any *SpanNames.h — it might be a + misplaced constant, or a legitimately dynamic value).""" + self.warnings.append((rule, loc, token, note)) + def skip(self, rule: str, reason: str) -> None: self.skips.append(f"SKIP: {rule} — {reason}") @@ -256,6 +283,12 @@ class Report: print(line) for line in self.checked: print(line) + if self.warnings: + print("\nNaming-convention warnings (non-fatal):\n") + print(f" {'RULE':<5} {'LOCATION':<48} {'TOKEN':<28} NOTE") + print(f" {'-' * 5} {'-' * 48} {'-' * 28} {'-' * 30}") + for rule, loc, token, note in self.warnings: + print(f" {rule:<5} {loc:<48} {token:<28} {note}") if self.violations: print("\nNaming-convention violations:\n") print(f" {'RULE':<5} {'LOCATION':<48} {'TOKEN':<28} EXPECTED") @@ -308,11 +341,14 @@ def main() -> None: # --- Rule G: keys must be lower_snake_case ----------------------------- if l1_keys: run_rule_g(keys_by_header, report) - # --- Rule F: no string literals at telemetry call-sites ---------------- - if headers: - run_rule_f(root, report) - else: - report.skip("F", "no *SpanNames.h present") + # --- Rule F (+ Rule H): scan telemetry call-sites ---------------------- + # Runs UNCONDITIONALLY: Rule F is a purely syntactic check (is this argument + # a literal?) and does not need the L1 key set, so a code path that uses + # SpanGuard::span/setAttribute directly without ever defining a *SpanNames.h + # is still caught. Rule H (warning) additionally flags constant references + # not defined in any *SpanNames.h. + header_symbols = spanname_symbol_names(headers) + run_rule_f(root, report, header_symbols) # --- Cross-layer rules B/C/D/E (each presence-gated) ------------------- run_rule_b_collector(root, l1_keys, report) @@ -405,11 +441,42 @@ CONSTANT_ARG_POSITIONS: Dict[str, Set[int]] = { } -def run_rule_f(root: Path, report: Report) -> None: - """Flag string literals in the constant-only argument positions of - setAttribute/addEvent/span/childSpan. Attribute VALUES are exempt (runtime - data). *SpanNames.h definitions are exempt (constants live there).""" - found = False +def is_test_path(path: Path) -> bool: + """True if the path is test code. Tests legitimately pass arbitrary literal + keys/names to exercise the API mechanics, so Rule F does not apply to them. + Matches a `test`/`tests` directory anywhere in the path (e.g. src/test/, + src/tests/, .../detail/tests/).""" + return any(part in ("test", "tests") for part in path.parts) + + +# A constant reference passed at a call-site, e.g. `rpc_span::attr::command` +# or a bare `myKey`. We capture the leaf identifier (after the last `::`). +IDENTIFIER_ARG = re.compile(r"^[\s&*]*([A-Za-z_][\w:]*)\s*$") + + +def spanname_symbol_names(headers: List[Path]) -> Set[str]: + """Every `inline constexpr auto NAME = ...;` symbol defined across the + *SpanNames.h headers, by bare name. Used by Rule H to tell whether a + constant referenced at a call-site actually lives in a SpanNames header.""" + names: Set[str] = set() + for h in headers: + for m in CONST_DEF.finditer(h.read_text(errors="ignore")): + names.add(m.group(1)) + return names + + +def run_rule_f(root: Path, report: Report, header_symbols: Set[str]) -> None: + """Walk every telemetry call-site (non-test, non-*SpanNames.h) and check the + constant-only argument positions of setAttribute/addEvent/span/childSpan: + + Rule F (FAIL): a string literal in a key / span-name position. Attribute + VALUES are exempt (runtime data). + Rule H (WARN): a constant reference whose name is not defined in any + *SpanNames.h. The constant should live in the corresponding + *SpanNames.h (single source of truth); defining it in-place bypasses + the naming rules. Warn rather than fail — the argument may instead be a + legitimately dynamic local (e.g. a computed span-name leaf).""" + found_f = False sources = [ p for base in ("src", "include") @@ -418,35 +485,56 @@ def run_rule_f(root: Path, report: Report) -> None: if p.is_file() ] for path in sorted(sources): - if path.name.endswith("SpanNames.h"): + if path.name.endswith("SpanNames.h") or is_test_path(path): continue text = path.read_text(errors="ignore") + rel = path.relative_to(root) for call, arglist, lineno in iter_calls(text): positions = CONSTANT_ARG_POSITIONS.get(call, set()) args = split_top_level_args(arglist) for idx in positions: if idx >= len(args): continue - lit = STRING_LITERAL.search(args[idx]) + arg = args[idx] + lit = STRING_LITERAL.search(arg) if lit: - found = True + found_f = True report.violation( "F", - f"{path.relative_to(root)}:{lineno}", + f"{rel}:{lineno}", f'{call} arg{idx} "{lit.group(1)}"', "use a *SpanNames.h constant", ) - if not found: + continue + # Not a literal: Rule H warns when a NAMESPACE-QUALIFIED constant + # reference (e.g. `consensus::span::accept`) is not defined in + # any *SpanNames.h — i.e. the constant was defined in-place + # instead of in the proper header. We only consider qualified + # refs (containing `::`): a bare lowercase identifier is almost + # always a legitimately dynamic local (a computed span-name leaf + # or attribute value), not a misplaced constant, so warning on it + # would be noise. Standard-library types (std::...) are skipped. + ident = IDENTIFIER_ARG.match(arg) + if not (ident and header_symbols): + continue + ref = ident.group(1) + if "::" not in ref or ref.startswith("std::"): + continue + leaf = ref.split("::")[-1] + if leaf not in header_symbols: + report.warning( + "H", + f"{rel}:{lineno}", + f"{call} arg{idx} {ref}", + "not defined in any *SpanNames.h", + ) + if not found_f: report.ok("F: no string-literal keys/names at telemetry call-sites") def iter_calls(text: str): """Yield (call_name, raw_arglist, lineno) for each setAttribute/addEvent/ span/childSpan invocation, spanning multiple physical lines if needed.""" - # Precompute line-number lookup by character offset. - line_starts = [0] - for ch in text: - line_starts.append(line_starts[-1] + 1) for m in CALLSITE.finditer(text): name = m.group(1) # Walk from the opening paren, balancing nesting to find the close. diff --git a/include/xrpl/telemetry/SpanGuard.h b/include/xrpl/telemetry/SpanGuard.h index 5b2313f0e7..51261e17b2 100644 --- a/include/xrpl/telemetry/SpanGuard.h +++ b/include/xrpl/telemetry/SpanGuard.h @@ -73,16 +73,16 @@ 3. Cross-thread context propagation: @code - #include + #include using namespace xrpl::telemetry; // Thread A: create span and capture context auto span = SpanGuard::span( - TraceCategory::Consensus, seg::consensus, consensus::span::op::round); + TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::process); auto ctx = span.captureContext(); // Thread B: create child with captured context - auto child = SpanGuard::childSpan(consensus::span::accept, ctx); + auto child = SpanGuard::childSpan(rpc_span::op::process, ctx); @endcode 4. Conditional check (rarely needed — methods are no-ops on null): From 2f02fc2c044b88c4e75bc3ecabe05b9c93c757bf Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:15:21 +0100 Subject: [PATCH 25/30] test(telemetry): use convention-correct attribute keys in SpanGuard test SpanGuardFactory.cpp set dotted "xrpl.rpc.command"/"xrpl.rpc.status" attribute keys. Test files are exempt from the naming check's Rule F (they pass arbitrary literals to exercise the API), so this slipped through, but the keys should still illustrate the underscore convention. Use "command"/"rpc_status". Co-Authored-By: Claude Opus 4.8 --- src/tests/libxrpl/telemetry/SpanGuardFactory.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp b/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp index 674f0073be..4025a0b1ef 100644 --- a/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp +++ b/src/tests/libxrpl/telemetry/SpanGuardFactory.cpp @@ -28,8 +28,8 @@ TEST(SpanGuardFactory, category_span_returns_null_when_disabled) auto span = SpanGuard::span(TraceCategory::Rpc, "rpc", "test"); EXPECT_FALSE(span); - span.setAttribute("xrpl.rpc.command", "test"); - span.setAttribute("xrpl.rpc.status", "success"); + span.setAttribute("command", "test"); + span.setAttribute("rpc_status", "success"); } TEST(SpanGuardFactory, child_span_null_when_no_parent) From 9e6c5b57784d9886b1b841fd91611aa0838f550b Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:21:28 +0100 Subject: [PATCH 26/30] docs(telemetry): note dotted attr keys in phase 3-5 task lists are illustrative MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Phase3/4/5 task lists show attribute keys in the older dotted `xrpl..` form because that mirrors how a fully qualified attribute reads in a Tempo trace view, which keeps the planning prose readable. Add a note to each clarifying that the implemented keys follow the underscore convention in CONTRIBUTING.md and that the *SpanNames.h constants (enforced by the CI naming check) are the single source of truth — so the dotted form here is illustrative, not the literal key. Co-Authored-By: Claude Opus 4.8 --- OpenTelemetryPlan/Phase3_taskList.md | 7 +++++++ OpenTelemetryPlan/Phase4_taskList.md | 7 +++++++ OpenTelemetryPlan/Phase5_taskList.md | 9 +++++++++ 3 files changed, 23 insertions(+) diff --git a/OpenTelemetryPlan/Phase3_taskList.md b/OpenTelemetryPlan/Phase3_taskList.md index 1e93d4fd4c..f21f00f0ed 100644 --- a/OpenTelemetryPlan/Phase3_taskList.md +++ b/OpenTelemetryPlan/Phase3_taskList.md @@ -6,6 +6,13 @@ > > **Branch**: `pratik/otel-phase3-tx-tracing` (from `pratik/otel-phase2-rpc-tracing`) +> **Note on attribute names**: the `xrpl..` keys shown below are +> written in the older dotted form for readability — it mirrors how the fully +> qualified attribute reads in a Tempo trace view. The implemented keys follow +> the convention in [CONTRIBUTING.md](../CONTRIBUTING.md#telemetry-span-attribute-naming) +> (underscore form, e.g. `tx_hash`, `peer_id`); the `*SpanNames.h` constants are +> the single source of truth. + ### Related Plan Documents | Document | Relevance | diff --git a/OpenTelemetryPlan/Phase4_taskList.md b/OpenTelemetryPlan/Phase4_taskList.md index a5ef457efd..d63ae8a072 100644 --- a/OpenTelemetryPlan/Phase4_taskList.md +++ b/OpenTelemetryPlan/Phase4_taskList.md @@ -6,6 +6,13 @@ > > **Branch**: `pratik/otel-phase4-consensus-tracing` (from `pratik/otel-phase3-tx-tracing`) +> **Note on attribute names**: the `xrpl..` keys shown below are +> written in the older dotted form for readability — it mirrors how the fully +> qualified attribute reads in a Tempo trace view. The implemented keys follow +> the convention in [CONTRIBUTING.md](../CONTRIBUTING.md#telemetry-span-attribute-naming) +> (underscore form, e.g. `consensus_round`, `consensus_mode`); the +> `*SpanNames.h` constants are the single source of truth. + ### Related Plan Documents | Document | Relevance | diff --git a/OpenTelemetryPlan/Phase5_taskList.md b/OpenTelemetryPlan/Phase5_taskList.md index c2d0aa9c60..f8dc3a86d8 100644 --- a/OpenTelemetryPlan/Phase5_taskList.md +++ b/OpenTelemetryPlan/Phase5_taskList.md @@ -6,6 +6,15 @@ > > **Branch**: `pratik/otel-phase5-docs-deployment` (from `pratik/otel-phase4-consensus-tracing`) +> **Note on attribute names**: the `xrpl..` keys shown below +> (including the collector spanmetrics dimension examples) are written in the +> older dotted form for readability — it mirrors how the fully qualified +> attribute reads in a Tempo trace view. The implemented keys follow the +> convention in [CONTRIBUTING.md](../CONTRIBUTING.md#telemetry-span-attribute-naming) +> (underscore form, e.g. `command`, `rpc_status`); the `*SpanNames.h` constants +> are the single source of truth, and the real collector dimensions must use +> those exact underscore keys (the CI naming check enforces this). + ### Related Plan Documents | Document | Relevance | From b1e6d90af121267e0d986247d404e36177e12b0e Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:29:06 +0100 Subject: [PATCH 27/30] ordering changes Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> --- .github/scripts/levelization/results/ordering.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 0e86ca8525..c480165983 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -252,7 +252,6 @@ xrpl.shamap > xrpl.basics xrpl.shamap > xrpl.nodestore xrpl.shamap > xrpl.protocol xrpl.telemetry > xrpl.config -xrpl.telemetry > xrpld.consensus xrpl.tx > xrpl.basics xrpl.tx > xrpl.core xrpl.tx > xrpl.ledger From 087fcf54b192bf930079000d7e5c845fd077120e Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:12:23 +0100 Subject: [PATCH 28/30] docs(telemetry): fix dotted attribute keys in 06 Spans Produced tables The two "Spans Produced" tables in 06-implementation-phases.md (added by the Phase 4 consensus work) listed span ATTRIBUTE keys in the dotted xrpl.consensus.* / ledger.seq / mode.old form. Convert them to the underscore convention, matching the authoritative ConsensusSpanNames.h constants (consensus_round, ledger_seq, consensus_mode, consensus_round_id, consensus_ledger_id, consensus_result, mode_old/new, agree_count/disagree_count, etc.). Span NAMES in column 1 stay dotted (rule 5). Every key verified to exist as a constant in ConsensusSpanNames.h / SpanNames.h. Co-Authored-By: Claude Opus 4.8 --- OpenTelemetryPlan/06-implementation-phases.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/OpenTelemetryPlan/06-implementation-phases.md b/OpenTelemetryPlan/06-implementation-phases.md index 1c68cd5808..794126635b 100644 --- a/OpenTelemetryPlan/06-implementation-phases.md +++ b/OpenTelemetryPlan/06-implementation-phases.md @@ -179,14 +179,14 @@ SHAMap tracing are not implemented. ### Spans Produced -| Span Name | Location | Attributes | -| --------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `consensus.phase.open` | `Consensus.h` | _(none)_ | -| `consensus.proposal.send` | `RCLConsensus.cpp` | `xrpl.consensus.round` | -| `consensus.ledger_close` | `RCLConsensus.cpp` | `xrpl.consensus.ledger.seq`, `xrpl.consensus.mode` | -| `consensus.accept` | `RCLConsensus.cpp` | `xrpl.consensus.proposers`, `xrpl.consensus.round_time_ms`, `xrpl.consensus.quorum` | -| `consensus.accept.apply` | `RCLConsensus.cpp` | `xrpl.consensus.close_time`, `close_time_correct`, `close_resolution_ms`, `state`, `proposing`, `round_time_ms`, `ledger.seq`, `parent_close_time`, `close_time_self`, `close_time_vote_bins`, `resolution_direction` | -| `consensus.validation.send` | `RCLConsensus.cpp` | `xrpl.consensus.ledger.seq`, `xrpl.consensus.proposing` | +| Span Name | Location | Attributes | +| --------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `consensus.phase.open` | `Consensus.h` | _(none)_ | +| `consensus.proposal.send` | `RCLConsensus.cpp` | `consensus_round` | +| `consensus.ledger_close` | `RCLConsensus.cpp` | `ledger_seq`, `consensus_mode` | +| `consensus.accept` | `RCLConsensus.cpp` | `proposers`, `round_time_ms`, `quorum` | +| `consensus.accept.apply` | `RCLConsensus.cpp` | `close_time`, `close_time_correct`, `close_resolution_ms`, `consensus_state`, `proposing`, `round_time_ms`, `ledger_seq`, `parent_close_time`, `close_time_self`, `close_time_vote_bins`, `resolution_direction` | +| `consensus.validation.send` | `RCLConsensus.cpp` | `ledger_seq`, `proposing` | ### Exit Criteria @@ -253,11 +253,11 @@ with `TraceCategory::Consensus` gating. No macros used — all tracing via direc | Span Name | Location | Key Attributes (actually set) | | ---------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------- | -| `consensus.round` | `RCLConsensus.cpp` | `round_id`, `ledger_id`, `ledger.seq`, `mode`, `trace_strategy` | +| `consensus.round` | `RCLConsensus.cpp` | `consensus_round_id`, `consensus_ledger_id`, `ledger_seq`, `consensus_mode`, `trace_strategy` | | `consensus.establish` | `Consensus.h` | `converge_percent`, `establish_count`, `proposers` | | `consensus.update_positions` | `Consensus.h` | `converge_percent`, `proposers`, `have_close_time_consensus`, `close_time_threshold`, `disputes_count`, `avalanche_threshold` | -| `consensus.check` | `Consensus.h` | `agree/disagree_count`, `converge_percent`, `have_close_time_consensus`, `threshold_percent`, `result` | -| `consensus.mode_change` | `RCLConsensus.cpp` | `mode.old`, `mode.new` | +| `consensus.check` | `Consensus.h` | `agree_count`, `disagree_count`, `converge_percent`, `have_close_time_consensus`, `threshold_percent`, `consensus_result` | +| `consensus.mode_change` | `RCLConsensus.cpp` | `mode_old`, `mode_new` | ### Exit Criteria From 85103e45501741d7e27396967598dead5b3406fe Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:16:36 +0100 Subject: [PATCH 29/30] levelization fix Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> --- .github/scripts/levelization/results/loops.txt | 3 --- .github/scripts/levelization/results/ordering.txt | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/scripts/levelization/results/loops.txt b/.github/scripts/levelization/results/loops.txt index 1f44b6159a..16e62bb0a7 100644 --- a/.github/scripts/levelization/results/loops.txt +++ b/.github/scripts/levelization/results/loops.txt @@ -4,9 +4,6 @@ Loop: test.jtx test.toplevel Loop: test.jtx test.unit_test test.unit_test ~= test.jtx -Loop: xrpl.telemetry xrpld.consensus - xrpld.consensus ~= xrpl.telemetry - Loop: xrpl.telemetry xrpld.rpc xrpld.rpc > xrpl.telemetry diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index d091753158..1f15c6b754 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -279,6 +279,7 @@ xrpld.consensus > xrpl.basics xrpld.consensus > xrpl.json xrpld.consensus > xrpl.ledger xrpld.consensus > xrpl.protocol +xrpld.consensus > xrpl.telemetry xrpld.core > xrpl.basics xrpld.core > xrpl.config xrpld.core > xrpl.core From 180c905a8ac2c66d10d049de554c6f7f9e7daa62 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:20:50 +0100 Subject: [PATCH 30/30] docs(telemetry): remove incorrect "rule 5" justification in Phase 4 task list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three entries in Phase4_taskList.md annotated dotted attribute keys (`xrpl.consensus.round`, `round_id`, `ledger_id`) as "(kept — rule 5)". That is wrong: rule 5 keeps SPAN NAMES dotted, not attribute keys — the code emits the underscore form (consensus_round, consensus_round_id, consensus_ledger_id) per ConsensusSpanNames.h. Remove the false justification; the dotted form in this task list remains as the illustrative-readability form documented in the note at the top of the file. Co-Authored-By: Claude Opus 4.8 --- OpenTelemetryPlan/Phase4_taskList.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OpenTelemetryPlan/Phase4_taskList.md b/OpenTelemetryPlan/Phase4_taskList.md index e7c1969d6a..9973331651 100644 --- a/OpenTelemetryPlan/Phase4_taskList.md +++ b/OpenTelemetryPlan/Phase4_taskList.md @@ -89,7 +89,7 @@ - In `Adaptor::propose()`: - Creates `consensus.proposal.send` span via `SpanGuard::span()` - - Sets `xrpl.consensus.round` attribute (kept — rule 5) + - Sets `xrpl.consensus.round` attribute - In `PeerImp::onMessage(TMProposeSet)`: - Creates `consensus.proposal.receive` span @@ -822,8 +822,8 @@ and OFF, and don't affect consensus timing. ```cpp // Round-level (on consensus.round) — ALL IMPLEMENTED -"xrpl.consensus.round_id" = int64 // Consensus round number (kept — rule 5) -"xrpl.consensus.ledger_id" = string // previousLedger.id() hash (kept — rule 5) +"xrpl.consensus.round_id" = int64 // Consensus round number +"xrpl.consensus.ledger_id" = string // previousLedger.id() hash "trace_strategy" = string // "deterministic" or "attribute" // Establish-level — IMPLEMENTED