Skip to main content
nestrs builds its observability story around four composable layers: a global tracing subscriber for structured logging, a request-tracing middleware that emits per-request spans and log lines, an optional Prometheus scrape endpoint, and — behind the otel feature flag — OpenTelemetry export for traces, metrics, and logs that can be layered on 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"):
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:
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
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:
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:
When any indicator returns HealthStatus::Down, the readiness endpoint returns 503 with a Terminus-style JSON summary containing status, info, error, and details keys.

Probe decorators (#[liveness], #[readiness], #[startup])

Beyond the always-OK liveness route, nestrs can mirror a real handler as a probe. Decorating a route handler with one of the probe decorators mounts a fixed endpoint under /__nestrs/health/* that calls the handler on each scrape (or once, for startup) and reports up/down from its status code:
The mirrored endpoints are mounted at the server root (not under set_global_prefix or URI versioning) so orchestrators can probe them without prefixes. Probe results are cached for 5 seconds per endpoint to prevent probe-storm self-DoS; a panicking handler reports down with a generic message (raw errors go to tracing only).
If your own routes already occupy /__nestrs/health/live or /__nestrs/health/ready, your routes win — the framework skips its mirrored endpoint. The builder-level enable_health_check(“/health”) and enable_readiness_check(“/ready”, […]) paths above are unaffected and remain the simplest options for most apps.
Use decorator-based probes when liveness should depend on something the app actually does (a warmup pass, a self-check handler), and the builder calls when a constant 200 plus indicator checks is enough. Kubernetes wiring: livenessProbe → /__nestrs/health/live (or your enable_health_check path), readinessProbe → /__nestrs/health/ready (or your readiness path), startupProbe → /__nestrs/health/startup.

OpenTelemetry (OTLP)

Enable the otel feature to export to any OTLP-compatible collector (Jaeger, Tempo, Honeycomb, SigNoz, etc.). Spans export by default; metrics and logs are per-signal opt-ins on the config:
Replace configure_tracing with configure_tracing_opentelemetry and supply an OpenTelemetryConfig:
When endpoint is not set, nestrs falls back to the OTEL_EXPORTER_OTLP_ENDPOINT environment variable, then to http://localhost:4317.

Dual metrics export: Prometheus pull + OTLP push

There is exactly one metrics recording surface in the process — the metrics facade — and nestrs owns it with a fan-out recorder. Every metrics::counter! / gauge! / histogram! call is forwarded to every enabled backend:
  • enable_metrics("/metrics") registers the Prometheus backend (pull, scraped at the path you choose).
  • OpenTelemetryConfig::metrics() registers the OTLP backend (push, on the collector’s schedule).
They compose freely and in either order. An app with both enabled gets identical series in Prometheus and in the collector — the framework’s RED metrics (http_requests_total, http_request_duration_seconds, http_requests_in_flight) and any instruments you define yourself. Use this to bridge a migration (Prometheus today, OTLP tomorrow) or to serve both a local scrape and a central pipeline.
Units are translated to OpenTelemetry’s UCUM-style conventions when pushed: facade Unit::Seconds exports as s, Unit::Count as 1. Facade labels become OTel attributes 1:1 (counter!(“rps”, “route” => “/x”) exports with attribute route=“/x”).

Custom instruments

Record your own metrics through the same surface — the metrics facade is re-exported at nestrs::metrics, so no extra dependency is needed:
Instruments declared before first use with nestrs::metrics::describe_counter!(...) etc. carry their unit and description into both backends.

Logs over OTLP

OpenTelemetryConfig::logs() bridges the tracing facade into the OTLP log pipeline: every tracing::info! / warn! / error! event becomes an OpenTelemetry log record on the same collector as your traces. Events emitted inside an active span are correlated with its trace and span IDs, so a trace in Jaeger and its surrounding log lines in Loki/Tempo line up automatically. The local tracing output (pretty or JSON) is unchanged — OTLP is an additional destination, not a replacement.

Runtime requirements and shutdown

  • Install from async context. The OTLP exporters are lazy — a missing collector never fails startup — but their transport is spawned onto the current Tokio reactor at construction. Call configure_tracing_opentelemetry from #[tokio::main] (before listen), exactly as in the examples above.
  • Metric export cadence follows the collector convention: every 60s by default, overridable with OTEL_METRIC_EXPORT_INTERVAL (milliseconds).
  • Shutdown flushes. NestApplication::listen* methods flush and stop the OTLP pipelines on graceful shutdown, so the last metrics window and log batch reach the collector. Calling the install functions directly (nestrs::otel::install_otlp_meter / install_otlp_logger) pairs with nestrs::otel::shutdown_meter_provider() / shutdown_logger_provider() for custom lifecycles.
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

Troubleshooting

Local dev vs production