# Structured tracing for a Rust service

> Initialize tracing-subscriber with runtime filtering and newline-delimited JSON output, then keep logs, spans, and exported telemetry conceptually separate.

HTML: https://rustprint.com/recipes/structured-tracing/

Async services quickly make plain text logging hard to follow. Tokio tasks can interleave work on the same runtime threads, so "line 12 happened after line 11" is not enough context. The `tracing` ecosystem models diagnostic events and spans instead of treating every message as an isolated string.

## Start with one subscriber

**JSON tracing with RUST_LOG filtering**

```rust
fn init_tracing() {
    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));

    tracing_subscriber::fmt()
        .with_env_filter(filter)
        .json()
        .init();
}
```

The checked setup does two jobs:

- `EnvFilter` reads runtime filtering directives, normally from `RUST_LOG`
- the formatter emits newline-delimited JSON records

If `RUST_LOG` is missing or invalid, the example falls back to `info`.

Examples of useful filters include:

```text
RUST_LOG=debug
RUST_LOG=my_service=debug
RUST_LOG=my_service=debug,hyper=warn
```

Filtering is operational configuration. You should not need a rebuild just to temporarily increase detail for one module.

## JSON logs are still logs

The JSON formatter is useful when a log collector parses structured records. It does not by itself export distributed traces, create metrics, or send anything to an observability backend.

Keep the layers separate:

- `tracing` records events and spans in the application
- a subscriber decides how those records are processed
- the JSON formatter writes structured log lines
- an OpenTelemetry layer, if you add one later, is a separate export decision

That separation makes it possible to keep local development readable while production ships structured records somewhere else.

## Instrument values, not preformatted sentences

Prefer fields that a collector can query.

Conceptually:

```rust
tracing::info!(user_id, latency_ms, "request completed");
```

is more useful than constructing one large string that contains the same values. The event still has a human message, but the interesting data remain fields.

For request tracing, add fields such as route, method, status, request ID, and latency at the middleware boundary. Avoid recording secrets, authorization headers, session tokens, or raw request bodies by default.

## Use spans for context that lasts

A span represents a period of execution with associated fields. Events emitted inside that span can carry the surrounding context.

That is especially useful in async code, where a logical request may move between runtime threads. It is one of the main reasons `tracing` is a better fit for Tokio applications than reasoning from thread-oriented log lines.

## Initialization belongs at process startup

Install the subscriber before starting the listener or spawning long-lived workers. A global subscriber is process-level infrastructure, and trying to initialize it more than once can fail or panic depending on the API used.

Libraries should emit `tracing` events. The final application should decide which subscriber and output format to install.

## What to add next

Do not add an observability stack just because a tutorial had one.

Add another layer when you have a concrete need:

- OpenTelemetry export when traces must cross service boundaries
- metrics when rates, saturation, and latency distributions matter
- log shipping when operators need centralized retention and search
- request IDs when one request must be followed across components

The [Axum REST API blueprint](/blueprints/axum-rest-api/index.md) uses this minimal subscriber so the example has a real production-facing logging seam without pretending to be a complete observability platform.

## Sources

- [tracing overview](https://docs.rs/tracing/0.1.44/tracing/)
- [tracing-subscriber fmt](https://docs.rs/tracing-subscriber/0.3.23/tracing_subscriber/fmt/index.html)
- [EnvFilter](https://docs.rs/tracing-subscriber/0.3.23/tracing_subscriber/filter/struct.EnvFilter.html)

## Related RustPrint guides

- [Production-oriented REST API with Axum](/blueprints/axum-rest-api/index.md): A checked Axum 0.8 service skeleton with typed state, JSON handlers, HTTP error responses, in-process tests, structured tracing, and graceful shutdown.
- [Graceful shutdown with Tokio and Axum](/recipes/graceful-shutdown-tokio/index.md): Handle Ctrl+C and SIGTERM, trigger Axum graceful shutdown, and understand what still needs explicit cancellation in background tasks.
- [How RustPrint verifies code and technical claims](/methodology/index.md): What SOURCE-BACKED and CI-CHECKED mean, what RustPrint actually validates, and which claims the verification system does not make.
