OpenTelemetry
First-class OpenTelemetry for nestrs — traces, logs and metrics, W3C propagation, OTLP export — wired by importing one module.
nest-rs-opentelemetry is the OpenTelemetry layer of the framework. It installs
a tracing subscriber, batches traces / logs /
metrics through the official
opentelemetry +
opentelemetry_sdk +
opentelemetry-otlp crates,
follows the
opentelemetry-semantic-conventions
for resource and HTTP attributes, and links the tracing span tree to its OTel
counterpart through
tracing-opentelemetry.
Three signal types ship in one crate, behind one import:
- Traces — every request, job and scheduled tick
opens a span; W3C
traceparentpropagation, sampling, and the per-request HTTP interceptor stitch distributed traces together. - Logs —
tracing::info!and friends flow to the console and to OTel logs (viaopentelemetry-appender-tracing) with structured fields, configurable level per layer, and JSON for prod. - Metrics — inject
Arc<OpenTelemetryMeter>and build counters / histograms / gauges; metrics batch on aPeriodicReaderand export through the same OTLP endpoint as the traces.
Install
Section titled “Install”cargo add nest-rs --features opentelemetryA headless binary (a queue worker) stops there; an app serving HTTP adds the
http feature for the per-request interceptor — see Feature
flags below.
Wire it in
Section titled “Wire it in”use anyhow::Result;use nest_rs::config::Environment;use nest_rs::core::App;use nest_rs::opentelemetry::OpenTelemetry;
use auth::AuthModule;
#[tokio::main]async fn main() -> Result<()> { let _environment = Environment::init(); let _otel = OpenTelemetry::init("auth")?;
App::builder() .module::<AuthModule>() .build() .await? .run() .await}#[module( imports = [ ConfigModule::for_root(), OpenTelemetryModule, HttpModule::for_root(HttpConfig { port: 3001, ..Default::default() }), ],)]pub struct AuthModule;Two pieces, one rule. OpenTelemetry::init(name)? installs the global
subscriber and must run before App::builder(). The returned guard flushes
batch exporters on Drop, so it must live until main returns.
OpenTelemetryModule then mounts the per-request HTTP interceptor and exposes
the meter as a provider.
Configure the exporter
Section titled “Configure the exporter”Every option of OpenTelemetryConfig is settable via env or via the pinned
struct — the framework-wide config dual path.
OpenTelemetry::init(name) reads env; OpenTelemetry::init_with(cfg) takes a
pinned OpenTelemetryConfig.
| Env var | Field | Default |
|---|---|---|
NESTRS_OPENTELEMETRY__SERVICE_NAME | service_name | argument to init |
NESTRS_OPENTELEMETRY__SERVICE_VERSION | service_version | None |
NESTRS_OPENTELEMETRY__SERVICE_ENVIRONMENT | deployment_environment | None |
NESTRS_OPENTELEMETRY__SERVICE_INSTANCE_ID | service_instance_id | fresh UUIDv7 per process |
NESTRS_LOG | log_filter | info (the framework-wide logging family) |
NESTRS_LOG_FORMAT | log_format | text in debug, json in release |
NESTRS_LOG_SOURCE_LOCATION | log_source_location | false |
NESTRS_OPENTELEMETRY__OTLP_ENDPOINT | otlp_endpoint | None (console-only) |
NESTRS_OPENTELEMETRY__SAMPLE_RATIO | trace_sample_ratio | 1.0 |
The OTLP exporter wires up only when otlp_endpoint is set. Without it,
the subscriber stays console-only; the W3C propagator is still installed so
an inbound traceparent is continued behind a trusted proxy and the response
carries traceresponse.
NESTRS_OPENTELEMETRY__OTLP_ENDPOINT=http://otel-collector:4318 \NESTRS_OPENTELEMETRY__SERVICE_VERSION=2026.06.0 \NESTRS_OPENTELEMETRY__SERVICE_ENVIRONMENT=production \NESTRS_OPENTELEMETRY__SAMPLE_RATIO=0.1 \NESTRS_LOG=info \ nestrs run start apiThe exporter uses HTTP/protobuf via reqwest (no tonic, no openssl) — the
batch processors export from a dedicated non-Tokio thread so the blocking
reqwest client is the right pick, the upstream-recommended pairing.
Feature flags
Section titled “Feature flags”nest-rs-opentelemetry ships two features of its own. You never name them: the
umbrella’s opentelemetry feature turns on otlp, and the umbrella’s http
feature turns on this crate’s http — so the two lines below differ only in
whether the binary serves HTTP at all.
-
otlp(default) — pulls theopentelemetry-*crates, installs the OTLP exporters for traces / metrics / logs, and bridgestracingevents into OTel logs. Without it, the subscriber stays a puretracing-subscribersetup — useful for a tiny binary that should not link the OTel SDK. -
http— pullspoem+nest-rs-http+nest-rs-interceptorsso the per-request interceptor compiles: the remote parent link and the sampling decision. A headless app (a queue worker) usesotlpwithouthttpand stays free of the HTTP transport. Requiresotlp— the interceptor reads the trace id off the OTel context.Without it you still get the request span, the access log and the whole W3C trace context —
trace_id,span_id,traceparentacross a queue: those are the transport’s, and they do not wait on this crate.
nest-rs = { workspace = true, features = ["opentelemetry"] }nest-rs = { workspace = true, features = ["http", "opentelemetry"] }Boot-time safety
Section titled “Boot-time safety”thread 'main' panicked at 'OpenTelemetryModule was imported without calling`OpenTelemetry::init` first — the global tracer and meter are no-ops, sotraces and metrics would be silently dropped. Add `let _otel =nest_rs::opentelemetry::OpenTelemetry::init("<service>")?;` at the top of`main`, before building the app.'Forgetting OpenTelemetry::init is the failure mode that hurts in production:
the backend stays quiet, the team thinks observability is on, and the first
time someone needs a trace there’s nothing to grep. The boot guard makes it a
deterministic startup error instead — visible the first time you run the
binary locally.
use nest_rs::testing::TestApp;
#[tokio::test]async fn users_list_returns_a_page() { let app = TestApp::builder() .module::<ApiModule>() .with_test_telemetry() .build() .await .unwrap();
app.http().get("/users").send().await.assert_status_is_ok();}with_test_telemetry calls OpenTelemetry::init_for_tests — a console-only
init honouring NESTRS_LOG then RUST_LOG (default warn for noise control). It is
idempotent; the first test to run wins, the rest no-op. It is gated by the
opentelemetry feature on nest-rs-testing.
A test that does not import OpenTelemetryModule does not need it.
Reference
Section titled “Reference”crates/nest-rs-opentelemetry/—OpenTelemetry::init,OpenTelemetryConfig,OpenTelemetryModule,OpenTelemetryMeter,LogFormat.crates/nest-rs-server-timing/— theServer-Timingheader middleware.
Going further
Section titled “Going further”- Traces — the spans this exporter ships, and what each edge opens.
- Logs — the record shape, and the ids every line carries.
- Correlation — the trace context underneath, which waits on no crate.
- Server-Timing — the companion header summarising a request’s main spans.