User Tracing#223
Conversation
| # TEMPORARY | ||
| [patch.crates-io] | ||
| cf-rustracing = { git = "https://github.com/mar-cf/rustracing.git", branch = "user-tracing" } |
Replace the blanket `impl From<Span> for SharedSpan` — which always registered the span in the harness's `active_roots` — with an explicit `shared_span()` constructor, and migrate the internal call sites. This makes tracked construction a deliberate choice and sets up a later untracked variant for user spans.
Adds the user-span pipeline core: an `Untracked` `user_shared_span` constructor, the `start_user_trace`/`user_span` entry points and `UserSpanScope` guard on a separate `USER_HARNESS` (with a `USER_NOOP_HARNESS` fallback), plus the `add_user_span_tags!`/`add_user_span_log_fields!`/`set_user_span_finish_callback!` macros. Includes the test harness (`user_traces()` second sink) so user spans are observed independently of the internal pipeline. `start_user_trace` is name-only here; routing and inbound W3C continuation are layered on in later changes.
Adds a `user_span` slot to `TelemetryContext` (captured by `current()`, re-established by `scope()`, cloned across forks) plus `UserSpanScope::into_context()` and a parallel carry on `SpanScope::into_context()`. This lets a user span survive `.await`/`tokio::spawn` and ride along even when propagation goes through an internal span's context — no explicit threading. Verified by `propagates_across_await` and `user_span_carried_by_internal_context`.
Adds `SpanScope::with_user_span()` to open a parallel user span off an internal span (named after it), and a `user = true` option on `#[span_fn]` that does the same for whole functions (sync and async). Both are no-ops when no user trace is active. Covered by macro snapshot tests plus parallel and no-op runtime tests.
Points `cf-rustracing` at the fork branch that adds `RoutingMetadata` as a span property, needed by the user-tracing exporter and `start_user_trace` routing. Placeholder to be replaced by a normal version bump once the rustracing change is released.
|
@inikulin PTAL |
281f273 to
6a141e5
Compare
Adds the user-tracing pipeline that exports user spans, with routing attached to each span to group and tag it on export.
Adds the `TraceparentContext` W3C parser and wires it through: `start_user_trace` gains an optional `inbound` traceparent that stitches the user root onto the upstream trace (shared trace id, inbound parent), and `user_tracing::w3c_traceparent()` derives the header for the current user span for outbound propagation. Covered by parser unit tests plus continuation tests through the test harness and the OTLP/UDS producer path.
| }}; | ||
| } | ||
|
|
||
| /// Adds tags to the current user span. No-op when no user trace is active. |
There was a problem hiding this comment.
please add references to the non-user macros that have more exhaustive docs. (you don't have to duplicate them.)
| #[cfg(feature = "user-tracing")] | ||
| user_span: Option<SharedSpan>, | ||
| #[cfg(feature = "user-tracing")] | ||
| _user_inner: Option<Scope<SharedSpan>>, |
There was a problem hiding this comment.
can this be an Option<UserSpanScope>?
Also: As far as I understand it, this is only used to enable a fluent API like span(...).with_user_tracing().into_context().apply(...). can we use a separate type for that instead so we dont pay the storage overhead on every SpanScope?
| #[cfg(feature = "tracing")] | ||
| impl TelemetryContext { | ||
| /// Creates a new telemetry context, that includes a forked trace, creating a | ||
| /// linked child trace. |
There was a problem hiding this comment.
please mention here that this only forks the internal (non-user) trace
| let name = self | ||
| .span | ||
| .inner | ||
| .with_read(|s| s.operation_name().to_string()); |
There was a problem hiding this comment.
I think given how user-tracing is structured, there could be a user trace even if the regular trace was dropped due to sampling. In that case .operation_name() returns an empty string. How should we handle this?
|
|
||
| impl UserTracingSettings { | ||
| fn default_enabled() -> bool { | ||
| true |
There was a problem hiding this comment.
please dont enable tracing by default. users have to configure a path for the UDS anyway, so they should also explicitly set enable: true.
we plan to turn off regular tracing by default as well: #158 (comment)
|
|
||
| impl OtlpUdsOutputSettings { | ||
| const fn default_num_tasks() -> usize { | ||
| 2 |
There was a problem hiding this comment.
the default for our other exporters is 1, can you add a comment explaining why you chose a higher number for user tracing?
| let sampler = AllSampler.boxed(); | ||
|
|
||
| if let Some(cap) = settings.max_queue_size { | ||
| let (consumer, span_rx) = super::channel::channel(cap); |
There was a problem hiding this comment.
This channel uses a hardcoded gauge to record its current size. This needs to be changed to allow a gauge to be passed in. It's straightforward, and I don't this PR to become even larger, so I'll put up a quick PR tomorow.
| /// Processes a single drained batch of spans: groups them by routing, | ||
| /// converts each to OTLP, and POSTs one request per group. Errors are | ||
| /// reported and do not abort the batch. | ||
| async fn process_batch(&self, service_info: &ServiceInfo, spans: Vec<FinishedSpan>) { |
There was a problem hiding this comment.
nit: please use spans: &mut Vec<...> here and spans.drain(..) in the loop to avoid reallocating the vec in do_export every time
|
|
||
| /// Introspection and outbound-propagation helpers for the user-tracing pipeline. | ||
| #[cfg(feature = "user-tracing")] | ||
| pub mod user_tracing { |
There was a problem hiding this comment.
Would you be ok with moving the rest of the public freestanding functions in here as well? i mean:
start_user_trace->user_tracing::start_traceuser_span->user_tracing::span- and maybe the macros as well
| /// Constructed via [`TraceparentContext::parse`]. Round-trippable via | ||
| /// [`TraceparentContext::to_traceparent_string`]. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| pub struct TraceparentContext { |
There was a problem hiding this comment.
can you explain why you decided not to use https://docs.rs/cf-rustracing-jaeger/latest/cf_rustracing_jaeger/span/struct.SpanContextState.html?
it has a FromStr implementation to parse the context from a string, and we have the code in w3c_traceparent() to convert back into a string. what is missing, and could we more easily add it in cf-rustracing-jaeger?
Overview
This stack adds a user-facing span pipeline to
foundations, parallel to the existing internal tracing pipeline. Application code can emit spans into a separateUSER_HARNESSthat exports OTLP HTTP over a Unix domain socket (gRPC unsupported) to an OTLP endpoint, with per-trace routing metadata (an application-definedRoutingMetadatatrait) carried on the wire and W3Ctraceparentcontinuation in/out.The design is deliberately a mirror of internal tracing onto a second harness: every user API (
user_span,start_user_trace,add_user_span_tags!,TelemetryContext.user_span, …) is theget_user()twin of an existingget()API. Two hard rules shape the surface:There are two layers to keep separate:
USER_HARNESS+ the exporter, pointed at the OTLP endpoint's socket.start_user_trace(...)opens a root. Without a root, everyuser_span/with_user_span()/#[span_fn(user = true)]/add_user_span_tags!is a no-op.User guide
Settings (init-time)
User tracing is configured via one optional block, gated by the
user-tracingfeature and fed to the existingfoundations::telemetry::init:The two settings a consumer must choose are
socket_path(the OTLP endpoint's UDS) androuting_header_name(the header the endpoint reads routing from).enabledandmax_queue_sizemirror their internal-tracing equivalents;num_tasksandmax_batch_sizetune the exporter. All others have defaults and can be configured as needed.There is deliberately no sampling configuration here. Unlike internal tracing, the user pipeline is not sampled inside foundations — the inbound
user_tracingcontrol header drives the activation (and therefore sampling) decision upstream.Instrumentation APIs
Toy example covering the whole surface:
Key points for users:
into_context()/TelemetryContext::current()/#[span_fn]all propagate it across.await, even through an internal span's context — no manual threading.with_user_span()and#[span_fn(user = true)]open a user span in parallel with an internal span — this matches the guideline to create an internal span for every user span (the#[span_fn]decorator does the same), so a single call feeds both pipelines.Notes to reviewers
The stack is four stacked PRs / seven commits. PR2–4 introduce the core user-span APIs; the surrounding PRs prepare for and power them — PR1 makes them safe to build, PR5–6 export them off-box, PR7 propagates them over W3C.
PR1 —
user-tracing-1: MakeSharedSpanconstruction explicitInternal-only pre-move: replaces the blanket
impl From<Span>(which always produced a tracked span) with an explicitshared_span()constructor, so user spans can later be built untracked and never enter the internal live registry.PR2–4 —
user-tracing-2-4: the in-process user APIIntroduces the core and the ways to drive it:
user_shared_span(untracked),start_user_trace/user_span, theUserSpanScopeguard, theadd_user_span_tags!/add_user_span_log_fields!/set_user_span_finish_callback!macros,get_user()+ a dedicatedUSER_NOOP_HARNESSfallback, and theuser_traces()test sink that observes user spans independently of the internal pipeline.TelemetryContext.user_span+UserSpanScope::into_context(), so a user span survives.await/spawnand rides along on an internal span's context.SpanScope::with_user_span()and#[span_fn(user = true)].PR5–6 —
user-tracing-5-6: ship spans off-boxcf-rustracing[patch.crates-io]for the construction-timeRoutingMetadataspan property — a trait (group_key/encode) carried asOption<Arc<dyn RoutingMetadata>>(placeholder for a later version bump).UserTracingSettings,init_user/USER_HARNESS, the OTLP/UDS exporter that groups spans bygroup_keyand writes each group'sencode()into the configuredrouting_header_nameheader) wired intotelemetry::init, plusstart_user_trace's requiredroutingarg. Verified by producer tests that decode the exported OTLP body.PR7 —
user-tracing-7: W3C trace propagationThe
TraceparentContextW3C parser, the optionalinboundstitch onstart_user_trace(continues the upstream trace — shared trace id, inbound parent), anduser_tracing::w3c_traceparent()for outbound.Alternatives considered
If constrained not to include user tracing specifics in this codebase, we can open up some seams to plug in similar functionality.
Routing metadata representation.
cf-rustracingcarries routing asOption<Arc<dyn RoutingMetadata>>— a small, Cloudflare-agnostic trait (group_key/encode). The concrete routing type (zone id / account id / account tag / destinations / persist) is defined by the consumer and never seen bycf-rustracing. Two alternatives were rejected:RoutingMetadatastruct and share it as-is acrosscf-rustracingandfoundations. Simplest, but it bakes Cloudflare routing concepts directly intocf-rustracing.Any. Carry routing as a type-erasedArc<dyn Any + Send + Sync>anddowncastin the exporter. This also keeps Cloudflare types out ofcf-rustracing, but trades the typedgroup_key/encodecontract for runtime downcasting.The trait keeps Cloudflare specifics out of
cf-rustracing(like the opaque option) while giving the exporter a typed, downcast-free interface.Pluggable exporter seam (
BatchHandler). Rather than a concrete exporter inside foundations, foundations could accept an object-safeArc<dyn BatchHandler>and run a shared drain loop, with the OTLP/UDS handler implemented in oxy:This would keep the OTLP/UDS (
hyper/prost) deps and wire format out of foundations. We chose a concrete output module (output_otlp_uds, mirroringoutput_jaeger_thrift_udp/output_otlp_grpc): no trait object or plugin point — future destinations are newUserTracesOutputenum variants.Additional notes
SpanContext/SpanContextStateconversion is internal-only —TraceparentContextis the only stitch type users see.cf-rustracing[patch.crates-io]in PR5 is intentionally temporary; the matching change is a separatecf-rustracingPR and will become a normal version bump once released.