mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-23 15:10:34 +00:00
Merge branch 'pratik/otel-phase1b-telemetry-infra' into pratik/otel-phase1c-rpc-integration
Brings coroutine-aware OTel context storage forward: CoroAwareContextStorage, its install in Telemetry, ScopedSpanGuard same-store assertion, and the non-owning ScopedActivation helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
121
include/xrpl/telemetry/CoroAwareContextStorage.h
Normal file
121
include/xrpl/telemetry/CoroAwareContextStorage.h
Normal file
@@ -0,0 +1,121 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* A coroutine-aware OpenTelemetry runtime-context storage.
|
||||
*
|
||||
* OpenTelemetry's default ThreadLocalContextStorage keeps the active-context
|
||||
* stack in a plain static thread_local, so the ambient span does NOT follow a
|
||||
* JobQueue::Coro across yield/resume — a scope pushed on one worker is stranded
|
||||
* when the coroutine resumes on another. This storage instead keeps the stack
|
||||
* in an xrpl::LocalValue, which JobQueue::Coro::resume() swaps in and out with
|
||||
* the coroutine (see Coro.ipp). The active context therefore rides the
|
||||
* coroutine: scopes held across yield are safe, and per-line log-trace
|
||||
* correlation (Log.cpp reads RuntimeContext::GetCurrent()) is retained.
|
||||
*
|
||||
* Off a coroutine, LocalValue transparently provides a per-thread store, so
|
||||
* behaviour is identical to the default thread-local storage.
|
||||
*
|
||||
* +-------------------------------------------------+
|
||||
* | CoroAwareContextStorage |
|
||||
* | (opentelemetry RuntimeContextStorage) |
|
||||
* +-------------------------------------------------+
|
||||
* | - stack_ : LocalValue<vector<Context>> |
|
||||
* +-------------------------------------------------+
|
||||
* | + GetCurrent() : Context |
|
||||
* | + Attach(ctx) : unique_ptr<Token> |
|
||||
* | + Detach(token): bool |
|
||||
* +-------------------------------------------------+
|
||||
* | backed by (coro-aware)
|
||||
* +------------------------+
|
||||
* | xrpl::LocalValue store |
|
||||
* | (swapped by Coro) |
|
||||
* +------------------------+
|
||||
*
|
||||
* Install once at telemetry start via
|
||||
* opentelemetry::context::RuntimeContext::SetRuntimeContextStorage(), BEFORE
|
||||
* any span is created (SDK requirement).
|
||||
*
|
||||
* @note Thread-safety: each thread/coroutine sees its own LocalValue store, so
|
||||
* the stack is never shared across threads — no locking needed. The storage
|
||||
* object itself is stateless apart from the LocalValue handle.
|
||||
* @note Known limitation: install once, before the first span; resetting the
|
||||
* storage while spans exist is undefined behaviour (SDK). A span created on a
|
||||
* raw worker thread and later moved onto a coroutine does not retro-attach to
|
||||
* the coroutine's store — not a pattern here (spans are created inside their
|
||||
* own coro/job body).
|
||||
*
|
||||
* Example 1 — install at telemetry start (primary use):
|
||||
* @code
|
||||
* using opentelemetry::context::RuntimeContext;
|
||||
* RuntimeContext::SetRuntimeContextStorage(
|
||||
* opentelemetry::nostd::shared_ptr<
|
||||
* opentelemetry::context::RuntimeContextStorage>(
|
||||
* new xrpl::telemetry::CoroAwareContextStorage()));
|
||||
* @endcode
|
||||
*
|
||||
* Example 2 — a scope held across a coroutine yield stays correct:
|
||||
* @code
|
||||
* // Inside a JobQueue::Coro body:
|
||||
* auto span = ScopedSpanGuard(TraceCategory::Rpc, "rpc", "process");
|
||||
* context.coro->yield(); // may resume on another worker
|
||||
* // span is still the ambient context here — the storage rode the coro.
|
||||
* @endcode
|
||||
*
|
||||
* Example 3 — edge case: off a coroutine, behaves like thread-local storage:
|
||||
* @code
|
||||
* // On a plain worker thread (no coro): each thread has its own stack,
|
||||
* // identical to opentelemetry's default ThreadLocalContextStorage.
|
||||
* auto span = ScopedSpanGuard(TraceCategory::Ledger, "ledger", "build");
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/basics/LocalValue.h>
|
||||
|
||||
#include <opentelemetry/context/context.h>
|
||||
#include <opentelemetry/context/runtime_context.h>
|
||||
#include <opentelemetry/nostd/unique_ptr.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
class CoroAwareContextStorage : public opentelemetry::context::RuntimeContextStorage
|
||||
{
|
||||
public:
|
||||
CoroAwareContextStorage() = default;
|
||||
|
||||
/**
|
||||
* @return the current (top-of-stack) context for this coro/thread.
|
||||
*/
|
||||
opentelemetry::context::Context
|
||||
GetCurrent() noexcept override;
|
||||
|
||||
/**
|
||||
* Push a context frame onto this coro/thread's stack.
|
||||
* @param context the context to make current.
|
||||
* @return a token that Detach() uses to pop back to the prior frame.
|
||||
*/
|
||||
opentelemetry::nostd::unique_ptr<opentelemetry::context::Token>
|
||||
Attach(opentelemetry::context::Context const& context) noexcept override;
|
||||
|
||||
/**
|
||||
* Pop the stack back through the frame the token refers to.
|
||||
* @param token a token returned by Attach().
|
||||
* @return true if the frame was found and detached.
|
||||
*/
|
||||
bool
|
||||
Detach(opentelemetry::context::Token& token) noexcept override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* The active-context stack, stored coro-locally. LocalValue hands back the
|
||||
* stack for whichever store (coro or thread) is currently installed.
|
||||
*/
|
||||
LocalValue<std::vector<opentelemetry::context::Context>> stack_;
|
||||
};
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
@@ -11,9 +11,11 @@
|
||||
* to and destroyed on any thread. Movable AND
|
||||
* move-assignable.
|
||||
* - ScopedSpanGuard — owns a SpanGuard plus an active OTel Scope that
|
||||
* pushes the span onto the constructing thread's
|
||||
* context stack. Thread-bound: it must be created and
|
||||
* destroyed on the same thread. Non-copyable and
|
||||
* pushes the span onto the constructing context store's
|
||||
* stack. Store-bound: it must be created and destroyed
|
||||
* while the same LocalValue context store is active (the
|
||||
* same thread, or the same JobQueue coroutine even if it
|
||||
* resumes on another worker). Non-copyable and
|
||||
* non-movable.
|
||||
*
|
||||
* Both wrap all OpenTelemetry types behind the pimpl idiom so no
|
||||
@@ -40,6 +42,7 @@
|
||||
* | + addEvent(name) |
|
||||
* | + recordException(e) |
|
||||
* | + discard() |
|
||||
* | + activate() : ScopedActivation |
|
||||
* | + operator bool() |
|
||||
* +--------------------------------------------+
|
||||
* | hides (pimpl)
|
||||
@@ -231,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).
|
||||
*
|
||||
@@ -438,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.
|
||||
*/
|
||||
@@ -446,26 +465,26 @@ public:
|
||||
};
|
||||
|
||||
/**
|
||||
* RAII guard that activates a span on the current thread (scoped).
|
||||
* RAII guard that activates a span on the current context store (scoped).
|
||||
*
|
||||
* Wraps a SpanGuard (which owns the span) plus an OTel Scope that
|
||||
* pushes the span onto this thread's thread-local context stack for
|
||||
* the guard's lifetime, so child spans created on the thread inherit
|
||||
* pushes the span onto the active context store's stack for the
|
||||
* guard's lifetime, so child spans created under that store inherit
|
||||
* it as parent. On destruction the Scope pops BEFORE the span ends
|
||||
* (member order: guard first, scope second). Non-copyable and
|
||||
* non-movable — factories return unnamed temporaries, so guaranteed
|
||||
* copy elision (C++17) covers `auto s = ScopedSpanGuard::freshRoot(...)`.
|
||||
*
|
||||
* When the span must outlive the current thread (e.g. handed to a job
|
||||
* When the span must outlive the current store (e.g. handed to a job
|
||||
* queue), convert to a plain SpanGuard with `operator SpanGuard() &&`:
|
||||
* that pops the Scope eagerly on the origin thread and yields a
|
||||
* that pops the Scope eagerly under the constructing store and yields a
|
||||
* thread-free guard.
|
||||
*
|
||||
* ScopedSpanGuard dependency diagram:
|
||||
*
|
||||
* +--------------------------------------------+
|
||||
* | ScopedSpanGuard |
|
||||
* | (scoped, thread-bound) |
|
||||
* | (scoped, store-bound) |
|
||||
* +--------------------------------------------+
|
||||
* | - impl_ : unique_ptr<ScopedImpl> (pimpl) |
|
||||
* +--------------------------------------------+
|
||||
@@ -483,7 +502,7 @@ public:
|
||||
* | |
|
||||
* +----------+ +-----------------------------+
|
||||
* | SpanGuard| | optional<Scope> |
|
||||
* | (span) | | (OTel, thread-bound; |
|
||||
* | (span) | | (OTel, store-bound; |
|
||||
* | | | present : span active) |
|
||||
* +----------+ +-----------------------------+
|
||||
*
|
||||
@@ -513,11 +532,14 @@ public:
|
||||
* @endcode
|
||||
*
|
||||
* @note Thread safety: A ScopedSpanGuard must be constructed AND
|
||||
* destroyed on the same thread — its Scope binds to that thread's
|
||||
* context stack. `operator SpanGuard() &&` and the destructor must
|
||||
* both run on the owning thread; both check this with an XRPL_ASSERT
|
||||
* in debug/test/fuzzing builds. `operator SpanGuard() &&` pops the
|
||||
* scope eagerly, so the resulting SpanGuard is thread-free.
|
||||
* destroyed while the same LocalValue context store is active — its
|
||||
* Scope binds to that store's context stack. This means the same
|
||||
* thread, or the same JobQueue coroutine even if it resumes on another
|
||||
* worker (coroutine-aware storage keeps the same store across the
|
||||
* resume). `operator SpanGuard() &&` and the destructor must both run
|
||||
* under the constructing store; both check this with an XRPL_ASSERT in
|
||||
* debug/test/fuzzing builds. `operator SpanGuard() &&` pops the scope
|
||||
* eagerly, so the resulting SpanGuard is thread-free.
|
||||
*
|
||||
* @note Known limitations:
|
||||
* - Non-movable: cannot be stored in a container that relocates, and
|
||||
@@ -609,9 +631,10 @@ public:
|
||||
// --- Handoff bridge -------------------------------------------------
|
||||
|
||||
/**
|
||||
* Pop the active Scope on the origin thread and yield the span as a
|
||||
* thread-free SpanGuard. Use to move a span into a job or onto
|
||||
* another thread. Must be called on the constructing thread.
|
||||
* Pop the active Scope under the constructing context store and yield
|
||||
* the span as a thread-free SpanGuard. Use to move a span into a job or
|
||||
* onto another thread. Must be called while the constructing context
|
||||
* store is active.
|
||||
* @return The unscoped SpanGuard that now owns the span.
|
||||
*/
|
||||
operator SpanGuard() &&;
|
||||
@@ -692,8 +715,9 @@ public:
|
||||
|
||||
/**
|
||||
* Mark this span for discard and end it immediately. discard() pops the
|
||||
* thread-local scope right away (on the owning thread) and discards the
|
||||
* span. After discard() the guard is inert and its destructor is a no-op.
|
||||
* scope right away (under the constructing context store) and discards
|
||||
* the span. After discard() the guard is inert and its destructor is a
|
||||
* no-op.
|
||||
*/
|
||||
void
|
||||
discard();
|
||||
@@ -705,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:
|
||||
@@ -811,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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user