Skip to main content
nestrs builds its observability story around three composable layers: a global tracing subscriber for structured logging, a request-tracing middleware that emits per-request spans and log lines, and an optional Prometheus scrape endpoint. A fourth layer — OpenTelemetry span export — is behind the otel feature flag and can be added without changing the rest of the pipeline.

Installing the tracing subscriber

Call configure_tracing once, before listen, so all log output and request spans share the same pipeline. The log level is read from NESTRS_LOG first, then RUST_LOG, then the default_directive on TracingConfig (default "info"):
use nestrs::prelude::*;

#[module]
struct AppModule;

#[tokio::main]
async fn main() {
    let tracing = TracingConfig::builder()
        .format(TracingFormat::Json)           // Pretty for local dev
        .default_directive("info,nestrs=debug");

    NestFactory::create::<AppModule>()
        .configure_tracing(tracing)
        .listen(3000)
        .await;
}
TracingFormat::Pretty is the default and produces human-readable multi-line output. Switch to TracingFormat::Json in production for log aggregation platforms.

Request tracing middleware

use_request_tracing adds a middleware that:
  • Records a completion log line with method, path, status, duration_ms, and request_id (when use_request_id() is also enabled).
  • Creates a tracing span named http.server.request for each request, with fields http.request.method and http.route.
Skip high-volume infrastructure paths (metrics, health) to avoid flooding request logs:
NestFactory::create::<AppModule>()
    .configure_tracing(TracingConfig::builder())
    .use_request_id()
    .use_request_tracing(
        RequestTracingOptions::builder()
            .skip_paths(["/metrics", "/health"])
    )
    .enable_metrics("/metrics")
    .enable_health_check("/health")
    .listen(3000)
    .await;
http.route is set to the concrete request path at this middleware layer. Axum’s route template (e.g. /users/:id) is not available here. For OTLP dashboards, treat the literal path as the closest stable route identifier unless you add a custom layer that sets a template field.

Prometheus metrics

enable_metrics registers a Prometheus scrape handler at the path you provide (default /metrics). It tracks:
  • http_request_duration_seconds — histogram with standard buckets
  • http_requests_total{method, status} — counter
  • http_requests_in_flight — in-flight gauge
NestFactory::create::<AppModule>()
    .use_request_tracing(RequestTracingOptions::builder().skip_paths(["/metrics"]))
    .enable_metrics("/metrics")
    .listen(3000)
    .await;
The /metrics path is mounted at the server root — it is not affected by set_global_prefix or enable_uri_versioning.

Health and readiness checks

Use enable_health_check for a simple liveness probe that always returns 200:
.enable_health_check("/health")
Use enable_readiness_check when you want to gate traffic on the health of dependencies. Implement HealthIndicator for each dependency and pass the indicators at startup:
use nestrs::prelude::*;
use std::sync::Arc;

pub struct DatabaseHealth {
    pool: Arc<sqlx::PgPool>,
}

#[async_trait]
impl HealthIndicator for DatabaseHealth {
    fn name(&self) -> &'static str { "database" }

    async fn check(&self) -> HealthStatus {
        match sqlx::query("SELECT 1").execute(self.pool.as_ref()).await {
            Ok(_) => HealthStatus::Up,
            Err(e) => HealthStatus::down(e.to_string()),
        }
    }
}

// In main:
let db_health = Arc::new(DatabaseHealth { pool: pool.clone() });

NestFactory::create::<AppModule>()
    .enable_readiness_check("/ready", [db_health as Arc<dyn HealthIndicator>])
    .listen(3000)
    .await;
When any indicator returns HealthStatus::Down, the readiness endpoint returns 503 with a Terminus-style JSON summary containing status, info, error, and details keys.

OpenTelemetry (OTLP)

Enable the otel feature to export spans to any OTLP-compatible collector (Jaeger, Tempo, Honeycomb, etc.):
[dependencies]
nestrs = { version = "0.3.8", features = ["otel"] }
Replace configure_tracing with configure_tracing_opentelemetry and supply an OpenTelemetryConfig:
use nestrs::prelude::*;

#[module]
struct AppModule;

#[tokio::main]
async fn main() {
    let tracing = TracingConfig::builder().format(TracingFormat::Json);
    let otel = OpenTelemetryConfig::new("my-service")
        .endpoint("http://localhost:4317")
        .sample_ratio(1.0);

    NestFactory::create::<AppModule>()
        .configure_tracing_opentelemetry(tracing, otel)
        .use_request_id()
        .use_request_tracing(RequestTracingOptions::builder().skip_paths(["/metrics"]))
        .enable_metrics("/metrics")
        .listen(3000)
        .await;
}
When endpoint is not set, nestrs falls back to the OTEL_EXPORTER_OTLP_ENDPOINT environment variable, then to http://localhost:4317.
Use try_init_tracing or try_init_tracing_opentelemetry directly if you need to install the tracing subscriber outside of the NestApplication builder chain (for example in test harnesses or CLI tools).

Environment variables reference

VariableRole
NESTRS_LOGPrimary log filter directive. Overrides RUST_LOG and TracingConfig::default_directive.
RUST_LOGFallback filter when NESTRS_LOG is unset (standard tracing-subscriber semantics).
OTEL_EXPORTER_OTLP_ENDPOINTOTLP collector address when not set via OpenTelemetryConfig::endpoint.

Troubleshooting

SymptomWhat to check
No log outputconfigure_tracing must run before listen. Check NESTRS_LOG / RUST_LOG and confirm nothing else installs a conflicting global subscriber.
/metrics floods access logsAdd /metrics to RequestTracingOptions::skip_paths.
Spans missing in Jaeger or TempoConfirm the otel feature is enabled, the endpoint URL is reachable, and the sampling ratio is > 0. Check that the collector receives traffic on the expected gRPC or HTTP port.
High cardinality in http.routeExpected — path is the literal URI at this layer. Add a custom layer or a business metric if you need route-template labels in OTLP dashboards.

Local dev vs production

TracingConfig::builder()
    .format(TracingFormat::Pretty)
    .default_directive("debug,nestrs=trace")