mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-22 06:40:53 +00:00
Merge branch 'pratik/otel-phase3-tx-tracing' into pratik/otel-phase4-consensus-tracing
# Conflicts: # src/libxrpl/telemetry/SpanGuard.cpp
This commit is contained in:
@@ -1,23 +1,26 @@
|
||||
/**
|
||||
* Pimpl implementation for SpanGuard and SpanContext.
|
||||
* Pimpl implementation for SpanGuard, ScopedSpanGuard and SpanContext.
|
||||
*
|
||||
* All OpenTelemetry SDK types are confined to this translation unit.
|
||||
* The public SpanGuard.h header contains only standard-library types
|
||||
* and forward-declares the Impl struct.
|
||||
* and forward-declares the Impl / ScopedImpl structs.
|
||||
*
|
||||
* Two guards with split responsibilities live here:
|
||||
* - SpanGuard::Impl holds ONLY the OTel Span (shared_ptr). It never
|
||||
* touches the thread-local context stack, so a SpanGuard is
|
||||
* thread-free and may be moved to and destroyed on any thread.
|
||||
* - ScopedSpanGuard::ScopedImpl holds a SpanGuard plus an optional
|
||||
* Scope. The Scope pushes the span onto the constructing thread's
|
||||
* context stack; member order (guard first, scope second) ensures
|
||||
* the Scope pops BEFORE the span ends on destruction. It is
|
||||
* thread-bound: construct and destroy on the same thread.
|
||||
*
|
||||
* Static factory methods access the global Telemetry instance via
|
||||
* Telemetry::getInstance(), check whether the requested TraceCategory
|
||||
* is enabled, and return either an active guard with a real Span+Scope
|
||||
* or a null guard whose methods are all no-ops.
|
||||
* is enabled, and return either an active guard with a real Span or a
|
||||
* null guard whose methods are all no-ops.
|
||||
*
|
||||
* The Impl struct holds the OTel Span (shared_ptr) and an optional
|
||||
* Scope. Scope is non-movable, but since Impl lives behind a
|
||||
* unique_ptr, SpanGuard's move constructor simply transfers the
|
||||
* pointer — no double-Scope issues. A scoped guard holds the Scope;
|
||||
* detached() produces an Impl with no Scope (nullopt) so the guard
|
||||
* carries no thread-local binding and is safe to move across threads.
|
||||
*
|
||||
* @see SpanGuard (SpanGuard.h), Telemetry (Telemetry.h),
|
||||
* @see SpanGuard, ScopedSpanGuard (SpanGuard.h), Telemetry (Telemetry.h),
|
||||
* FilteringSpanProcessor (Telemetry.cpp)
|
||||
*/
|
||||
|
||||
@@ -25,8 +28,8 @@
|
||||
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
|
||||
#include <xrpl/basics/random.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/telemetry/DeterministicIdGenerator.h>
|
||||
#include <xrpl/telemetry/DiscardFlag.h>
|
||||
#include <xrpl/telemetry/Telemetry.h>
|
||||
#include <xrpl/telemetry/TraceContextPropagator.h>
|
||||
@@ -47,6 +50,7 @@
|
||||
#include <opentelemetry/trace/trace_flags.h>
|
||||
#include <opentelemetry/trace/trace_id.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
@@ -90,65 +94,22 @@ SpanContext::isValid() const
|
||||
struct SpanGuard::Impl
|
||||
{
|
||||
/**
|
||||
* The OTel span being guarded. Set to nullptr after discard() or
|
||||
* once detached() moves it into a new guard, so ~Impl skips End().
|
||||
* The OTel span being guarded. Set to nullptr after discard() so
|
||||
* ~Impl skips End().
|
||||
*/
|
||||
opentelemetry::nostd::shared_ptr<otel_trace::Span> span;
|
||||
|
||||
/**
|
||||
* Scope that activates span on the current thread's context stack.
|
||||
* nullopt for a detached guard, which holds the span with no
|
||||
* thread-local binding and is therefore safe to move across threads.
|
||||
*/
|
||||
std::optional<otel_trace::Scope> scope;
|
||||
|
||||
/**
|
||||
* Thread that constructed this Impl. Meaningful only when `scope`
|
||||
* holds a value: a Scope is bound to the thread-local context stack
|
||||
* of the thread that pushed it, so popping it (via ~Scope, when
|
||||
* ~Impl runs) on a different thread corrupts that thread's stack.
|
||||
* Checked in ~Impl() and detached() to turn a silent cross-thread
|
||||
* corruption into an assertion failure in debug/test/fuzzing builds.
|
||||
*/
|
||||
std::thread::id owner{std::this_thread::get_id()};
|
||||
|
||||
/**
|
||||
* Construct a scoped guard: the span is pushed onto this thread's
|
||||
* active-context stack for the lifetime of the guard.
|
||||
* Construct an unscoped guard: the span is owned but NOT pushed
|
||||
* onto any thread's context stack. The guard is thread-free.
|
||||
* @param s The span to guard.
|
||||
*/
|
||||
explicit Impl(opentelemetry::nostd::shared_ptr<otel_trace::Span> s) : span(std::move(s))
|
||||
{
|
||||
scope.emplace(span);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag type selecting the scope-less (detached) constructor.
|
||||
*/
|
||||
struct Detached
|
||||
{
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a detached guard: the span is held with NO thread-local
|
||||
* Scope, so the guard carries no context-stack binding and is safe
|
||||
* to move to and destroy on another thread.
|
||||
* @param s The span to guard.
|
||||
*/
|
||||
Impl(opentelemetry::nostd::shared_ptr<otel_trace::Span> s, Detached) : span(std::move(s))
|
||||
{
|
||||
}
|
||||
|
||||
~Impl()
|
||||
{
|
||||
// A live Scope (scope engaged) must be popped on the thread that
|
||||
// pushed it; ending on any other thread corrupts that thread's
|
||||
// context stack. A detached guard (scope == nullopt) carries no
|
||||
// binding and may be destroyed anywhere, so the check is skipped.
|
||||
XRPL_ASSERT(
|
||||
!scope.has_value() || owner == std::this_thread::get_id(),
|
||||
"xrpl::telemetry::SpanGuard::Impl::~Impl : scoped guard destroyed on "
|
||||
"constructing thread");
|
||||
if (span)
|
||||
span->End();
|
||||
}
|
||||
@@ -166,6 +127,8 @@ struct SpanGuard::Impl
|
||||
SpanGuard::SpanGuard() = default;
|
||||
SpanGuard::~SpanGuard() = default;
|
||||
SpanGuard::SpanGuard(SpanGuard&&) noexcept = default;
|
||||
SpanGuard&
|
||||
SpanGuard::operator=(SpanGuard&&) noexcept = default;
|
||||
|
||||
SpanGuard::SpanGuard(std::unique_ptr<Impl> impl) : impl_(std::move(impl))
|
||||
{
|
||||
@@ -248,7 +211,7 @@ SpanGuard::span(TraceCategory cat, std::string_view prefix, std::string_view nam
|
||||
}
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::rootSpan(TraceCategory cat, std::string_view prefix, std::string_view name)
|
||||
SpanGuard::freshRoot(TraceCategory cat, std::string_view prefix, std::string_view name)
|
||||
{
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled() || !isCategoryEnabled(*tel, cat))
|
||||
@@ -342,45 +305,9 @@ SpanGuard::linkedSpan(std::string_view name, SpanContext const& linkCtx)
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::detached() &&
|
||||
{
|
||||
if (!impl_)
|
||||
return {};
|
||||
// Popping the Scope (below, via impl_.reset()) must happen on the thread
|
||||
// that pushed it; calling detached() elsewhere would pop the wrong stack.
|
||||
XRPL_ASSERT(
|
||||
!impl_->scope.has_value() || impl_->owner == std::this_thread::get_id(),
|
||||
"xrpl::telemetry::SpanGuard::detached : called on constructing thread");
|
||||
// Take the span out; the old Impl.span is now null so ~Impl won't End().
|
||||
auto s = std::move(impl_->span);
|
||||
// Resetting the old Impl destroys its Scope HERE, on the origin thread,
|
||||
// popping this thread's context stack correctly. The returned guard holds
|
||||
// the span with no Scope, so it is safe to move to another thread.
|
||||
impl_.reset();
|
||||
return SpanGuard(std::make_unique<Impl>(std::move(s), Impl::Detached{}));
|
||||
}
|
||||
|
||||
// ===== Detach-in-place helpers =============================================
|
||||
|
||||
void
|
||||
detachInPlace(std::optional<SpanGuard>& guard)
|
||||
{
|
||||
if (!guard || !*guard)
|
||||
return;
|
||||
guard.emplace(std::move(*guard).detached());
|
||||
}
|
||||
|
||||
std::shared_ptr<SpanGuard>
|
||||
detachInPlace(std::shared_ptr<SpanGuard> guard)
|
||||
{
|
||||
if (!guard || !*guard)
|
||||
return guard;
|
||||
return std::make_shared<SpanGuard>(std::move(*guard).detached());
|
||||
}
|
||||
|
||||
// ===== Hash-derived span (category-gated) ==================================
|
||||
|
||||
// Standalone hash-derived span: a TRUE root whose trace_id == hashData[0:16].
|
||||
SpanGuard
|
||||
SpanGuard::hashSpan(
|
||||
TraceCategory const cat,
|
||||
@@ -395,23 +322,19 @@ SpanGuard::hashSpan(
|
||||
if ((tel == nullptr) || !tel->isEnabled() || !isCategoryEnabled(*tel, cat))
|
||||
return {};
|
||||
|
||||
otel_trace::TraceId const traceId(
|
||||
opentelemetry::nostd::span<std::uint8_t const, 16>(hashData, 16));
|
||||
|
||||
auto const rval = defaultPrng()();
|
||||
std::uint8_t spanIdBytes[8];
|
||||
std::memcpy(spanIdBytes, &rval, sizeof(spanIdBytes));
|
||||
otel_trace::SpanId const spanId(
|
||||
opentelemetry::nostd::span<std::uint8_t const, 8>(spanIdBytes, 8));
|
||||
|
||||
otel_trace::SpanContext const syntheticCtx(
|
||||
traceId, spanId, otel_trace::TraceFlags(1), /* remote = */ false);
|
||||
|
||||
auto parentCtx = opentelemetry::context::Context{}.SetValue(
|
||||
otel_trace::kSpanKey,
|
||||
opentelemetry::nostd::shared_ptr<otel_trace::Span>(
|
||||
new otel_trace::DefaultSpan(syntheticCtx)));
|
||||
// Force the deterministic trace_id via the DeterministicIdGenerator, on the
|
||||
// SDK root branch, so the span is a TRUE root (empty parent_span_id) whose
|
||||
// trace_id == hashData[0:16]. The kIsRootSpanKey context forces the SDK's
|
||||
// no-valid-parent branch, where GenerateTraceId() is called and returns the
|
||||
// pending id. The PendingTraceId guard scopes that id to this one startSpan.
|
||||
std::array<std::uint8_t, 16> tid{};
|
||||
std::memcpy(tid.data(), hashData, 16);
|
||||
auto rootCtx = opentelemetry::context::Context{otel_trace::kIsRootSpanKey, true};
|
||||
PendingTraceId const pending{tid};
|
||||
|
||||
// When a prior context is supplied, attach it as a follows-from LINK (not a
|
||||
// parent) so consecutive roots (e.g. consensus rounds) stay navigable while
|
||||
// this span remains a true root under its deterministic trace_id.
|
||||
if (followsFrom != nullptr && followsFrom->isValid())
|
||||
{
|
||||
auto linkSpan = otel_trace::GetSpan(followsFrom->impl_->ctx);
|
||||
@@ -419,7 +342,7 @@ SpanGuard::hashSpan(
|
||||
{
|
||||
auto tracer = tel->getTracer("xrpld");
|
||||
otel_trace::StartSpanOptions opts;
|
||||
opts.parent = parentCtx;
|
||||
opts.parent = rootCtx; // root marker → forces GenerateTraceId()
|
||||
opts.kind = categoryToSpanKind(cat);
|
||||
return SpanGuard(
|
||||
std::make_unique<Impl>(tracer->StartSpan(
|
||||
@@ -430,9 +353,13 @@ SpanGuard::hashSpan(
|
||||
}
|
||||
}
|
||||
|
||||
return SpanGuard(std::make_unique<Impl>(tel->startSpan(std::string(name), parentCtx)));
|
||||
return SpanGuard(
|
||||
std::make_unique<Impl>(
|
||||
tel->startSpan(std::string(name), rootCtx, categoryToSpanKind(cat))));
|
||||
}
|
||||
|
||||
// Cross-node hash-derived span: a CHILD of the sender's real remote span
|
||||
// (remote=true), not a root — so it must NOT use PendingTraceId / rootCtx.
|
||||
SpanGuard
|
||||
SpanGuard::hashSpan(
|
||||
TraceCategory const cat,
|
||||
@@ -633,6 +560,201 @@ SpanGuard::discard()
|
||||
}
|
||||
}
|
||||
|
||||
// ===== ScopedSpanGuard::ScopedImpl =========================================
|
||||
|
||||
struct ScopedSpanGuard::ScopedImpl
|
||||
{
|
||||
/**
|
||||
* The unscoped guard that owns the span. Declared FIRST so it is
|
||||
* destroyed AFTER `scope`: the Scope must pop before the span ends.
|
||||
*/
|
||||
SpanGuard guard;
|
||||
|
||||
/**
|
||||
* Active OTel Scope binding the span to this thread's context stack.
|
||||
* Present while the guard is active; reset() (via operator SpanGuard)
|
||||
* pops it eagerly on the origin thread. Declared AFTER `guard` so
|
||||
* destruction order pops the scope before the span ends.
|
||||
*/
|
||||
std::optional<otel_trace::Scope> scope;
|
||||
|
||||
/**
|
||||
* Thread that constructed this ScopedImpl. The Scope is bound to
|
||||
* this thread's context stack, so popping it (via ~Scope or reset())
|
||||
* on a different thread corrupts that thread's stack. Checked in the
|
||||
* destructor and operator SpanGuard() to turn a silent cross-thread
|
||||
* corruption into an assertion failure in debug/test/fuzzing builds.
|
||||
*/
|
||||
std::thread::id owner{std::this_thread::get_id()};
|
||||
|
||||
/**
|
||||
* Wrap a SpanGuard and activate its span on this thread. If the
|
||||
* guard is active, push its span onto the thread-local context
|
||||
* stack; a null guard leaves `scope` empty.
|
||||
* @param g The span-owning guard to activate.
|
||||
*/
|
||||
explicit ScopedImpl(SpanGuard g) : guard(std::move(g))
|
||||
{
|
||||
if (guard)
|
||||
scope.emplace(guard.impl_->span);
|
||||
}
|
||||
};
|
||||
|
||||
// ===== ScopedSpanGuard core lifecycle ======================================
|
||||
|
||||
ScopedSpanGuard::ScopedSpanGuard(SpanGuard&& guard)
|
||||
: impl_(std::make_unique<ScopedImpl>(std::move(guard)))
|
||||
{
|
||||
}
|
||||
|
||||
ScopedSpanGuard::~ScopedSpanGuard()
|
||||
{
|
||||
// A live Scope must be popped on the thread that pushed it; destroying
|
||||
// the guard on any other thread corrupts that thread's context stack.
|
||||
// A null guard holds no Scope, so the check is skipped.
|
||||
XRPL_ASSERT(
|
||||
!impl_ || !impl_->scope.has_value() || impl_->owner == std::this_thread::get_id(),
|
||||
"xrpl::telemetry::ScopedSpanGuard::~ScopedSpanGuard : destroyed on "
|
||||
"constructing thread");
|
||||
}
|
||||
|
||||
// ===== ScopedSpanGuard factory methods =====================================
|
||||
|
||||
ScopedSpanGuard::ScopedSpanGuard(TraceCategory cat, std::string_view prefix, std::string_view name)
|
||||
: ScopedSpanGuard(SpanGuard::span(cat, prefix, name))
|
||||
{
|
||||
}
|
||||
|
||||
ScopedSpanGuard
|
||||
ScopedSpanGuard::freshRoot(TraceCategory cat, std::string_view prefix, std::string_view name)
|
||||
{
|
||||
return ScopedSpanGuard(SpanGuard::freshRoot(cat, prefix, name));
|
||||
}
|
||||
|
||||
ScopedSpanGuard
|
||||
ScopedSpanGuard::childSpan(std::string_view name) const
|
||||
{
|
||||
return ScopedSpanGuard(impl_->guard.childSpan(name));
|
||||
}
|
||||
|
||||
ScopedSpanGuard
|
||||
ScopedSpanGuard::childSpan(std::string_view name, SpanContext const& parentCtx)
|
||||
{
|
||||
return ScopedSpanGuard(SpanGuard::childSpan(name, parentCtx));
|
||||
}
|
||||
|
||||
ScopedSpanGuard
|
||||
ScopedSpanGuard::linkedSpan(std::string_view name) const
|
||||
{
|
||||
return ScopedSpanGuard(impl_->guard.linkedSpan(name));
|
||||
}
|
||||
|
||||
ScopedSpanGuard
|
||||
ScopedSpanGuard::linkedSpan(std::string_view name, SpanContext const& linkCtx)
|
||||
{
|
||||
return ScopedSpanGuard(SpanGuard::linkedSpan(name, linkCtx));
|
||||
}
|
||||
|
||||
// ===== ScopedSpanGuard handoff bridge ======================================
|
||||
|
||||
ScopedSpanGuard::
|
||||
operator SpanGuard() &&
|
||||
{
|
||||
// The scope must be popped on the thread that pushed it; handing off
|
||||
// elsewhere would pop the wrong stack.
|
||||
XRPL_ASSERT(
|
||||
impl_->owner == std::this_thread::get_id(),
|
||||
"xrpl::telemetry::ScopedSpanGuard::operator SpanGuard : handoff on "
|
||||
"constructing thread");
|
||||
impl_->scope.reset(); // eager pop, on the origin thread
|
||||
return std::move(impl_->guard);
|
||||
}
|
||||
|
||||
// ===== ScopedSpanGuard context capture =====================================
|
||||
|
||||
SpanContext
|
||||
ScopedSpanGuard::captureContext() const
|
||||
{
|
||||
return impl_->guard.captureContext();
|
||||
}
|
||||
|
||||
// ===== ScopedSpanGuard forwarding methods ==================================
|
||||
|
||||
void
|
||||
ScopedSpanGuard::setAttribute(std::string_view key, std::string_view value)
|
||||
{
|
||||
impl_->guard.setAttribute(key, value);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::setAttribute(std::string_view key, char const* value)
|
||||
{
|
||||
impl_->guard.setAttribute(key, value);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::setAttribute(std::string_view key, std::int64_t value)
|
||||
{
|
||||
impl_->guard.setAttribute(key, value);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::setAttribute(std::string_view key, double value)
|
||||
{
|
||||
impl_->guard.setAttribute(key, value);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::setAttribute(std::string_view key, bool value)
|
||||
{
|
||||
impl_->guard.setAttribute(key, value);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::setOk()
|
||||
{
|
||||
impl_->guard.setOk();
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::setError(std::string_view description)
|
||||
{
|
||||
impl_->guard.setError(description);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::addEvent(std::string_view name)
|
||||
{
|
||||
impl_->guard.addEvent(name);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::recordException(std::exception const& e)
|
||||
{
|
||||
impl_->guard.recordException(e);
|
||||
}
|
||||
|
||||
void
|
||||
ScopedSpanGuard::discard()
|
||||
{
|
||||
// A live Scope must be popped on the thread that pushed it; discarding
|
||||
// on any other thread corrupts that thread's context stack. A null guard
|
||||
// holds no Scope, so the check is skipped.
|
||||
XRPL_ASSERT(
|
||||
!impl_ || !impl_->scope.has_value() || impl_->owner == std::this_thread::get_id(),
|
||||
"xrpl::telemetry::ScopedSpanGuard::discard : discarded on constructing thread");
|
||||
// Pop the scope first (on this thread) so no span stays active on the
|
||||
// stack, then discard the span via the owned guard.
|
||||
impl_->scope.reset();
|
||||
impl_->guard.discard();
|
||||
}
|
||||
|
||||
ScopedSpanGuard::
|
||||
operator bool() const
|
||||
{
|
||||
return impl_ && static_cast<bool>(impl_->guard);
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
|
||||
Reference in New Issue
Block a user