Skip to content

User Tracing#223

Merged
TheJokr merged 1 commit into
cloudflare:mainfrom
mar-cf:user-tracing-7
Jul 16, 2026
Merged

User Tracing#223
TheJokr merged 1 commit into
cloudflare:mainfrom
mar-cf:user-tracing-7

Conversation

@mar-cf

@mar-cf mar-cf commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Overview

This adds a user-facing span pipeline to foundations, parallel to the existing internal tracing pipeline. Application code can emit spans into a separate USER_HARNESS that exports OTLP HTTP over a Unix domain socket (gRPC unsupported) to an OTLP endpoint, with per-trace routing metadata (an application-defined RoutingMetadata trait) carried on the wire and W3C traceparent continuation in/out.

The design is deliberately a mirror of internal tracing onto a second harness: the user API lives in a dedicated tracing::user_tracing module whose entry points (user_tracing::start_trace, user_tracing::span, user_tracing::add_span_tags!, …) mirror the top-level internal-tracing functions (start_trace, span, add_span_tags!, …), plus a top-level dual_span that records into both pipelines at once. Two hard rules shape the surface:

  • The user and internal pipelines are independent — separate harnesses, scope stacks, and exporters
  • Public surface speaks W3C.

There are two layers to keep separate:

  • init-time: stand up USER_HARNESS + the exporter, pointed at the OTLP endpoint's socket.
  • per-request activation: user_tracing::start_trace(...) opens a root. Without a root, every user_tracing::span / dual_span() / #[span_fn(user = true)] / user_tracing::add_span_tags! is a no-op.

User guide

Settings (init-time)

User tracing is configured via one optional block, gated by the user-tracing feature and fed to the existing foundations::telemetry::init:

TelemetrySettings.user_tracing: Option<UserTracingSettings>   // None = off
UserTracingSettings {
    enabled: bool,                       // default: false  (user tracing is opt-in)
    max_queue_size: Option<NonZeroUsize>,// default: 1_000_000; None = unbounded (mirrors internal)
    output: UserTracesOutput,            // only variant today: OtlpUds(..)
}

OtlpUdsOutputSettings {
    socket_path: String,        // ← path to the OTLP endpoint's UDS (required, no default)
                                //   e.g. "/path/to/otlp-receptor.sock"
    routing_header_name: String,// ← header the endpoint reads each span's encoded routing from
                                //   (required, no default) e.g. "cf-trace-config"
    num_tasks: usize,           // default: 1    (concurrent export workers)
    max_batch_size: usize,      // default: 512  (spans drained per export batch)
}

The two settings a consumer must choose are socket_path (the OTLP endpoint's UDS) and routing_header_name (the header the endpoint reads routing from). Unlike internal tracing, user tracing is off by default (enabled: false) and must be turned on explicitly; max_queue_size mirrors its internal-tracing equivalent; num_tasks and max_batch_size tune 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_tracing control header drives the activation (and therefore sampling) decision upstream.

Instrumentation APIs

Toy example covering the whole surface:

use foundations::telemetry::tracing::{self, RoutingMetadata, TraceparentContext};

// `RoutingMetadata` is a trait — you define the concrete routing type. `group_key` batches
// spans that share it into one request; `encode` produces the routing header's value.
#[derive(Debug)]
struct MyRouting { zone_id: u64, account_id: u64 }

impl RoutingMetadata for MyRouting {
    fn group_key(&self) -> String { format!("{}|{}", self.zone_id, self.account_id) }
    fn encode(&self) -> String { format!("zone={};account={}", self.zone_id, self.account_id) }
}

// Per request: open the root. `routing` is required and fixed at construction (inherited by all
// descendants); `inbound` continues an upstream W3C trace, or None for a fresh one.
let _root = tracing::user_tracing::start_trace(
    "example_span_name",
    MyRouting { zone_id, account_id },
    inbound,                       // Option<TraceparentContext>
);

// Children — pick whichever fits:
let _child = tracing::user_tracing::span("lookup"); // explicit standalone user child
let _s     = tracing::dual_span("db");              // parallel internal + user child

#[tracing::span_fn("handle_request", user = true)]  // whole-fn dual span (sync or async)
async fn handle_request() { /* ... */ }

// Annotate the current user span (no-op if no user trace is active):
tracing::user_tracing::add_span_tags!("cache.status" => "HIT");

// Across .await / spawn / separate hooks (UserSpanScope is !Send): hold the context.
let ctx = tracing::user_tracing::start_trace("req", routing, None).into_context(); // -> TelemetryContext
ctx.apply(async { let _c = tracing::user_tracing::span("work"); }).await;

// Outbound propagation to the next hop:
let traceparent: Option<String> = tracing::user_tracing::w3c_traceparent();

// Inbound parsing (strict W3C):
let inbound = TraceparentContext::parse(header_bytes); // -> Option<TraceparentContext>

Key points for users:

  • The context carries the user span: into_context() / TelemetryContext::current() / #[span_fn] all propagate it across .await, even through an internal span's context — no manual threading.
  • dual_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, so a single call feeds both pipelines. dual_span names both spans from the same argument, so the user span keeps its name even when the internal trace is sampled out (the two pipelines sample independently).
  • Forking a trace leaves the user trace intactTelemetryContext's trace fork only forks the internal trace; user spans created in the forked context stay in the current user trace.

@mar-cf mar-cf changed the title User tracing 7 User Tracing Jun 24, 2026
Comment thread Cargo.toml Outdated
@mar-cf mar-cf force-pushed the user-tracing-7 branch 2 times, most recently from 530dd13 to 0b7d4e8 Compare June 24, 2026 16:27
@mar-cf mar-cf marked this pull request as ready for review June 24, 2026 16:27
@mar-cf

mar-cf commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

@inikulin PTAL

@mar-cf mar-cf force-pushed the user-tracing-7 branch 3 times, most recently from 281f273 to 6a141e5 Compare July 1, 2026 11:20
@TheJokr TheJokr self-requested a review July 2, 2026 16:26
Comment thread foundations/src/telemetry/tracing/mod.rs
Comment thread foundations/src/telemetry/tracing/mod.rs Outdated
Comment thread foundations/src/telemetry/telemetry_context.rs
Comment thread foundations/src/telemetry/tracing/mod.rs Outdated
Comment thread foundations/src/telemetry/settings/user_tracing.rs Outdated
Comment thread foundations/src/telemetry/settings/user_tracing.rs Outdated
Comment thread foundations/src/telemetry/tracing/init.rs Outdated
Comment thread foundations/src/telemetry/tracing/output_otlp_uds.rs Outdated
Comment thread foundations/src/telemetry/tracing/mod.rs Outdated
Comment thread foundations/src/telemetry/tracing/traceparent.rs
@mar-cf

mar-cf commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Will update the PR description shortly

TheJokr added a commit that referenced this pull request Jul 13, 2026
In #223 we are adding a second tracing pipeline. To avoid clashing
metrics between the two, we need to add a label to distinguish them.

@TheJokr TheJokr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM now. Please rebase/squash or resolve conflicts with main, bump the cf-rustracing version, and add PipelineType::User for the metrics, then we can merge this.

@mar-cf mar-cf force-pushed the user-tracing-7 branch 2 times, most recently from c27a28d to d517d59 Compare July 16, 2026 03:03
Adds a user-facing span pipeline to `foundations`, parallel to and
independent of the existing internal tracing pipeline. Application code
emits spans into a separate harness that exports OTLP over HTTP on a Unix
domain socket, with per-trace routing metadata (an application-defined
`RoutingMetadata`) attached to each span and carried to the endpoint in a
configurable header, plus W3C `traceparent` continuation in and out.

The public API lives in a dedicated `tracing::user_tracing` module that
mirrors the top-level internal-tracing functions (`start_trace`, `span`,
`add_span_tags!`, ...), plus a top-level `dual_span` that records into both
pipelines at once; `#[span_fn(user = true)]` emits a dual span. The user
span rides on `TelemetryContext`, so it propagates across `.await`, spawns,
and hooks without manual threading.

Two rules shape the surface: the user and internal pipelines are fully
independent (separate harnesses, scope stacks, and exporters), and the
public surface speaks W3C. Sampling is not configured inside foundations --
per-request activation is driven by the caller. Everything is gated behind
the `user-tracing` cargo feature.
@mar-cf

mar-cf commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@TheJokr All done, ready to go

@TheJokr TheJokr merged commit 68988e2 into cloudflare:main Jul 16, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants