mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-23 15:10:34 +00:00
feat(telemetry): add ScopedActivation for non-owning span activation
Add SpanGuard::activate() returning a ScopedActivation RAII helper that activates an already-owned span (from a thread-free SpanGuard) as the current context WITHOUT taking ownership. The activation pushes the span onto the current LocalValue context store on construction and pops it on destruction; it never ends the span (its owning SpanGuard does). This lets a job-handoff span be made ambient for the duration of a synchronous, non-yielding worker body so log lines there carry the span's trace_id. Non-copyable and non-movable, mirroring ScopedSpanGuard. owner is captured after the Scope push via declaration-order member initialization (scope declared before owner), matching the A3 capture-after-materialization invariant. A #else no-op stub keeps the API zero-overhead when telemetry is compiled out. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,7 @@
|
||||
* | + addEvent(name) |
|
||||
* | + recordException(e) |
|
||||
* | + discard() |
|
||||
* | + activate() : ScopedActivation |
|
||||
* | + operator bool() |
|
||||
* +--------------------------------------------+
|
||||
* | hides (pimpl)
|
||||
@@ -233,6 +234,11 @@ public:
|
||||
// ---------------------------------------------------------------------------
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
// SpanGuard::activate() returns a ScopedActivation by value; the full class is
|
||||
// defined after ScopedSpanGuard below. Forward-declared here so the return type
|
||||
// is named before its definition.
|
||||
class ScopedActivation;
|
||||
|
||||
/**
|
||||
* RAII owner of an OTel span (unscoped, thread-free).
|
||||
*
|
||||
@@ -440,6 +446,17 @@ public:
|
||||
void
|
||||
discard();
|
||||
|
||||
// --- Activation (non-owning) ---------------------------------------
|
||||
|
||||
/**
|
||||
* Activate this guard's span as the current context for the returned
|
||||
* activation's lifetime, without transferring ownership. Returns a no-op
|
||||
* activation if this guard is null. See ScopedActivation.
|
||||
* @return an RAII activation; drop it to restore the prior context.
|
||||
*/
|
||||
[[nodiscard]] ScopedActivation
|
||||
activate() const;
|
||||
|
||||
/**
|
||||
* @return true if this guard holds an active span.
|
||||
*/
|
||||
@@ -712,11 +729,106 @@ public:
|
||||
operator bool() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* RAII activation of an already-owned span as the current context, WITHOUT
|
||||
* taking ownership. Use to give a thread-free SpanGuard (e.g. one handed into
|
||||
* a JobQueue job) ambient status for the duration of a synchronous, non-yielding
|
||||
* worker body, so log lines emitted there carry the span's trace_id. The span
|
||||
* is neither owned nor ended here — its owning SpanGuard still controls its
|
||||
* lifetime. Non-copyable, non-movable; must be created and destroyed while the
|
||||
* same context store is active (see ScopedSpanGuard).
|
||||
*
|
||||
* Dependency diagram:
|
||||
*
|
||||
* +---------------------------------------------+
|
||||
* | ScopedActivation |
|
||||
* | (non-owning RAII span activation) |
|
||||
* +---------------------------------------------+
|
||||
* | - impl_ : unique_ptr<Impl> |
|
||||
* | Impl { scope : otel::Scope; owner } |
|
||||
* +---------------------------------------------+
|
||||
* | + (ctor) pushes span onto current store |
|
||||
* | + (dtor) pops; does NOT end the span |
|
||||
* +---------------------------------------------+
|
||||
* | activates (borrows, no ownership)
|
||||
* +-----------+ ends the span
|
||||
* | SpanGuard |----------------------------> (owner)
|
||||
* +-----------+
|
||||
*
|
||||
* @note Thread-safety: like ScopedSpanGuard, it pushes/pops the current
|
||||
* LocalValue context store; construct and destroy it while the same store is
|
||||
* active (asserted in debug). Must NOT outlive the SpanGuard whose span it
|
||||
* activates. Intended as a short-lived stack local inside a worker body that
|
||||
* runs to completion on one thread (no coro yield inside the activation).
|
||||
*
|
||||
* Example 1 — correlate a handed-off span's worker logs (primary use):
|
||||
* @code
|
||||
* // span is std::shared_ptr<SpanGuard> handed into this job body:
|
||||
* auto activation = (span && *span) ? span->activate() : ScopedActivation{};
|
||||
* JLOG(j.info()) << "applied"; // carries span's trace_id
|
||||
* // span is still owned/ended elsewhere; activation only pops here.
|
||||
* @endcode
|
||||
*
|
||||
* Example 2 — edge case: a null guard yields a no-op activation:
|
||||
* @code
|
||||
* SpanGuard g; // null (telemetry off, or disabled category)
|
||||
* auto a = g.activate(); // no-op; GetCurrent() unchanged
|
||||
* @endcode
|
||||
*/
|
||||
class ScopedActivation
|
||||
{
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
friend class SpanGuard;
|
||||
explicit ScopedActivation(std::unique_ptr<Impl> impl);
|
||||
|
||||
public:
|
||||
/**
|
||||
* Construct a null / no-op activation. Its destructor does nothing and
|
||||
* leaves the current context unchanged. Returned by SpanGuard::activate()
|
||||
* for a null guard.
|
||||
*/
|
||||
ScopedActivation();
|
||||
|
||||
/**
|
||||
* Pop the activated span off the current context store, restoring the
|
||||
* prior context. Does NOT end the span (the owning SpanGuard does that).
|
||||
* A null activation is a no-op.
|
||||
*/
|
||||
~ScopedActivation();
|
||||
|
||||
ScopedActivation(ScopedActivation&&) = delete;
|
||||
ScopedActivation&
|
||||
operator=(ScopedActivation&&) = delete;
|
||||
ScopedActivation(ScopedActivation const&) = delete;
|
||||
ScopedActivation&
|
||||
operator=(ScopedActivation const&) = delete;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// No-op stub (all inline, zero overhead, no OTel dependency)
|
||||
// ---------------------------------------------------------------------------
|
||||
#else // XRPL_ENABLE_TELEMETRY not defined
|
||||
|
||||
/**
|
||||
* No-op activation used when telemetry is disabled at compile time. Holds no
|
||||
* state and does nothing on construction or destruction. Declared before
|
||||
* SpanGuard so its inline activate() can return one by value. Non-copyable and
|
||||
* non-movable to match the real ScopedActivation.
|
||||
*/
|
||||
class ScopedActivation
|
||||
{
|
||||
public:
|
||||
ScopedActivation() = default;
|
||||
~ScopedActivation() = default;
|
||||
ScopedActivation(ScopedActivation&&) = delete;
|
||||
ScopedActivation&
|
||||
operator=(ScopedActivation&&) = delete;
|
||||
ScopedActivation(ScopedActivation const&) = delete;
|
||||
ScopedActivation&
|
||||
operator=(ScopedActivation const&) = delete;
|
||||
};
|
||||
|
||||
class SpanGuard
|
||||
{
|
||||
public:
|
||||
@@ -818,6 +930,14 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
// NOLINTBEGIN(readability-convert-member-functions-to-static)
|
||||
[[nodiscard]] ScopedActivation
|
||||
activate() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
// NOLINTEND(readability-convert-member-functions-to-static)
|
||||
|
||||
explicit
|
||||
operator bool() const
|
||||
{
|
||||
|
||||
@@ -634,6 +634,64 @@ operator bool() const
|
||||
return impl_ && static_cast<bool>(impl_->guard);
|
||||
}
|
||||
|
||||
// ===== ScopedActivation ====================================================
|
||||
|
||||
struct ScopedActivation::Impl
|
||||
{
|
||||
/**
|
||||
* Active OTel Scope binding an externally-owned span to the current
|
||||
* context store. Popped on destruction; never ends the span.
|
||||
*/
|
||||
otel_trace::Scope scope;
|
||||
|
||||
/**
|
||||
* Identity of the LocalValue store the Scope was pushed onto. The Scope
|
||||
* must pop while the SAME store is active (see
|
||||
* ScopedSpanGuard::ScopedImpl::owner). Initialized AFTER `scope` in the
|
||||
* member init list: members initialize in declaration order, so the
|
||||
* Scope's push (the first LocalValue touch on a fresh worker, which
|
||||
* materializes the store) runs first, then this captures the
|
||||
* now-materialized store. Capturing before the push would record
|
||||
* nullptr while the destructor sees the materialized store.
|
||||
*/
|
||||
void const* owner;
|
||||
|
||||
/**
|
||||
* Push an externally-owned span onto the current context store.
|
||||
* @param span The already-owned span to activate (not owned here).
|
||||
*/
|
||||
explicit Impl(opentelemetry::nostd::shared_ptr<otel_trace::Span> const& span)
|
||||
: scope(span), owner(detail::getLocalValues().get())
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
ScopedActivation::ScopedActivation() = default;
|
||||
|
||||
ScopedActivation::ScopedActivation(std::unique_ptr<Impl> impl) : impl_(std::move(impl))
|
||||
{
|
||||
}
|
||||
|
||||
ScopedActivation::~ScopedActivation()
|
||||
{
|
||||
// A live Scope must be popped while its constructing context store is
|
||||
// active; destroying the activation under a different store corrupts
|
||||
// that store's context stack. A null activation holds no Scope, so the
|
||||
// check is skipped.
|
||||
XRPL_ASSERT(
|
||||
!impl_ || impl_->owner == detail::getLocalValues().get(),
|
||||
"xrpl::telemetry::ScopedActivation::~ScopedActivation : destroyed on the "
|
||||
"constructing context store");
|
||||
}
|
||||
|
||||
ScopedActivation
|
||||
SpanGuard::activate() const
|
||||
{
|
||||
if (!impl_ || !impl_->span)
|
||||
return {};
|
||||
return ScopedActivation(std::make_unique<ScopedActivation::Impl>(impl_->span));
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
|
||||
Reference in New Issue
Block a user