Skip to content

Logs

tracing::info!, debug!, warn!, error! are the framework’s logging API. Every event flows through the installed subscriber to the console and, when OTLP is configured, through the OpenTelemetryTracingBridge to OTel logs. Backend collectors index them alongside the traces of the same request.

ConcernTargetDefault level
HTTP request lognest_rs::accessinfo
HTTP spannest_rs::httpinfo
Route / endpoint mounting (boot)nest_rs::routesinfo
ORM queriesnest_rs::ormtrace
Layer composition (per-route guard/pipe/… chains)nest_rs::layerstrace for the per-route effective-chain dump; debug for the redundant multi-scope lint (HTTP deduped once, WS per gateway); warn for actionable posture smells (reversed authn/authz order, denials, routes with no guard and no #[public] when no global pool is active)
Authn / Authznest_rs::authn / nest_rs::authzinfo (denials warn)
WebSocket eventsnest_rs::wsinfo
Queue / Schedulenest_rs::queue / nest_rs::scheduleinfo
Module lifecyclenest_rs::moduleinfo
App-specific spans<app>::<feature>info

Controllers, resolvers, gateways log at info on success. Services at debug. Repo at trace. Access denials and security events at warn+ so they appear under the default RUST_LOG=info. A production deploy should respect info — no per-request debug! on the hot path.

tracing::info!(
target: "api::users",
user_id = %id,
actor = %auth.sub,
"creating user",
);

Always prefer field = %value to format!("{}", value). The OTLP bridge serializes structured fields directly; a formatted string becomes opaque text the backend cannot filter on.

The request span (installed by the HTTP interceptor — see Traces) already carries request_id, actor_id, tenant_id when an authn module is mounted, so your structured fields layer on top of those.

The console layer ships both formats. Default is text (pretty for dev). For prod, set:

Terminal window
NESTRS_OPENTELEMETRY__LOG_FORMAT=json

JSON drops the current_span and span_list fields — the OTel export carries span structure, and duplicating it in the console line is noise.

To trace a console line back to the exact source that emitted it, enable source location:

Terminal window
NESTRS_OPENTELEMETRY__LOG_SOURCE_LOCATION=true

Each event then carries the emitting file:line:

Terminal window
DEBUG nest_rs::layers: crates/nest-rs-core/src/layer_chain.rs: layer declared at multiple scopes…

Off by default — it widens every line and leaks source paths, so it stays out of production. Scaffolded projects turn it on in .env.development (next to LOG_LEVEL=debug), so a fresh nestrs new app ships with it on while developing and off everywhere else. Pin it explicitly with OpenTelemetryConfig::with_log_source_location(true).

The HTTP interceptor emits one access event per request on nest_rs::access, at end-of-body, with byte-accurate bytes and duration_ms:

Terminal window
INFO nest_rs::access: GET /users 200 28ms request_id=01J… bytes=412 client_ip=10.0.0.1 trace_id=4bf92f…

The same fields land on the OTel HTTP span (see Traces), so the access line and the span line up in the backend. Toggle the access line with NESTRS_HTTP__ACCESS_LOG=false; the OTel span still opens, so propagation and OTLP export keep working.

The OpenTelemetryTracingBridge is wired in only when an OTLP endpoint is configured. Without an endpoint, the bridge is skipped — it pays a small per-event cost just to drop the event, which is not worth paying when console-only logging is the goal. When the endpoint is set, every tracing event becomes an OTel log record with the same target, level, fields and trace_id.