Skip to content

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.

Targets are dotted, lowercase, framework-prefixed. One target per concern per crate.

LayerTarget
HTTP request spannest_rs::http — opened by the transport, not by this crate
Operation log, one line per unit of worknest_rs::operation — the only target naming a kind of line rather than a subsystem; see Logs
ORM queriesnest_rs::orm
Authenticationnest_rs::authn
Authorizationnest_rs::authz
WebSocket eventsnest_rs::ws
GraphQL execution and subscriptionsnest_rs::graphql
MCP operationsnest_rs::mcp
Queue jobsnest_rs::queue
Scheduled jobsnest_rs::schedule
Module lifecyclenest_rs::module
Application spans<crate>::<feature> (e.g. features::users)
crates/features/src/users/http/controller.rs
#[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.

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.

ParentBased(TraceIdRatioBased(ratio)) — children inherit the parent’s sampling decision, so a request either samples end-to-end or not at all.

Terminal window
NESTRS_OPENTELEMETRY__SAMPLE_RATIO=0.1

Or pinned:

apps/api/src/module.rs
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].

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:

  1. 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.
  2. 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.

  • Logs — the access event and structured fields on the span.
  • Metrics — the aggregate counterpart to spans.
  • OpenTelemetry — sampling, propagation, and OTLP export.