mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-23 15:10:34 +00:00
feat(telemetry): add DeterministicIdGenerator + PendingTraceId for true-root deterministic trace_ids
Add a custom OTel IdGenerator that returns a thread-local pending trace_id on the SDK no-parent (root) branch and a random one otherwise, plus a PendingTraceId RAII guard that pins that id for the next forced-root span and asserts on destruction that it was consumed. Wire the generator into TracerProviderFactory::Create via its 4-arg overload. This lets hash-derived spans become true trace roots so they line up into one trace across nodes. It is installed but dormant on this branch: the caller (hashSpan) arrives on a later branch (phase-3). GenerateSpanId is always random and is_random_ is false so the W3C random-trace-id flag is not set on deterministic ids. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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(). That caller (hashSpan) lands on
|
||||
* a later branch, so this generator is installed but dormant here.
|
||||
*
|
||||
* Example 1 - primary use, inside a forced-root hash span (arrives on a later
|
||||
* branch; 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; the
|
||||
* caller lands on a later branch):
|
||||
* @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
|
||||
92
src/libxrpl/telemetry/DeterministicIdGenerator.cpp
Normal file
92
src/libxrpl/telemetry/DeterministicIdGenerator.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Implementation of DeterministicIdGenerator and PendingTraceId.
|
||||
*
|
||||
* The pending trace_id lives in file-local thread_locals shared between the
|
||||
* generator and the RAII guard. GenerateTraceId() consumes the pending id on
|
||||
* the SDK's no-parent branch; PendingTraceId sets it and asserts consumption.
|
||||
* All OpenTelemetry SDK types stay confined to telemetry translation units.
|
||||
*
|
||||
* @see DeterministicIdGenerator, PendingTraceId (DeterministicIdGenerator.h)
|
||||
*/
|
||||
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/telemetry/DeterministicIdGenerator.h>
|
||||
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
|
||||
#include <opentelemetry/nostd/span.h>
|
||||
#include <opentelemetry/sdk/trace/random_id_generator_factory.h>
|
||||
#include <opentelemetry/trace/span_id.h>
|
||||
#include <opentelemetry/trace/trace_id.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::telemetry {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Trace_id pinned by PendingTraceId, consumed by GenerateTraceId(). File-local
|
||||
* and thread-local, so it is set and read on the same thread with no locking.
|
||||
*/
|
||||
thread_local std::optional<std::array<std::uint8_t, 16>> tlsPendingTraceId;
|
||||
|
||||
/**
|
||||
* True once GenerateTraceId() has consumed the pending id. ~PendingTraceId
|
||||
* asserts on it to catch a forced-root span that never reached the SDK root
|
||||
* branch.
|
||||
*/
|
||||
thread_local bool tlsPendingConsumed = false;
|
||||
|
||||
} // namespace
|
||||
|
||||
DeterministicIdGenerator::DeterministicIdGenerator()
|
||||
: opentelemetry::sdk::trace::IdGenerator(/*is_random=*/false)
|
||||
, random_(opentelemetry::sdk::trace::RandomIdGeneratorFactory::Create())
|
||||
{
|
||||
}
|
||||
|
||||
opentelemetry::trace::TraceId
|
||||
DeterministicIdGenerator::GenerateTraceId() noexcept
|
||||
{
|
||||
if (tlsPendingTraceId)
|
||||
{
|
||||
auto const id = *tlsPendingTraceId;
|
||||
tlsPendingTraceId.reset();
|
||||
tlsPendingConsumed = true;
|
||||
return opentelemetry::trace::TraceId(
|
||||
opentelemetry::nostd::span<std::uint8_t const, 16>(id.data(), 16));
|
||||
}
|
||||
return random_->GenerateTraceId();
|
||||
}
|
||||
|
||||
opentelemetry::trace::SpanId
|
||||
DeterministicIdGenerator::GenerateSpanId() noexcept
|
||||
{
|
||||
return random_->GenerateSpanId(); // ALWAYS random — never uses the pending id
|
||||
}
|
||||
|
||||
PendingTraceId::PendingTraceId(std::array<std::uint8_t, 16> const& id) noexcept
|
||||
{
|
||||
tlsPendingTraceId = id;
|
||||
tlsPendingConsumed = false;
|
||||
}
|
||||
|
||||
PendingTraceId::~PendingTraceId() noexcept
|
||||
{
|
||||
// The forced no-parent (root) span-start MUST have consumed the pending id
|
||||
// via GenerateTraceId(). If not, the SDK took a different branch than the
|
||||
// caller forced — a bug — so fail loudly in debug/test builds.
|
||||
XRPL_ASSERT(
|
||||
tlsPendingConsumed,
|
||||
"xrpl::telemetry::PendingTraceId : deterministic trace_id was not consumed");
|
||||
tlsPendingTraceId.reset(); // never leak, even in release
|
||||
tlsPendingConsumed = false;
|
||||
}
|
||||
|
||||
} // namespace xrpl::telemetry
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/telemetry/DeterministicIdGenerator.h>
|
||||
#include <xrpl/telemetry/DiscardFlag.h>
|
||||
|
||||
#include <opentelemetry/context/context.h>
|
||||
@@ -321,9 +322,15 @@ public:
|
||||
std::make_shared<trace_sdk::TraceIdRatioBasedSampler>(setup_.samplingRatio);
|
||||
auto sampler = trace_sdk::ParentBasedSamplerFactory::Create(std::move(rootSampler));
|
||||
|
||||
// Create TracerProvider
|
||||
// Create TracerProvider with a DeterministicIdGenerator. It returns a
|
||||
// deterministic trace_id when a PendingTraceId is active on the thread,
|
||||
// else a random one — letting hash-derived roots (introduced on a later
|
||||
// branch) become true trace roots. Dormant until such a caller exists.
|
||||
sdkProvider_ = trace_sdk::TracerProviderFactory::Create(
|
||||
std::move(processor), resourceAttrs, std::move(sampler));
|
||||
std::move(processor),
|
||||
resourceAttrs,
|
||||
std::move(sampler),
|
||||
std::make_unique<DeterministicIdGenerator>());
|
||||
|
||||
// Set as global provider
|
||||
trace_api::Provider::SetTracerProvider(
|
||||
|
||||
Reference in New Issue
Block a user