diff --git a/include/xrpl/telemetry/SpanGuard.h b/include/xrpl/telemetry/SpanGuard.h index 10a4a80b4a..24ae206694 100644 --- a/include/xrpl/telemetry/SpanGuard.h +++ b/include/xrpl/telemetry/SpanGuard.h @@ -918,6 +918,15 @@ public: void addEvent(std::string_view name) noexcept; + /** + * Add a named event with key-value attributes to the span's timeline. + * No-op on a null guard. + * @param name Event name. + * @param attrs Attribute pairs (all string_view for simplicity). + */ + void + addEvent(std::string_view name, std::initializer_list attrs) noexcept; + /** * Record an exception as a span event and mark status as error. * No-op on a null guard. @@ -1355,6 +1364,10 @@ public: { } void + addEvent(std::string_view, std::initializer_list) noexcept + { + } + void recordException(std::exception const&) noexcept { } diff --git a/src/libxrpl/telemetry/SpanGuard.cpp b/src/libxrpl/telemetry/SpanGuard.cpp index 4939fb012a..5ec341a5fd 100644 --- a/src/libxrpl/telemetry/SpanGuard.cpp +++ b/src/libxrpl/telemetry/SpanGuard.cpp @@ -863,6 +863,14 @@ ScopedSpanGuard::addEvent(std::string_view name) noexcept impl_->guard.addEvent(name); } +void +ScopedSpanGuard::addEvent( + std::string_view name, + std::initializer_list attrs) noexcept +{ + impl_->guard.addEvent(name, attrs); +} + void ScopedSpanGuard::recordException(std::exception const& e) noexcept { diff --git a/src/tests/libxrpl/telemetry/SpanGuardScope.cpp b/src/tests/libxrpl/telemetry/SpanGuardScope.cpp index 8f546183b8..cb90b8e5a9 100644 --- a/src/tests/libxrpl/telemetry/SpanGuardScope.cpp +++ b/src/tests/libxrpl/telemetry/SpanGuardScope.cpp @@ -661,6 +661,33 @@ TEST_F(SpanGuardScopeTest, spanGuard_addEvent_without_attributes_records_bare_ev EXPECT_EQ(events.front().GetAttributes().size(), 0u); } +// The scoped guard records event attributes too. consensus.accept.apply relies +// on it for one tx.included event per transaction of the accepted set. +TEST_F(SpanGuardScopeTest, scopedGuard_addEvent_records_name_and_attribute_values) +{ + namespace cs = consensus::span; + + static constexpr std::string_view kEventName{cs::event::txIncluded}; + static constexpr std::string_view kTxIdKey{cs::attr::txId}; + static constexpr std::string_view kTxId{"6B5F1A2C3D4E5F60718293A4B5C6D7E8"}; + + { + ScopedSpanGuard guard(TraceCategory::Consensus, seg::consensus, cs::op::acceptApply); + ASSERT_TRUE(static_cast(guard)); + guard.addEvent(kEventName, {{kTxIdKey, kTxId}}); + } + + auto spans = spanData()->GetSpans(); + auto* applySpan = findSpan(spans, cs::acceptApply); + ASSERT_NE(applySpan, nullptr); + + auto const& events = applySpan->GetEvents(); + ASSERT_EQ(events.size(), 1u); + EXPECT_EQ(events.front().GetName(), std::string(kEventName)); + EXPECT_EQ(events.front().GetAttributes().size(), 1u); + EXPECT_EQ(eventAttribute(events.front(), kTxIdKey), std::string(kTxId)); +} + // A forced-root span started while a PendingTraceId is active adopts that // pinned 16-byte trace_id and remains a true root (no parent). TEST_F(SpanGuardScopeTest, deterministicIdGenerator_forced_root_gets_pending_trace_id)