Traces
Every request, job and scheduled tick opens a span; W3C traceparent propagation, sampling, and the HTTP interceptor stitch distributed traces together.
Traces come from tracing spans. Because OpenTelemetry::init links
tracing-opentelemetry to
the OTel SDK at boot, every framework span is exported under the trace id the
kernel already decided — the SDK adopts it rather than minting a second one, so
a log line and the exported span name one request by one value. Every other span
automatically gets a trace_id
and exports through OTLP when an endpoint is configured.
Span targets
Section titled “Span targets”Targets are dotted, lowercase, framework-prefixed. One target per concern per crate.
| Layer | Target |
|---|---|
| HTTP request span | nest_rs::http — opened by the transport, not by this crate |
| Operation log, one line per unit of work | nest_rs::operation — the only target naming a kind of line rather than a subsystem; see Logs |
| ORM queries | nest_rs::orm |
| Authentication | nest_rs::authn |
| Authorization | nest_rs::authz |
| WebSocket events | nest_rs::ws |
| GraphQL execution and subscriptions | nest_rs::graphql |
| MCP operations | nest_rs::mcp |
| Queue jobs | nest_rs::queue |
| Scheduled jobs | nest_rs::schedule |
| Module lifecycle | nest_rs::module |
| Application spans | <crate>::<feature> (e.g. features::users) |
Custom spans
Section titled “Custom spans”#[get("/{id}")]#[tracing::instrument(skip(self), fields(user_id = %id.0))]async fn read(&self, id: Path<Uuid>) -> Result<Json<User>> { let user = self.svc.find(id.0).await?; Ok(Json(user))}#[tracing::instrument] opens a span that stays current for the whole async
fn, across every .await. Inside it, use tracing::info! for an event.
Always prefer field = %value to format!("{}", value) — structured fields
serialize cleanly to OTLP and stay filterable in the backend.
W3C propagation
Section titled “W3C propagation”traceparent propagation is automatic on incoming HTTP requests — behind a
trusted proxy. The transport, not this crate, decides whether an inbound
traceparent may be believed: it is continued when the direct peer is in
NESTRS_HTTP__TRUSTED_PROXIES, and the trace is restarted otherwise, which
is what the specification defines a front gate to do. Without that gate any
client could file its request into a trace of its choosing and set the sampled
flag on traffic it generates. The response carries traceresponse so a caller
can match a span in their backend to a span in yours.
The W3C propagator is installed even when no OTLP endpoint is set — useful when a sibling service in your monorepo does export and you want the connection preserved across the hop.
Sampling
Section titled “Sampling”ParentBased(TraceIdRatioBased(ratio)) — children inherit the parent’s
sampling decision, so a request either samples end-to-end or not at all.
NESTRS_OPENTELEMETRY__SAMPLE_RATIO=0.1Or pinned:
OpenTelemetryConfig::new("api").with_trace_sample_ratio(0.1)The sampler comes from
opentelemetry_sdk::trace::Sampler.
Ratios are clamped to [0.0, 1.0].
HTTP interceptor
Section titled “HTTP interceptor”Importing OpenTelemetryModule activates the OpenTelemetryHttp interceptor
automatically — there’s nothing else to mount. It does two things, and both
are things nothing else can supply:
- Links the remote parent when the transport continued an upstream trace, so the exported span is a child of the caller’s rather than a second root. A restarted trace has no parent by definition, and none is invented.
- Writes the sampler’s decision back onto the trace context, so an outbound
traceparent— into a queue envelope, back to the caller — reports what this deployment actually kept.
Everything else belongs to the transport and exists whether or not this crate is
installed: the span itself, its W3C trace context, the traceresponse response
header, and the access log. The span carries the OTel HTTP semantic-convention attributes:
http.request.method, url.path (the addressed path), http.route (the
matched template, so a backend groups on something low-cardinality),
http.response.status_code, http.response.body.size, client.address and
user_agent.original.
It is named {method} {route} through otel.name. tracing fixes a span
name to a literal, and one literal for every route makes a trace list
unreadable. A request that matched
nothing is named by its method alone, never by the URL.
Body size and duration are measured at end-of-body, not at handler return, so a half-streamed response and a client disconnect both still produce one access line and one complete span.
The request span and the access event both belong to nest-rs-http, and exist
whether or not this crate is installed. What the interceptor adds is the parent
from an inbound traceparent and the sampler’s verdict — the access
line reads that header, which is the whole of the coupling between the two.
Toggle the line with NESTRS_HTTP__ACCESS_LOG=false; nothing about propagation
or OTLP export changes.
Going further
Section titled “Going further”- Logs — the access event and structured fields on the span.
- Metrics — the aggregate counterpart to spans.
- OpenTelemetry — sampling, propagation, and OTLP export.