ridl_rt/trace.rs
1//! The trace context a call or an event can carry.
2//!
3//! [`TraceContext`] is the W3C Trace Context `traceparent` layout without the
4//! version byte, as plain data. `ridl-rt` carries it unvalidated: an all-zero
5//! id is carried like any other value, and rejecting one is the choice of
6//! whatever exports the trace.
7//!
8//! With the `std` feature, [`Propagation`] is the hook through which an
9//! application gives generated code the context to send and receives the
10//! context that arrived.
11
12/// A trace id, a span id and the trace flags, in the layout of the W3C Trace
13/// Context `traceparent` header without its version byte.
14///
15/// The value is not validated. `size_of::<TraceContext>()` is 25 bytes and
16/// `size_of::<Option<TraceContext>>()` is 26 bytes, both with alignment 1.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
18pub struct TraceContext {
19 /// The identifier of the whole trace.
20 pub trace_id: [u8; 16],
21 /// The identifier of the span that sent the call or raised the event.
22 pub span_id: [u8; 8],
23 /// The trace flags byte, carried as given.
24 pub flags: u8,
25}
26
27#[cfg(feature = "std")]
28use std::sync::OnceLock;
29
30/// The hook through which an application connects its telemetry library to the
31/// trace context that crosses a port. Available with the `std` feature.
32///
33/// `ridl-rt` carries the bytes. The code that a generated crate emits is meant
34/// to call the hook at the points below. No generated code calls it yet: the
35/// generated face still passes `None` as the trace context (driftsys/ridl#754
36/// tracks the change). Which span is current, how ids are created and where traces are
37/// exported belong to the application's library. No hook is registered until
38/// the application calls [`set_propagation`].
39///
40/// # Call points
41///
42/// Generated code that calls the hook calls:
43///
44/// - [`current`](Propagation::current) just before it sends a call or a raise.
45/// It returns the context to send, or `None`.
46/// - [`enter`](Propagation::enter) when it receives a claim, before the claim
47/// span exists and before the handler runs.
48/// - [`leave`](Propagation::leave) after the handler has returned, and also
49/// when the handler panics.
50///
51/// # Call order for a claim
52///
53/// Generated code that serves a claim and calls the hook follows this order:
54///
55/// 1. `enter(received)`, where `received` is the context that came over the
56/// wire. It is `None` for a claim that carried no context, and `leave` is
57/// still called to pair with that `enter`.
58/// 2. The claim span is created with no explicit parent, and entered.
59/// 3. The handler runs.
60/// 4. The claim span is exited and dropped.
61/// 5. `leave()`.
62///
63/// An OpenTelemetry implementation of `enter` attaches a context that carries
64/// the received span context as a remote parent, keeps the guard, and drops
65/// the guard in `leave`. It relies on `tracing-opentelemetry` choosing the
66/// parent of a span when the span is created, from the attached context. That
67/// context activation is on by default since `tracing-opentelemetry` 0.32.0.
68/// Setting the parent after the claim span is entered fails, because entering
69/// the span starts it.
70///
71/// The guard that calls `leave` is created only after `enter` returns, so a
72/// panic inside `enter` does not call `leave`. An implementation's `enter` and
73/// `leave` should not panic: a panic in `leave` while another panic unwinds
74/// aborts the process, and a panic in `enter` leaves nothing attached.
75///
76/// # Nesting and threads
77///
78/// Each `enter` is paired with one `leave`. Pairs nest: a handler that serves
79/// another claim produces an inner pair inside the outer one. The `leave` of a
80/// pair runs on the thread that ran its `enter`, so an implementation can keep
81/// what `enter` attached on a per-thread stack.
82#[cfg(feature = "std")]
83pub trait Propagation: Sync {
84 /// Called just before a call or a raise is sent. Returns the context to
85 /// send, or `None`.
86 fn current(&self) -> Option<TraceContext>;
87
88 /// Called when a claim is received, before the claim span is created and
89 /// before the handler runs. The caller of the hook calls `leave` after the
90 /// handler, also when `received` is `None`. `received` is the context that came over the
91 /// wire, or `None` when the claim carried none.
92 fn enter(&self, received: Option<TraceContext>);
93
94 /// Called after the handler has returned, and also when it panics. Undoes
95 /// the matching [`enter`](Propagation::enter).
96 fn leave(&self);
97}
98
99/// [`set_propagation`] was called after a hook was already registered.
100#[cfg(feature = "std")]
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102pub struct AlreadySet;
103
104#[cfg(feature = "std")]
105impl core::fmt::Display for AlreadySet {
106 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
107 f.write_str("a trace propagation hook is already registered")
108 }
109}
110
111#[cfg(feature = "std")]
112impl std::error::Error for AlreadySet {}
113
114#[cfg(feature = "std")]
115static HOOK: OnceLock<&'static dyn Propagation> = OnceLock::new();
116
117/// Registers the hook once for the process, as `log::set_logger` does for the
118/// `log` crate. A second call returns [`AlreadySet`] and keeps the first hook.
119///
120/// One hook serves every generated crate in the process, so a handler in one
121/// generated crate that calls a client of another continues the same trace.
122#[cfg(feature = "std")]
123pub fn set_propagation(p: &'static dyn Propagation) -> Result<(), AlreadySet> {
124 HOOK.set(p).map_err(|_| AlreadySet)
125}
126
127/// The registered hook, or `None` while the application has not called
128/// [`set_propagation`].
129#[cfg(feature = "std")]
130pub fn propagation() -> Option<&'static dyn Propagation> {
131 HOOK.get().copied()
132}