Library / SDK
tokio-rs/tracing avatar
tokio-rs/tracing

tracing: the Rust instrumentation framework whose two traps are scope and lifetime

Application level tracing for Rust.

6,978 stars969 forksRustMIT

At a glance

What is it?
Maintained by the Tokio project but independent of the Tokio runtime, tracing collects structured events and prints nothing until a subscriber is installed. Its two documented failure modes are a library taking the global slot and a span guard held across an await.
Who is it for?
Take tracing when you need spans and events correlated across a Rust service and you control the binary that installs the subscriber. If you are shipping a library, depend only on the tracing crate and leave installing a subscriber to the executable, because events emitted outside one are collected by nobody.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • tokio-rs/tracing6,978 ★ · +91
Star history
Repository2026-09-232026-11-13
tokio-rs/tracing6.9k7k

91 Stars in the last 51 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

No tokio runtime required, but a subscriber is

tracing is a framework for instrumenting Rust programs to collect structured, event-based diagnostic information. It is maintained by the Tokio project and it does not require the tokio runtime to be used, which is the first thing worth separating from the name. What it does require is something on the other end: an executable has to use a Subscriber implementation to collect trace data, and any trace event generated outside the context of a subscriber will not be collected. The crate emits; the subscriber decides where events go.

For a default that logs to standard output, tracing-subscriber provides an fmt module, and it can also consume messages emitted by log-instrumented libraries and modules. Two dependencies get you there:

toml
[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"

Then install a subscriber once, at startup. The call below reads its configuration from the RUST_LOG environment variable:

rust
use tracing::info;
use tracing_subscriber;

fn main() {
    // install global subscriber configured based on RUST_LOG envvar.
    tracing_subscriber::fmt::init();

    let number_of_yaks = 3;
    // this creates a new event, outside of any spans.
    info!(number_of_yaks, "preparing to shave yaks");

    let number_shaved = yak_shave::shave_all(number_of_yaks);
    info!(
        all_yaks_shaved = number_shaved == number_of_yaks,
        "yak shaving completed."
    );
}

init() calls set_global_default(), so that subscriber becomes the default in all threads for the rest of the program's run, the way loggers behave in the log crate.

with_default only changes the thread that called it

When you need control over filtering without owning the global slot, the subscriber can be built in stages, left uninstalled, and applied as a local override for a block of code:

rust
use tracing::{info, Level};
use tracing_subscriber;

fn main() {
    let subscriber = tracing_subscriber::fmt()
        .with_max_level(Level::TRACE)
        .finish();

    tracing::subscriber::with_default(subscriber, || {
        info!("This will be logged to stdout");
    });
    info!("This will _not_ be logged to stdout");
}

Two constraints make this narrower than it looks. The override applies only to the currently executing thread, so other threads will not see the change from with_default. And the subscriber is built but not installed, which is the point: it never touches the global default. That is the mechanism you want in a test, where one thread asserts on captured events, and the mechanism that will quietly give you nothing in a worker pool, where the tasks that emit the interesting events are not the ones that installed the subscriber.

A library that installs a subscriber takes the global slot

The guidance for library authors is unambiguous: rely only on the tracing crate and use its macros and types to collect whatever might be useful downstream, and do not install a subscriber by using a method that calls set_global_default(). The stated reason is that doing so causes conflicts when the executable tries to set the default later.

That makes the global default a single contested resource with two would-be owners, and the library is the worse owner. A binary that has already installed its own subscriber will not get yours, and if your library wins the race because it initialises first, the binary's own configuration is the one that loses. Nothing about this is version specific and nothing is fixed by pinning, so the safe rule for a dependency you do not control is to treat any initialisation it performs as a defect in that dependency.

Once a subscriber is set, instrumentation points go in through the macros. The order matters for review purposes: subscriber first, macros second. A macro added before anything is installed produces events that nobody collects, which is exactly the case the documentation warns about.

span.enter() inside an async block keeps the span alive too long

Async code has its own documented trap. The preferred method is the instrument attribute, but code that works with std::future::Future or uses async and await blocks needs special handling, and the obvious pattern will not work:

rust
async {
    let _s = span.enter();
    // ...
}

The reason is a lifetime argument rather than a compile error. The span guard does not exit until the future generated by the async block is complete. Futures and spans can be entered and exited multiple times without either completing, so the span remains entered for as long as the future exists rather than being entered only when it is polled. The documented outcome is very confusing and incorrect output.

The practical damage is that time gets attributed to the wrong span. A span entered for a whole future looks open across every yield point inside it, including the ones where your code is waiting on IO and doing nothing. If you compute durations from span boundaries, those numbers will not mean what they appear to mean, and no compile error or test failure will point at the cause.

instrument names the span after the function and records its parameters

The attribute macro does the work that hand-written span handling gets wrong in async code. Applied to a function, it creates and enters a span every time the instrumented function is called, names the span after the function or method, and records the parameters passed to the function as fields. There is no guard to hold across an await and nothing to remember to exit, which is why it is the recommended route for tracing async fn.

The catch is that the field names come from your parameter names. Rename a function argument from yak to yak_count and the structured event changes key, with no compiler error, no changelog entry and no deprecation path. Anything downstream that queries these fields by name, whether that is a log pipeline, a dashboard or an alert rule, breaks quietly.

That makes parameter names in an instrumented function part of your public telemetry surface. Treating them as such costs nothing while writing the signature and saves a migration later, because unlike a log message nobody has to update, a field key is read by machines that are not in the repository.

main and v0.2.x hold different futures for the same crates

The branch layout is unusual enough to trip up anyone following an older link. main is the default branch and crates.io releases are cut from it. That branch was previously called v0.1.x. The v0.2.x branch holds the as-yet unreleased 0.2 version of tracing-core, tracing and all the other tracing crates that depend on those versions, and that branch was previously called master.

So the names have been swapped relative to the usual convention, where master is the trunk and a version branch is the odd one out. An older tutorial that tells you to check out master is now pointing at the 0.2 line, and one that says v0.1.x is now pointing at the released line. Both instructions still work mechanically and both get you somewhere you did not intend.

The second consequence is more useful. A 0.2 line exists in the repository today and is not what you get from crates.io. If you are evaluating whether the API you are about to write against 0.1 will survive, the v0.2.x branch is where that question can be answered, and main is not the place to look.

Fifteen workspace crates on unrelated version lines

The workspace manifest lists fifteen members plus an examples package: tracing, tracing-core, tracing-attributes, tracing-error, tracing-flame, tracing-futures, tracing-tower, tracing-log, tracing-macros, tracing-mock, tracing-subscriber, tracing-serde, tracing-test, tracing-appender and tracing-journald. They share a repository and a release process, not a version number. The recent tags make that concrete: tracing-appender 0.2.5 published 2026-04-17, tracing-subscriber 0.3.23 on 2026-03-13, tracing 0.1.44 on 2025-12-18.

So a lockfile will contain tracing at 0.1 and tracing-subscriber at 0.3 at the same time, and neither number tells you anything about the other. When you write dependencies as tracing = "0.1" and tracing-subscriber = "0.3", you are pinning two independent lines, and an update to one can pull a change in the other without any version relationship announcing it.

The workspace manifest also declares lint configuration for cfg names, including flaky_tests, tracing_unstable and unsound_local_offset, with a comment explaining that the check-cfg lint exists to fix issues that first appeared in Rust 1.80 and that the table is ignored on Rust older than 1.74.

tracing-log absorbs log output, and nothing states the reverse

The interoperation story with the older log ecosystem is one directional in the documentation. tracing-subscriber is able to consume messages emitted by log-instrumented libraries and modules. That is the stated capability, and it is the reason a service can adopt tracing incrementally: dependencies that still use log keep working, and their output lands in the same subscriber as your tracing events.

The visible part of the README does not cover the reverse path, so check the crate documentation before assuming that a component still reading plain log output will see the spans and structured fields your other components produce.

The rest of the tree is worth knowing for the same reason. tracing-flame produces a flamegraph from span timings, tracing-journald targets the systemd journal, tracing-tower bridges Tower middleware, tracing-error builds error events, tracing-futures instruments futures directly, and tracing-mock and tracing-test exist for asserting on captured events. Each one is a separate crate with its own release cadence, so adopting a companion crate is its own dependency decision.

Editorial conclusion

Take tracing when you need spans and events correlated across a Rust service and you control the binary that installs the subscriber. If you are shipping a library, depend only on the tracing crate and leave installing a subscriber to the executable, because events emitted outside one are collected by nobody. Before you build on the 0.2 line, note that it lives on the v0.2.x branch and that crates.io releases are cut from main.

Frequently asked questions

Does tracing require the tokio runtime?

No. The project states that tracing is maintained by the Tokio project but does not require the tokio runtime to be used. What a program does need is a Subscriber, since any trace event generated outside the context of a subscriber will not be collected.

Why should a library not call set_global_default on tracing?

Because it conflicts with the executable that depends on it. The guidance for library authors is to rely only on the tracing crate and its macros and types, and not to install a subscriber through a method that calls set_global_default, since the executable will try to set the default later.

Where does the unreleased 0.2 version of tracing live?

On the v0.2.x branch, which holds the as-yet unreleased 0.2 version of tracing-core, tracing and all the tracing crates depending on those versions. crates.io releases are cut from main, which was previously the v0.1.x branch, while v0.2.x was previously called master.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. tokio-rs/tracing on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tokio-rs-tracing.svg)](https://hysenlabs.com/projects/tokio-rs-tracing)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/tokio-rs-tracing.svg)](https://hysenlabs.com/projects/tokio-rs-tracing)

Was this useful?