mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-26 23:19:07 +00:00
feat(telemetry): let a call site choose a span's role, not just its category
Span kind was derived from TraceCategory alone, so every Rpc-category span was kServer. A category cannot tell an inbound handler from the internal work under it, and trace backends pair kServer with kClient, so internal spans left as kServer become unpaired edges in a service graph and read as extra inbound requests. SpanRole is a new xrpl-owned enum, orthogonal to TraceCategory: the category names the subsystem and gates the span on config, the role says whether the span handles a remote call. It is a defaulted fourth parameter on span(), freshRoot() and the ScopedSpanGuard equivalents, defaulting to SpanRole::FromCategory, so no existing call site changes. resolveSpanKind() applies an explicit role and falls back to the category map, which keeps its single responsibility. The telemetry-disabled stubs mirror all four signatures. No call site passes a role yet. The two that need it are on a later branch. Also fixes a ScopedSpanGuard example that passed a bare op:: suffix to childSpan(), which takes the name verbatim. Naming the child rpc.command made it a child of rpc.command.<cmd>, inverting the hierarchy, so the example's parent is now rpc.process and the command attribute moved onto the command span.
This commit is contained in:
@@ -155,6 +155,22 @@
|
||||
* });
|
||||
* @endcode
|
||||
*
|
||||
* 8. Internal work inside a category whose default role is Server:
|
||||
* @code
|
||||
* #include <xrpld/rpc/detail/RpcSpanNames.h>
|
||||
* using namespace xrpl::telemetry;
|
||||
*
|
||||
* // Only the inbound handler is the server side of a remote call.
|
||||
* // Work below it is internal, so pass the role explicitly: the
|
||||
* // category default (Server) would read as a second inbound
|
||||
* // request and leave an unpaired edge in a service graph.
|
||||
* auto span = SpanGuard::span(
|
||||
* TraceCategory::Rpc,
|
||||
* rpc_span::prefix::rpc,
|
||||
* rpc_span::op::process,
|
||||
* SpanRole::Internal);
|
||||
* @endcode
|
||||
*
|
||||
* @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
|
||||
@@ -196,6 +212,25 @@ namespace xrpl::telemetry {
|
||||
*/
|
||||
enum class TraceCategory { Rpc, Transactions, Consensus, Peer, Ledger };
|
||||
|
||||
/**
|
||||
* Role a span plays in a call relationship. Each value maps to the OTel
|
||||
* span kind of the same name; see Telemetry::startSpan() for what those
|
||||
* mean.
|
||||
*
|
||||
* Orthogonal to TraceCategory. The category names the subsystem and gates
|
||||
* the span on config (`trace_rpc=1`); the role says whether the span
|
||||
* handles a remote call or is internal work. An Rpc-category span can be
|
||||
* either: the inbound request handler is Server, everything it calls into
|
||||
* is Internal.
|
||||
*
|
||||
* FromCategory takes the category's own role, so a call site that does not
|
||||
* care passes nothing. Pick a role explicitly where the category default
|
||||
* is wrong: trace backends pair Server with Client and Consumer with
|
||||
* Producer, so internal work left as Server becomes an unpaired edge in a
|
||||
* service graph.
|
||||
*/
|
||||
enum class SpanRole { FromCategory, Internal, Server, Client, Producer, Consumer };
|
||||
|
||||
/**
|
||||
* Opaque wrapper for an OTel context snapshot.
|
||||
*
|
||||
@@ -291,9 +326,16 @@ public:
|
||||
* @param cat Trace subsystem category.
|
||||
* @param prefix Span name prefix (e.g. "rpc.command").
|
||||
* @param name Span name suffix (e.g. "submit").
|
||||
* @param role Call-relationship role; defaults to the category's own
|
||||
* role. Pass Internal for work the category maps to Server or Consumer
|
||||
* but that handles no remote call.
|
||||
*/
|
||||
[[nodiscard]] static SpanGuard
|
||||
span(TraceCategory cat, std::string_view prefix, std::string_view name) noexcept;
|
||||
span(
|
||||
TraceCategory cat,
|
||||
std::string_view prefix,
|
||||
std::string_view name,
|
||||
SpanRole role = SpanRole::FromCategory) noexcept;
|
||||
|
||||
/**
|
||||
* Create a span that always starts a fresh trace root.
|
||||
@@ -308,10 +350,16 @@ public:
|
||||
* @param cat Trace subsystem category.
|
||||
* @param prefix Span name prefix (e.g. "peer").
|
||||
* @param name Span name suffix (e.g. "validation.receive").
|
||||
* @param role Call-relationship role; defaults to the category's own
|
||||
* role. See span().
|
||||
* @return An active root-span guard, or a null guard if disabled.
|
||||
*/
|
||||
[[nodiscard]] static SpanGuard
|
||||
freshRoot(TraceCategory cat, std::string_view prefix, std::string_view name) noexcept;
|
||||
freshRoot(
|
||||
TraceCategory cat,
|
||||
std::string_view prefix,
|
||||
std::string_view name,
|
||||
SpanRole role = SpanRole::FromCategory) noexcept;
|
||||
|
||||
// --- Child / linked span creation ----------------------------------
|
||||
|
||||
@@ -528,10 +576,12 @@ public:
|
||||
* 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::process);
|
||||
* TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::process);
|
||||
* // childSpan takes the name verbatim, so pass a full dotted constant,
|
||||
* // never a bare op:: suffix. The child parents to `span` because
|
||||
* // `span` is active on this thread.
|
||||
* auto child = span.childSpan(rpc_span::prefix::command);
|
||||
* child.setAttribute(rpc_span::attr::command, commandName);
|
||||
* @endcode
|
||||
*
|
||||
* 2. Capture on this thread, hand off to another (edge case):
|
||||
@@ -578,8 +628,14 @@ public:
|
||||
* @param cat Trace subsystem category.
|
||||
* @param prefix Span name prefix (e.g. "rpc.command").
|
||||
* @param name Span name suffix (e.g. "submit").
|
||||
* @param role Call-relationship role; defaults to the category's own
|
||||
* role. See SpanGuard::span().
|
||||
*/
|
||||
ScopedSpanGuard(TraceCategory cat, std::string_view prefix, std::string_view name) noexcept;
|
||||
ScopedSpanGuard(
|
||||
TraceCategory cat,
|
||||
std::string_view prefix,
|
||||
std::string_view name,
|
||||
SpanRole role = SpanRole::FromCategory) noexcept;
|
||||
|
||||
~ScopedSpanGuard();
|
||||
|
||||
@@ -598,10 +654,16 @@ public:
|
||||
* @param cat Trace subsystem category.
|
||||
* @param prefix Span name prefix.
|
||||
* @param name Span name suffix.
|
||||
* @param role Call-relationship role; defaults to the category's own
|
||||
* role. See SpanGuard::span().
|
||||
* @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) noexcept;
|
||||
freshRoot(
|
||||
TraceCategory cat,
|
||||
std::string_view prefix,
|
||||
std::string_view name,
|
||||
SpanRole role = SpanRole::FromCategory) noexcept;
|
||||
|
||||
// --- Child / linked span creation ----------------------------------
|
||||
|
||||
@@ -893,13 +955,21 @@ public:
|
||||
operator=(SpanGuard const&) = delete;
|
||||
|
||||
[[nodiscard]] static SpanGuard
|
||||
span(TraceCategory, std::string_view, std::string_view) noexcept
|
||||
span(
|
||||
TraceCategory,
|
||||
std::string_view,
|
||||
std::string_view,
|
||||
SpanRole = SpanRole::FromCategory) noexcept
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] static SpanGuard
|
||||
freshRoot(TraceCategory, std::string_view, std::string_view) noexcept
|
||||
freshRoot(
|
||||
TraceCategory,
|
||||
std::string_view,
|
||||
std::string_view,
|
||||
SpanRole = SpanRole::FromCategory) noexcept
|
||||
{
|
||||
return {};
|
||||
}
|
||||
@@ -1004,7 +1074,11 @@ class ScopedSpanGuard
|
||||
ScopedSpanGuard() = default;
|
||||
|
||||
public:
|
||||
ScopedSpanGuard(TraceCategory, std::string_view, std::string_view) noexcept
|
||||
ScopedSpanGuard(
|
||||
TraceCategory,
|
||||
std::string_view,
|
||||
std::string_view,
|
||||
SpanRole = SpanRole::FromCategory) noexcept
|
||||
{
|
||||
}
|
||||
/**
|
||||
@@ -1025,7 +1099,11 @@ public:
|
||||
operator=(ScopedSpanGuard const&) = delete;
|
||||
|
||||
[[nodiscard]] static ScopedSpanGuard
|
||||
freshRoot(TraceCategory, std::string_view, std::string_view) noexcept
|
||||
freshRoot(
|
||||
TraceCategory,
|
||||
std::string_view,
|
||||
std::string_view,
|
||||
SpanRole = SpanRole::FromCategory) noexcept
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -168,11 +168,13 @@ namespace {
|
||||
constexpr char const* kLinkTypeKey = "link_type";
|
||||
constexpr char const* kLinkTypeFollowsFrom = "follows_from";
|
||||
|
||||
// Map a TraceCategory to an OTel SpanKind so Tempo's service-graph /
|
||||
// RED metrics see the correct direction. RPC spans are emitted at the
|
||||
// server entry point (handler dispatch), Peer spans at inbound-message
|
||||
// receipt. Transactions / Consensus / Ledger are internal processing
|
||||
// and keep the default kInternal.
|
||||
// Per-category default OTel SpanKind, used when a call site passes no
|
||||
// SpanRole. A category cannot tell an inbound entry point from the
|
||||
// internal work under it, so RPC and Peer default to the entry-point
|
||||
// kind and any call site below the entry point passes SpanRole::Internal
|
||||
// instead. Transactions / Consensus / Ledger are internal throughout.
|
||||
// The kind drives direction in Tempo's service-graph / RED metrics,
|
||||
// which pair kServer with kClient and kConsumer with kProducer.
|
||||
otel_trace::SpanKind
|
||||
categoryToSpanKind(TraceCategory cat)
|
||||
{
|
||||
@@ -190,6 +192,38 @@ categoryToSpanKind(TraceCategory cat)
|
||||
return otel_trace::SpanKind::kInternal; // unreachable
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the span kind to start a span with.
|
||||
*
|
||||
* An explicit SpanRole wins; SpanRole::FromCategory falls back to the
|
||||
* category default above. Role and category are separate axes, so a single
|
||||
* category can emit both an inbound handler and the internal work under it.
|
||||
*
|
||||
* @param cat Trace subsystem category. Read only for SpanRole::FromCategory.
|
||||
* @param role Role the caller asked for.
|
||||
* @return The OTel span kind for this span.
|
||||
*/
|
||||
[[nodiscard]] otel_trace::SpanKind
|
||||
resolveSpanKind(TraceCategory cat, SpanRole role)
|
||||
{
|
||||
switch (role)
|
||||
{
|
||||
case SpanRole::FromCategory:
|
||||
return categoryToSpanKind(cat);
|
||||
case SpanRole::Internal:
|
||||
return otel_trace::SpanKind::kInternal;
|
||||
case SpanRole::Server:
|
||||
return otel_trace::SpanKind::kServer;
|
||||
case SpanRole::Client:
|
||||
return otel_trace::SpanKind::kClient;
|
||||
case SpanRole::Producer:
|
||||
return otel_trace::SpanKind::kProducer;
|
||||
case SpanRole::Consumer:
|
||||
return otel_trace::SpanKind::kConsumer;
|
||||
}
|
||||
return categoryToSpanKind(cat); // unreachable
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a span-name prefix and suffix into the dotted full name.
|
||||
*
|
||||
@@ -221,7 +255,11 @@ joinSpanName(std::string_view prefix, std::string_view name) noexcept
|
||||
} // namespace
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::span(TraceCategory cat, std::string_view prefix, std::string_view name) noexcept
|
||||
SpanGuard::span(
|
||||
TraceCategory cat,
|
||||
std::string_view prefix,
|
||||
std::string_view name,
|
||||
SpanRole role) noexcept
|
||||
{
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled() || !isCategoryEnabled(*tel, cat))
|
||||
@@ -229,11 +267,15 @@ SpanGuard::span(TraceCategory cat, std::string_view prefix, std::string_view nam
|
||||
auto const fullName = joinSpanName(prefix, name);
|
||||
if (!fullName)
|
||||
return {};
|
||||
return SpanGuard(std::make_unique<Impl>(tel->startSpan(*fullName, categoryToSpanKind(cat))));
|
||||
return SpanGuard(std::make_unique<Impl>(tel->startSpan(*fullName, resolveSpanKind(cat, role))));
|
||||
}
|
||||
|
||||
SpanGuard
|
||||
SpanGuard::freshRoot(TraceCategory cat, std::string_view prefix, std::string_view name) noexcept
|
||||
SpanGuard::freshRoot(
|
||||
TraceCategory cat,
|
||||
std::string_view prefix,
|
||||
std::string_view name,
|
||||
SpanRole role) noexcept
|
||||
{
|
||||
auto* tel = Telemetry::getInstance();
|
||||
if ((tel == nullptr) || !tel->isEnabled() || !isCategoryEnabled(*tel, cat))
|
||||
@@ -244,7 +286,7 @@ SpanGuard::freshRoot(TraceCategory cat, std::string_view prefix, std::string_vie
|
||||
// Force a fresh trace root: do NOT inherit this thread's active span.
|
||||
auto rootCtx = opentelemetry::context::Context{otel_trace::kIsRootSpanKey, true};
|
||||
return SpanGuard(
|
||||
std::make_unique<Impl>(tel->startSpan(*fullName, rootCtx, categoryToSpanKind(cat))));
|
||||
std::make_unique<Impl>(tel->startSpan(*fullName, rootCtx, resolveSpanKind(cat, role))));
|
||||
}
|
||||
|
||||
// ===== Child / linked span creation ========================================
|
||||
@@ -532,8 +574,9 @@ ScopedSpanGuard::~ScopedSpanGuard()
|
||||
ScopedSpanGuard::ScopedSpanGuard(
|
||||
TraceCategory cat,
|
||||
std::string_view prefix,
|
||||
std::string_view name) noexcept
|
||||
: ScopedSpanGuard(SpanGuard::span(cat, prefix, name))
|
||||
std::string_view name,
|
||||
SpanRole role) noexcept
|
||||
: ScopedSpanGuard(SpanGuard::span(cat, prefix, name, role))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -541,9 +584,10 @@ ScopedSpanGuard
|
||||
ScopedSpanGuard::freshRoot(
|
||||
TraceCategory cat,
|
||||
std::string_view prefix,
|
||||
std::string_view name) noexcept
|
||||
std::string_view name,
|
||||
SpanRole role) noexcept
|
||||
{
|
||||
return ScopedSpanGuard(SpanGuard::freshRoot(cat, prefix, name));
|
||||
return ScopedSpanGuard(SpanGuard::freshRoot(cat, prefix, name, role));
|
||||
}
|
||||
|
||||
ScopedSpanGuard
|
||||
|
||||
Reference in New Issue
Block a user