mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-23 23:20:33 +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:
170
include/xrpl/telemetry/DeterministicIdGenerator.h
Normal file
170
include/xrpl/telemetry/DeterministicIdGenerator.h
Normal file
@@ -0,0 +1,170 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <opentelemetry/sdk/trace/id_generator.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
/**
|
||||
* OTel IdGenerator that can mint a deterministic (hash-derived) trace_id.
|
||||
*
|
||||
* By default the OTel SDK generates random trace_ids, so a span derived from
|
||||
* a stable hash (e.g. a transaction id) cannot become a real trace root: the
|
||||
* SDK either inherits the active span as parent or invents a random root id.
|
||||
* This generator lets a caller pin the trace_id of the next forced-root span
|
||||
* to a chosen 16-byte value, so hash-derived spans line up across nodes into
|
||||
* one trace. When no value is pinned it behaves exactly like the default
|
||||
* random generator.
|
||||
*
|
||||
* The pinned value lives in a thread-local slot set by PendingTraceId (below).
|
||||
* Only GenerateTraceId() consults that slot, and only on the SDK's no-parent
|
||||
* (root) branch; span_ids are always random. is_random_ is false so the W3C
|
||||
* random-trace-id flag is not set on these deterministic ids.
|
||||
*
|
||||
* Dependency / data-flow diagram:
|
||||
*
|
||||
* +-----------------------------------------------------------+
|
||||
* | DeterministicIdGenerator |
|
||||
* | (IdGenerator) |
|
||||
* +-----------------------------------------------------------+
|
||||
* | - random_ : unique_ptr<IdGenerator> (random delegate) |
|
||||
* +-----------------------------------------------------------+
|
||||
* | GenerateTraceId(): |
|
||||
* | thread-local pending id set? --yes--> return that id |
|
||||
* | --no --> random_ ... |
|
||||
* | GenerateSpanId(): always ---------------> random_ ... |
|
||||
* +-----------------------------------------------------------+
|
||||
* ^ |
|
||||
* sets/clears| delegates|
|
||||
* | v
|
||||
* +----------------+ +--------------------+
|
||||
* | PendingTraceId | | RandomIdGenerator |
|
||||
* | (RAII guard) | | (delegate) |
|
||||
* +----------------+ +--------------------+
|
||||
*
|
||||
* @note Thread safety: the pending trace_id is a file-local thread_local, so
|
||||
* each thread sees only its own pinned value and no synchronization is needed.
|
||||
* The pending id is consumed ONLY by GenerateTraceId(), which the SDK calls
|
||||
* solely when a span has no valid parent; GenerateSpanId() never reads it and
|
||||
* is always random.
|
||||
* @note Limitation: minting a deterministic root only works when the caller
|
||||
* forces the SDK's root branch (e.g. by starting the span with a
|
||||
* Context{kIsRootSpanKey, true}); otherwise the SDK reuses the parent's
|
||||
* trace_id and never calls GenerateTraceId(). The primary such caller is
|
||||
* SpanGuard::hashSpan(), which forces the root branch to mint deterministic
|
||||
* per-object trace roots.
|
||||
*
|
||||
* Example 1 - primary use, inside a forced-root hash span (shown as
|
||||
* pseudocode):
|
||||
* @code
|
||||
* // std::array<std::uint8_t, 16> id = deriveTraceIdFromHash(txHash);
|
||||
* // PendingTraceId const pending{id}; // pin id for this thread
|
||||
* // auto root = Context{kIsRootSpanKey, true}; // force the no-parent branch
|
||||
* // auto guard = telemetry.startSpan("tx.process", root);
|
||||
* // // GenerateTraceId() returns `id`; the guard's trace_id == id.
|
||||
* @endcode
|
||||
*
|
||||
* Example 2 - edge case, a normal child span never consults the pending id:
|
||||
* @code
|
||||
* // With an active parent span, startSpan() inherits the parent's trace_id
|
||||
* // and the SDK does NOT call GenerateTraceId(), so no PendingTraceId is used.
|
||||
* // auto child = parentGuard.childSpan("subtask"); // random/parent trace_id
|
||||
* @endcode
|
||||
*/
|
||||
class DeterministicIdGenerator final : public opentelemetry::sdk::trace::IdGenerator
|
||||
{
|
||||
/**
|
||||
* Random generator the deterministic path falls back to. Used for every
|
||||
* span_id and for any trace_id when no PendingTraceId is active.
|
||||
*/
|
||||
std::unique_ptr<opentelemetry::sdk::trace::IdGenerator> random_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Build a generator with is_random_ = false and a random delegate.
|
||||
*/
|
||||
DeterministicIdGenerator();
|
||||
|
||||
/**
|
||||
* @return the thread's pending trace_id if a PendingTraceId is active,
|
||||
* otherwise a fresh random trace_id from the delegate. Consuming the
|
||||
* pending id clears it so it applies to exactly one root span.
|
||||
*/
|
||||
opentelemetry::trace::TraceId
|
||||
GenerateTraceId() noexcept override;
|
||||
|
||||
/**
|
||||
* @return a fresh random span_id. Never uses the pending trace_id.
|
||||
*/
|
||||
opentelemetry::trace::SpanId
|
||||
GenerateSpanId() noexcept override;
|
||||
};
|
||||
|
||||
/**
|
||||
* RAII guard that pins a deterministic trace_id for the next forced-root span
|
||||
* started on this thread.
|
||||
*
|
||||
* Mirrors DiscardScope: it sets a thread-local pending trace_id on
|
||||
* construction and clears it on destruction, so the pinned id stays confined
|
||||
* to the guard's scope and cannot leak onto a later span. On destruction it
|
||||
* asserts that the id was actually consumed by GenerateTraceId() — if it was
|
||||
* not, the SDK took a branch other than the root branch the caller intended,
|
||||
* which is a bug worth catching in debug/test builds.
|
||||
*
|
||||
* Wrap ONLY the single forced-root startSpan() call that must receive the
|
||||
* deterministic id. Non-copyable and non-movable: its sole purpose is the
|
||||
* scoped lifetime of the pending id.
|
||||
*
|
||||
* @note Thread safety: the pending id is thread-local, so a guard on one
|
||||
* thread never affects another. Construct and destroy the guard on the same
|
||||
* thread as the startSpan() call it wraps.
|
||||
* @note Limitation: the consumed-assert only holds when the wrapped span is
|
||||
* started on the SDK root branch (Context{kIsRootSpanKey, true}); wrapping a
|
||||
* child span would leave the id unconsumed and trip the assert. Nesting two
|
||||
* guards on one thread is unsupported: the inner guard consumes/clears first,
|
||||
* so the outer id is silently dropped and its destructor trips the assert.
|
||||
*
|
||||
* Example 1 - primary use, wrapping a forced-root span start (pseudocode):
|
||||
* @code
|
||||
* // {
|
||||
* // PendingTraceId const pending{id}; // pin for this scope
|
||||
* // auto root = Context{kIsRootSpanKey, true};
|
||||
* // auto guard = telemetry.startSpan(name, root);// consumes the pinned id
|
||||
* // } // ~PendingTraceId asserts the id was consumed, then clears it
|
||||
* @endcode
|
||||
*
|
||||
* Example 2 - edge case, misuse detection: wrapping a non-root span leaves the
|
||||
* id unconsumed, so ~PendingTraceId trips XRPL_ASSERT in debug/test builds.
|
||||
*/
|
||||
class PendingTraceId
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Pin @p id as the pending trace_id for this thread.
|
||||
* @param id The 16-byte trace_id the next forced-root span should adopt.
|
||||
*/
|
||||
explicit PendingTraceId(std::array<std::uint8_t, 16> const& id) noexcept;
|
||||
|
||||
/**
|
||||
* Assert the pinned id was consumed by a root span, then clear it so it
|
||||
* never leaks onto the next span (even in release, where the assert is a
|
||||
* no-op).
|
||||
*/
|
||||
~PendingTraceId() noexcept;
|
||||
|
||||
PendingTraceId(PendingTraceId const&) = delete;
|
||||
PendingTraceId&
|
||||
operator=(PendingTraceId const&) = delete;
|
||||
PendingTraceId(PendingTraceId&&) = delete;
|
||||
PendingTraceId&
|
||||
operator=(PendingTraceId&&) = delete;
|
||||
};
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
@@ -1,25 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* RAII guard for OpenTelemetry trace spans.
|
||||
* RAII guards for OpenTelemetry trace spans.
|
||||
*
|
||||
* Wraps an OTel Span and Scope behind the pimpl idiom so that no
|
||||
* opentelemetry headers are exposed in this public header. When
|
||||
* XRPL_ENABLE_TELEMETRY is not defined, SpanGuard is an empty class
|
||||
* with all-inline no-op methods — zero overhead, zero dependencies.
|
||||
* This header declares two complementary span guards, split by
|
||||
* responsibility:
|
||||
*
|
||||
* Dependency diagram:
|
||||
* - SpanGuard — owns a span only. Thread-free: it never touches the
|
||||
* OTel thread-local context stack, so it may be moved
|
||||
* 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
|
||||
* non-movable.
|
||||
*
|
||||
* Both wrap all OpenTelemetry types behind the pimpl idiom so no
|
||||
* opentelemetry headers are exposed here. When XRPL_ENABLE_TELEMETRY
|
||||
* is not defined, both are empty classes with all-inline no-op methods
|
||||
* — zero overhead, zero dependencies.
|
||||
*
|
||||
* SpanGuard dependency diagram:
|
||||
*
|
||||
* +--------------------------------------------+
|
||||
* | SpanGuard |
|
||||
* | (unscoped, thread-free) |
|
||||
* +--------------------------------------------+
|
||||
* | - impl_ : unique_ptr<Impl> (pimpl) |
|
||||
* +--------------------------------------------+
|
||||
* | + span(cat, prefix, name) [static] |
|
||||
* | + rootSpan(cat, prefix, name) [static] |
|
||||
* | + freshRoot(cat, prefix, name) [static] |
|
||||
* | + childSpan(name) : SpanGuard |
|
||||
* | + linkedSpan(name) : SpanGuard |
|
||||
* | + detached() : SpanGuard |
|
||||
* | + captureContext() : SpanContext |
|
||||
* | + setAttribute(key, value) |
|
||||
* | + setOk() / setError(desc) |
|
||||
@@ -29,14 +42,10 @@
|
||||
* | + operator bool() |
|
||||
* +--------------------------------------------+
|
||||
* | hides (pimpl)
|
||||
* +-------+-------------+
|
||||
* | |
|
||||
* +--------+ +---------------------------+
|
||||
* | Span | | optional<Scope> |
|
||||
* | (OTel) | | (OTel, non-movable) |
|
||||
* | | | present : scoped guard |
|
||||
* | | | nullopt : detached guard |
|
||||
* +--------+ +---------------------------+
|
||||
* +--------+
|
||||
* | Span |
|
||||
* | (OTel) |
|
||||
* +--------+
|
||||
*
|
||||
* Static factory methods access the global Telemetry instance
|
||||
* internally (via Telemetry::getInstance()), check whether tracing
|
||||
@@ -60,7 +69,7 @@
|
||||
* 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
|
||||
* // span ended automatically when the guard is destroyed
|
||||
* @endcode
|
||||
*
|
||||
* 2. Error recording:
|
||||
@@ -110,7 +119,7 @@
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* 6. Fresh trace root at an inbound entry point (primary rootSpan use):
|
||||
* 6. Fresh trace root at an inbound entry point (primary freshRoot use):
|
||||
* @code
|
||||
* #include <xrpld/overlay/detail/PeerSpanNames.h>
|
||||
* using namespace xrpl::telemetry;
|
||||
@@ -119,50 +128,45 @@
|
||||
* // already have unrelated spans active — start a clean root so
|
||||
* // those do not become parents of this trace. Names come from a
|
||||
* // *SpanNames.h header, never raw literals.
|
||||
* auto span = SpanGuard::rootSpan(
|
||||
* auto span = SpanGuard::freshRoot(
|
||||
* TraceCategory::Peer, seg::peer, peer_span::op::validationReceive);
|
||||
* span.setAttribute(peer_span::attr::ledgerHash, hashStr);
|
||||
* @endcode
|
||||
*
|
||||
* 7. Hand a span to a job on another thread (edge case, detached):
|
||||
* 7. Hand a span to a job on another thread (thread-free — just move):
|
||||
* @code
|
||||
* #include <xrpld/app/ledger/detail/LedgerSpanNames.h>
|
||||
* using namespace xrpl::telemetry;
|
||||
*
|
||||
* // Build the guard on THIS thread, then strip its thread-local
|
||||
* // Scope so it can be safely moved into a job and ended there.
|
||||
* // SpanGuard owns no thread-local Scope, so it is safe to move
|
||||
* // into a job and end on the worker thread. No detach step is
|
||||
* // needed — just move the guard in.
|
||||
* auto span = SpanGuard::span(
|
||||
* TraceCategory::Ledger, seg::ledger, ledger_span::op::build);
|
||||
* jobQueue.addJob(
|
||||
* [g = std::move(span).detached()]() mutable {
|
||||
* [g = std::move(span)]() mutable {
|
||||
* doWork();
|
||||
* // g's span ends when the job's lambda is destroyed,
|
||||
* // on the worker thread — no origin-stack corruption.
|
||||
* // on the worker thread.
|
||||
* });
|
||||
* @endcode
|
||||
*
|
||||
* @note Thread safety: A SCOPED guard (from span(), rootSpan(),
|
||||
* childSpan(), linkedSpan()) must only be used on the thread where it
|
||||
* was constructed — its internal Scope binds to that thread's
|
||||
* thread-local context stack, and destroying it elsewhere would pop
|
||||
* the wrong stack. A guard returned by detached() holds no Scope, so
|
||||
* it may be moved to and destroyed on another thread; detached()
|
||||
* itself must be called on the origin (constructing) thread. Use
|
||||
* captureContext() to propagate the trace context to other threads.
|
||||
* Violating this rule is enforced (not just documented): a scoped
|
||||
* guard destroyed on a foreign thread, or detached() called from one,
|
||||
* trips an XRPL_ASSERT in debug/test/fuzzing builds instead of
|
||||
* silently corrupting the other thread's context stack.
|
||||
* @note Thread safety: SpanGuard is thread-free. It holds only the
|
||||
* span (no Scope), so it never binds to a thread-local context stack
|
||||
* and may be moved to and destroyed on any thread. To make a span the
|
||||
* ambient parent for child spans on a thread, wrap it in a
|
||||
* ScopedSpanGuard. Use captureContext() to propagate the trace
|
||||
* context across threads explicitly.
|
||||
*
|
||||
* @note Move semantics: Move construction transfers ownership of
|
||||
* the pimpl pointer — no double-Scope issues. Move assignment is
|
||||
* deleted to prevent re-scoping mid-flight.
|
||||
* @note Move semantics: Move construction and move assignment both
|
||||
* transfer ownership of the pimpl pointer. There is no Scope to
|
||||
* re-bind, so move assignment is safe and enabled. Copy is deleted.
|
||||
*
|
||||
* @note Known limitations:
|
||||
* - Attributes cannot be removed per the OTel spec; use
|
||||
* setAttribute with an empty value as a convention.
|
||||
* - SpanGuard::span() (raw Span access) is intentionally not
|
||||
* exposed — all interaction goes through the public methods.
|
||||
* - The raw OTel Span is intentionally not exposed — all interaction
|
||||
* goes through the public methods.
|
||||
*/
|
||||
|
||||
#include <array>
|
||||
@@ -171,7 +175,6 @@
|
||||
#include <exception>
|
||||
#include <initializer_list>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
@@ -256,9 +259,16 @@ public:
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
/**
|
||||
* RAII wrapper that activates a span on construction and ends it on
|
||||
* destruction. All OTel types are hidden behind the Impl pointer.
|
||||
* Non-copyable, move-constructible.
|
||||
* RAII owner of an OTel span (unscoped, thread-free).
|
||||
*
|
||||
* Holds only the span behind the Impl pointer — it never pushes the
|
||||
* span onto the thread-local context stack, so it carries no
|
||||
* thread-affinity and may be moved to and destroyed on any thread.
|
||||
* The span is ended when the guard is destroyed (unless discard()
|
||||
* ended it earlier). Non-copyable; movable and move-assignable.
|
||||
*
|
||||
* To activate a span as the ambient parent for child spans on a
|
||||
* thread, wrap the guard in a ScopedSpanGuard.
|
||||
*/
|
||||
class SpanGuard
|
||||
{
|
||||
@@ -267,6 +277,10 @@ class SpanGuard
|
||||
|
||||
explicit SpanGuard(std::unique_ptr<Impl> impl);
|
||||
|
||||
// ScopedSpanGuard wraps a SpanGuard and needs the raw span to build
|
||||
// its Scope; it reads impl_ directly so no OTel type leaks here.
|
||||
friend class ScopedSpanGuard;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Construct a null (no-op) guard. All methods are safe to call.
|
||||
@@ -276,7 +290,7 @@ public:
|
||||
|
||||
SpanGuard(SpanGuard&& other) noexcept;
|
||||
SpanGuard&
|
||||
operator=(SpanGuard&&) = delete;
|
||||
operator=(SpanGuard&&) noexcept;
|
||||
SpanGuard(SpanGuard const&) = delete;
|
||||
SpanGuard&
|
||||
operator=(SpanGuard const&) = delete;
|
||||
@@ -308,16 +322,17 @@ public:
|
||||
* @param prefix Span name prefix (e.g. "peer").
|
||||
* @param name Span name suffix (e.g. "validation.receive").
|
||||
* @return An active root-span guard, or a null guard if disabled.
|
||||
* @note Must be called on the thread that will own the span, like
|
||||
* span(); the returned guard is scoped to that thread.
|
||||
*/
|
||||
[[nodiscard]] static SpanGuard
|
||||
rootSpan(TraceCategory cat, std::string_view prefix, std::string_view name);
|
||||
freshRoot(TraceCategory cat, std::string_view prefix, std::string_view name);
|
||||
|
||||
// --- Child / linked span creation ----------------------------------
|
||||
|
||||
/**
|
||||
* Create a child span parented to this guard's active context.
|
||||
* Create a child span parented to the current thread's active
|
||||
* context. This is meaningful when this guard's span is active on
|
||||
* the thread (e.g. wrapped in a ScopedSpanGuard); otherwise the
|
||||
* child inherits whatever context is currently on the stack.
|
||||
* @param name Span name for the child.
|
||||
* @return A new guard, or null if this guard is inactive.
|
||||
*/
|
||||
@@ -352,30 +367,6 @@ public:
|
||||
[[nodiscard]] static SpanGuard
|
||||
linkedSpan(std::string_view name, SpanContext const& linkCtx);
|
||||
|
||||
/**
|
||||
* Detach this guard's span from the current thread's context stack.
|
||||
*
|
||||
* A scoped guard holds an OTel Scope bound to the constructing
|
||||
* thread's context stack. Moving such a guard to another thread
|
||||
* (e.g. into a job queue) and destroying it there would pop the
|
||||
* wrong stack, leaving the origin thread's stack corrupted so
|
||||
* later spans inherit a stale parent. detached() pops the Scope
|
||||
* now, on the origin thread, and returns a new guard that holds
|
||||
* the same span with no thread-local binding.
|
||||
*
|
||||
* Consumes this guard (rvalue-qualified): after the call this
|
||||
* guard is null and the returned guard owns the span.
|
||||
*
|
||||
* @return A scope-less guard safe to move to and destroy on
|
||||
* another thread, or a null guard if this guard was null.
|
||||
* @note Must be called on the origin (constructing) thread; this is
|
||||
* checked by an XRPL_ASSERT in debug/test/fuzzing builds. The
|
||||
* returned guard may be freely moved across threads; only
|
||||
* its final destruction ends the span.
|
||||
*/
|
||||
[[nodiscard]] SpanGuard
|
||||
detached() &&;
|
||||
|
||||
// --- Hash-derived span (category-gated) -----------------------------
|
||||
|
||||
/**
|
||||
@@ -547,42 +538,260 @@ public:
|
||||
operator bool() const;
|
||||
};
|
||||
|
||||
// --- Detach-in-place helpers -----------------------------------------------
|
||||
|
||||
/**
|
||||
* Detach an active `std::optional<SpanGuard>` member in place.
|
||||
* RAII guard that activates a span on the current thread (scoped).
|
||||
*
|
||||
* Equivalent to `guard.emplace(std::move(*guard).detached())`, which is the
|
||||
* required idiom for detaching a live guard stored in an `optional` (move
|
||||
* assignment is deleted because the underlying Scope cannot be re-scoped in
|
||||
* place). No-op if `guard` is empty or already null.
|
||||
* 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
|
||||
* 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(...)`.
|
||||
*
|
||||
* @param guard The optional guard to detach. Must be called on the thread
|
||||
* that constructed the active guard inside it (same rule as
|
||||
* `SpanGuard::detached()`).
|
||||
* @note Must run BEFORE any `captureContext()` snapshot is taken if the
|
||||
* caller also needs the pre-detach context — capture first, then call
|
||||
* this helper (same ordering rule `detached()` itself has).
|
||||
* When the span must outlive the current thread (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
|
||||
* thread-free guard.
|
||||
*
|
||||
* ScopedSpanGuard dependency diagram:
|
||||
*
|
||||
* +--------------------------------------------+
|
||||
* | ScopedSpanGuard |
|
||||
* | (scoped, thread-bound) |
|
||||
* +--------------------------------------------+
|
||||
* | - impl_ : unique_ptr<ScopedImpl> (pimpl) |
|
||||
* +--------------------------------------------+
|
||||
* | + (cat, prefix, name) [ctor] |
|
||||
* | + freshRoot(cat, prefix, name) [static] |
|
||||
* | + childSpan(name) : ScopedSpanGuard |
|
||||
* | + linkedSpan(name) : ScopedSpanGuard |
|
||||
* | + operator SpanGuard() && (handoff) |
|
||||
* | + setAttribute / setOk / setError / ... |
|
||||
* | + captureContext() / discard() |
|
||||
* | + operator bool() |
|
||||
* +--------------------------------------------+
|
||||
* | hides (pimpl)
|
||||
* +------+--------------------+
|
||||
* | |
|
||||
* +----------+ +-----------------------------+
|
||||
* | SpanGuard| | optional<Scope> |
|
||||
* | (span) | | (OTel, thread-bound; |
|
||||
* | | | present : span active) |
|
||||
* +----------+ +-----------------------------+
|
||||
*
|
||||
* Usage examples:
|
||||
*
|
||||
* 1. Same-thread scoped tracing (child inherits parent automatically):
|
||||
* @code
|
||||
* using namespace xrpl::telemetry;
|
||||
*
|
||||
* ScopedSpanGuard span(
|
||||
* TraceCategory::Rpc, rpc_span::prefix::command, commandName);
|
||||
* span.setAttribute(rpc_span::attr::command, commandName);
|
||||
* // childSpan parents to `span` because it is active on this thread
|
||||
* auto child = span.childSpan(rpc_span::op::dispatch);
|
||||
* @endcode
|
||||
*
|
||||
* 2. Capture on this thread, hand off to another (edge case):
|
||||
* @code
|
||||
* using namespace xrpl::telemetry;
|
||||
*
|
||||
* auto scoped = ScopedSpanGuard::freshRoot(
|
||||
* TraceCategory::Ledger, seg::ledger, ledger_span::op::build);
|
||||
* // Pop the scope now, on this thread, and move a thread-free
|
||||
* // SpanGuard into the job.
|
||||
* SpanGuard span = std::move(scoped);
|
||||
* jobQueue.addJob([g = std::move(span)]() mutable { doWork(); });
|
||||
* @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.
|
||||
*
|
||||
* @note Known limitations:
|
||||
* - Non-movable: cannot be stored in a container that relocates, and
|
||||
* cannot be returned then move-assigned into an existing variable.
|
||||
* Store the thread-free SpanGuard (via `operator SpanGuard() &&`)
|
||||
* instead when a movable handle is needed.
|
||||
* - Same attribute-removal limitation as SpanGuard.
|
||||
*/
|
||||
void
|
||||
detachInPlace(std::optional<SpanGuard>& guard);
|
||||
class ScopedSpanGuard
|
||||
{
|
||||
struct ScopedImpl;
|
||||
std::unique_ptr<ScopedImpl> impl_;
|
||||
|
||||
/**
|
||||
* Detach an active `std::shared_ptr<SpanGuard>`, returning the detached
|
||||
* guard as a new `shared_ptr`.
|
||||
*
|
||||
* Equivalent to
|
||||
* `guard = std::make_shared<SpanGuard>(std::move(*guard).detached())`.
|
||||
* No-op (returns the input unchanged) if `guard` is null or points at a
|
||||
* null guard.
|
||||
*
|
||||
* @param guard The guard to detach, taken by value (the caller's pointer
|
||||
* is consumed; assign the return value back).
|
||||
* @return A `shared_ptr` to the detached guard (new allocation), or the
|
||||
* input pointer unchanged if it was null/inactive.
|
||||
*/
|
||||
[[nodiscard]] std::shared_ptr<SpanGuard>
|
||||
detachInPlace(std::shared_ptr<SpanGuard> guard);
|
||||
explicit ScopedSpanGuard(SpanGuard&& guard);
|
||||
|
||||
public:
|
||||
/**
|
||||
* Create a scoped span guarded by a TraceCategory flag and activate
|
||||
* it on this thread. Equivalent to SpanGuard::span() wrapped in an
|
||||
* active Scope. The span name is built as "prefix.name".
|
||||
* @param cat Trace subsystem category.
|
||||
* @param prefix Span name prefix (e.g. "rpc.command").
|
||||
* @param name Span name suffix (e.g. "submit").
|
||||
*/
|
||||
ScopedSpanGuard(TraceCategory cat, std::string_view prefix, std::string_view name);
|
||||
|
||||
~ScopedSpanGuard();
|
||||
|
||||
ScopedSpanGuard(ScopedSpanGuard&&) = delete;
|
||||
ScopedSpanGuard&
|
||||
operator=(ScopedSpanGuard&&) = delete;
|
||||
ScopedSpanGuard(ScopedSpanGuard const&) = delete;
|
||||
ScopedSpanGuard&
|
||||
operator=(ScopedSpanGuard const&) = delete;
|
||||
|
||||
// --- Static factory methods ----------------------------------------
|
||||
|
||||
/**
|
||||
* Create a scoped span that always starts a fresh trace root and
|
||||
* activate it on this thread. See SpanGuard::freshRoot().
|
||||
* @param cat Trace subsystem category.
|
||||
* @param prefix Span name prefix.
|
||||
* @param name Span name suffix.
|
||||
* @return An active scoped root-span guard, or a null one if disabled.
|
||||
*/
|
||||
[[nodiscard]] static ScopedSpanGuard
|
||||
freshRoot(TraceCategory cat, std::string_view prefix, std::string_view name);
|
||||
|
||||
// --- Child / linked span creation ----------------------------------
|
||||
|
||||
/**
|
||||
* Create a scoped child span parented to this guard's active span
|
||||
* and activate it on this thread.
|
||||
* @param name Span name for the child.
|
||||
* @return A new scoped guard, or a null one if this guard is inactive.
|
||||
*/
|
||||
[[nodiscard]] ScopedSpanGuard
|
||||
childSpan(std::string_view name) const;
|
||||
|
||||
/**
|
||||
* Create a scoped child span parented to an explicit captured
|
||||
* context and activate it on this thread.
|
||||
* @param name Span name for the child.
|
||||
* @param parentCtx Context captured via captureContext().
|
||||
* @return A new scoped guard, or a null one if parentCtx is invalid.
|
||||
*/
|
||||
[[nodiscard]] static ScopedSpanGuard
|
||||
childSpan(std::string_view name, SpanContext const& parentCtx);
|
||||
|
||||
/**
|
||||
* Create a scoped span linked (follows-from) to this guard's span
|
||||
* and activate it on this thread.
|
||||
* @param name Span name for the linked span.
|
||||
* @return A new scoped guard, or a null one if this guard is inactive.
|
||||
*/
|
||||
[[nodiscard]] ScopedSpanGuard
|
||||
linkedSpan(std::string_view name) const;
|
||||
|
||||
/**
|
||||
* Create a scoped span linked to an explicit captured context and
|
||||
* activate it on this thread.
|
||||
* @param name Span name for the linked span.
|
||||
* @param linkCtx Context to link from.
|
||||
* @return A new scoped guard, or a null one if linkCtx is invalid.
|
||||
*/
|
||||
[[nodiscard]] static ScopedSpanGuard
|
||||
linkedSpan(std::string_view name, SpanContext const& linkCtx);
|
||||
|
||||
// --- 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.
|
||||
* @return The unscoped SpanGuard that now owns the span.
|
||||
*/
|
||||
operator SpanGuard() &&;
|
||||
|
||||
// --- Context capture -----------------------------------------------
|
||||
|
||||
/**
|
||||
* Snapshot the current thread's OTel context for cross-thread use.
|
||||
* @return An opaque SpanContext, or an invalid one if null guard.
|
||||
*/
|
||||
[[nodiscard]] SpanContext
|
||||
captureContext() const;
|
||||
|
||||
// --- Forwarding methods (delegate to the owned SpanGuard) ----------
|
||||
|
||||
/**
|
||||
* Set a string attribute. No-op on a null guard.
|
||||
*/
|
||||
void
|
||||
setAttribute(std::string_view key, std::string_view value);
|
||||
|
||||
/**
|
||||
* Set a string attribute (C-string overload). No-op on a null guard.
|
||||
*/
|
||||
void
|
||||
setAttribute(std::string_view key, char const* value);
|
||||
|
||||
/**
|
||||
* Set an integer attribute. No-op on a null guard.
|
||||
*/
|
||||
void
|
||||
setAttribute(std::string_view key, std::int64_t value);
|
||||
|
||||
/**
|
||||
* Set a floating-point attribute. No-op on a null guard.
|
||||
*/
|
||||
void
|
||||
setAttribute(std::string_view key, double value);
|
||||
|
||||
/**
|
||||
* Set a boolean attribute. No-op on a null guard.
|
||||
*/
|
||||
void
|
||||
setAttribute(std::string_view key, bool value);
|
||||
|
||||
/**
|
||||
* Mark the span status as OK. No-op on a null guard.
|
||||
*/
|
||||
void
|
||||
setOk();
|
||||
|
||||
/**
|
||||
* Mark the span status as error. No-op on a null guard.
|
||||
* @param description Optional human-readable error description.
|
||||
*/
|
||||
void
|
||||
setError(std::string_view description = "");
|
||||
|
||||
/**
|
||||
* Add a named event to the span's timeline. No-op on a null guard.
|
||||
* @param name Event name.
|
||||
*/
|
||||
void
|
||||
addEvent(std::string_view name);
|
||||
|
||||
/**
|
||||
* Record an exception as a span event and mark status as error.
|
||||
* No-op on a null guard.
|
||||
* @param e The exception to record.
|
||||
*/
|
||||
void
|
||||
recordException(std::exception const& e);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
void
|
||||
discard();
|
||||
|
||||
/**
|
||||
* @return true if this guard holds an active span.
|
||||
*/
|
||||
explicit
|
||||
operator bool() const;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// No-op stub (all inline, zero overhead, no OTel dependency)
|
||||
@@ -596,7 +805,7 @@ public:
|
||||
~SpanGuard() = default;
|
||||
SpanGuard(SpanGuard&&) noexcept = default;
|
||||
SpanGuard&
|
||||
operator=(SpanGuard&&) = delete;
|
||||
operator=(SpanGuard&&) noexcept = default;
|
||||
SpanGuard(SpanGuard const&) = delete;
|
||||
SpanGuard&
|
||||
operator=(SpanGuard const&) = delete;
|
||||
@@ -608,7 +817,7 @@ public:
|
||||
}
|
||||
|
||||
[[nodiscard]] static SpanGuard
|
||||
rootSpan(TraceCategory, std::string_view, std::string_view)
|
||||
freshRoot(TraceCategory, std::string_view, std::string_view)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
@@ -635,12 +844,6 @@ public:
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] SpanGuard
|
||||
detached() &&
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] static SpanGuard
|
||||
hashSpan(
|
||||
TraceCategory,
|
||||
@@ -734,18 +937,114 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// --- Detach-in-place helpers (no-op stubs) ---------------------------------
|
||||
|
||||
inline void
|
||||
detachInPlace(std::optional<SpanGuard>&)
|
||||
class ScopedSpanGuard
|
||||
{
|
||||
}
|
||||
// Private default ctor lets the inline factories/child methods build
|
||||
// a no-op instance without exposing a public default constructor
|
||||
// (mirrors the real class, which has no public default ctor).
|
||||
ScopedSpanGuard() = default;
|
||||
|
||||
[[nodiscard]] inline std::shared_ptr<SpanGuard>
|
||||
detachInPlace(std::shared_ptr<SpanGuard> guard)
|
||||
{
|
||||
return guard;
|
||||
}
|
||||
public:
|
||||
ScopedSpanGuard(TraceCategory, std::string_view, std::string_view)
|
||||
{
|
||||
}
|
||||
~ScopedSpanGuard() = default;
|
||||
|
||||
ScopedSpanGuard(ScopedSpanGuard&&) = delete;
|
||||
ScopedSpanGuard&
|
||||
operator=(ScopedSpanGuard&&) = delete;
|
||||
ScopedSpanGuard(ScopedSpanGuard const&) = delete;
|
||||
ScopedSpanGuard&
|
||||
operator=(ScopedSpanGuard const&) = delete;
|
||||
|
||||
[[nodiscard]] static ScopedSpanGuard
|
||||
freshRoot(TraceCategory, std::string_view, std::string_view)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
// NOLINTBEGIN(readability-convert-member-functions-to-static)
|
||||
[[nodiscard]] ScopedSpanGuard
|
||||
childSpan(std::string_view) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
[[nodiscard]] static ScopedSpanGuard
|
||||
childSpan(std::string_view, SpanContext const&)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
[[nodiscard]] ScopedSpanGuard
|
||||
linkedSpan(std::string_view) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
[[nodiscard]] static ScopedSpanGuard
|
||||
linkedSpan(std::string_view, SpanContext const&)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] SpanContext
|
||||
captureContext() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
// NOLINTEND(readability-convert-member-functions-to-static)
|
||||
|
||||
operator SpanGuard() &&
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void
|
||||
setAttribute(std::string_view, std::string_view)
|
||||
{
|
||||
}
|
||||
void
|
||||
setAttribute(std::string_view, char const*)
|
||||
{
|
||||
}
|
||||
void
|
||||
setAttribute(std::string_view, std::int64_t)
|
||||
{
|
||||
}
|
||||
void
|
||||
setAttribute(std::string_view, double)
|
||||
{
|
||||
}
|
||||
void
|
||||
setAttribute(std::string_view, bool)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
setOk()
|
||||
{
|
||||
}
|
||||
void
|
||||
setError(std::string_view = "")
|
||||
{
|
||||
}
|
||||
void
|
||||
addEvent(std::string_view)
|
||||
{
|
||||
}
|
||||
void
|
||||
recordException(std::exception const&)
|
||||
{
|
||||
}
|
||||
void
|
||||
discard()
|
||||
{
|
||||
}
|
||||
|
||||
explicit
|
||||
operator bool() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
|
||||
|
||||
Reference in New Issue
Block a user