Skip to content

OpenTelemetry

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 traceparent propagation, sampling, and the per-request HTTP interceptor stitch distributed traces together.
  • Logstracing::info! and friends flow to the console and to OTel logs (via opentelemetry-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 a PeriodicReader and export through the same OTLP endpoint as the traces.
Terminal window
cargo add nest-rs-opentelemetry

A headless binary (a queue worker) stops there; an app serving HTTP adds the http feature for the per-request interceptor — see Feature flags below.

apps/api/src/main.rs
use anyhow::Result;
use nest_rs_config::Environment;
use nest_rs_core::App;
use nest_rs_opentelemetry::OpenTelemetry;
use api::ApiModule;
#[tokio::main]
async fn main() -> Result<()> {
let _environment = Environment::init();
let _opentelemetry = OpenTelemetry::init("api")?;
App::builder()
.module::<ApiModule>()
.build()
.await?
.run()
.await
}
apps/api/src/module.rs
#[module(
imports = [
OpenTelemetryModule,
ServerTimingModule,
],
)]
pub struct ApiModule;

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.

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 varFieldDefault
NESTRS_OPENTELEMETRY__SERVICE_NAMEservice_nameargument to init
NESTRS_OPENTELEMETRY__SERVICE_VERSIONservice_versionNone
NESTRS_OPENTELEMETRY__SERVICE_ENVIRONMENTdeployment_environmentNone
NESTRS_OPENTELEMETRY__SERVICE_INSTANCE_IDservice_instance_idfresh UUIDv7 per process
NESTRS_OPENTELEMETRY__LOG_LEVELlog_filterinfo
NESTRS_OPENTELEMETRY__LOG_FORMATlog_formattext (json available)
NESTRS_OPENTELEMETRY__LOG_SOURCE_LOCATIONlog_source_locationfalse (on in scaffolded dev)
NESTRS_OPENTELEMETRY__OTLP_ENDPOINTotlp_endpointNone (console-only)
NESTRS_OPENTELEMETRY__SAMPLE_RATIOtrace_sample_ratio1.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 incoming traceparent is parsed and outgoing headers carry one.

Terminal window
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 \
RUST_LOG=info \
nestrs run start api

The 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.

nest-rs-opentelemetry ships two features:

  • otlp (default) — pulls the opentelemetry-* crates, installs the OTLP exporters for traces / metrics / logs, and bridges tracing events into OTel logs. Without it, the subscriber stays a pure tracing-subscriber setup — useful for a tiny binary that should not link the OTel SDK.
  • http — pulls poem + nest-rs-http + nest-rs-middleware so the per-request HTTP interceptor compiles. A headless app (a queue worker) uses otlp without http and stays free of the HTTP transport. Requires otlp — the interceptor reads the trace id off the OTel span.
apps/worker/Cargo.toml
nest-rs-opentelemetry.workspace = true
apps/api/Cargo.toml
nest-rs-opentelemetry = { workspace = true, features = ["http"] }
Terminal window
thread 'main' panicked at 'OpenTelemetryModule was imported without calling
`OpenTelemetry::init` first — the global tracer and meter are no-ops, so
traces and metrics would be silently dropped. Add `let _opentelemetry =
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 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.

  • Per-request context — the request span carries request_id, actor_id, tenant_id when an authn module is mounted. Layer your own fields with tracing::info!(target: "api::users", actor = %auth.sub, …).
  • Server-Timing — the companion nest-rs-server-timing crate adds a Server-Timing header summarising the request’s main spans. Mount with ServerTimingModule.
  • Module lifecycletracing::info! on nest_rs::module fires once per module when its providers register, so the boot trace shows your import tree in resolution order.
  • crates/nest-rs-opentelemetry/OpenTelemetry::init, OpenTelemetryConfig, OpenTelemetryModule, OpenTelemetryMeter, LogFormat.
  • crates/nest-rs-server-timing/ — the Server-Timing header middleware.