Rust Tracing 实战指南:从基础用法到生产级落地

0 阅读8分钟

Rust Tracing 实战指南:从基础用法到生产级落地

在分布式系统开发中,可观测性是定位问题、保障系统稳定的核心能力。Rust 的标准 log 库仅能满足简单日志输出,无法关联请求上下文、追踪链路流转,难以满足复杂场景的调试与运维需求。而 tracing 作为 Tokio 团队主导的可观测性框架,凭借 Span 链路追踪、结构化日志等特性,已经成为 Rust 生态中复杂系统的首选工具。

核心概念:Span 与 Event

Tracing 的核心设计是围绕链路追踪展开的,通过两个核心概念 Span 和 Event 实现上下文关联与关键信息记录。

  • Span(跨度):描述一个操作的时间范围和上下文,是链路追踪的基本单元。每个 Span 可以包含多个子 Span,形成层级化的调用链路,用于清晰展示操作的链路过程。
  • Event(事件):记录 Span 生命周期内的关键瞬间,类似于日志记录,可携带键值对形式的结构化数据,便于后续筛选、解析和调试。

快速入门

首先我们需要我们以下这些依赖:

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

Tracing 依赖订阅者(Subscriber)处理 Span 和 Event,需在程序入口初始化订阅者,这里我们先实现一个简单的初始化函数:

fn init_tracing() {
    // tracing_subscriber::fmt::layer() 提供终端输出
    tracing_subscriber::registry().with(fmt::layer()).init();
}

Tracing 提供两种核心埋点方式:手动创建 Span 和自动创建 Span(使用宏),可根据场景灵活选择,这里两种方式我们都会展示。

fn main() {
    init_tracing();

    let root_span = span!(Level::INFO, "demo");

    let _enter = root_span.enter();

    info!("hello world");
}

// INFO demo: tracing_demo: hello world

这里我们通过 span! 宏创建 Span,调用 enter() 函数进入 Span 上下文,然后使用 info! 宏记录 Event。

span! 宏的第一个参数为日志类型,第二个参数为 Span 名称,后续的参数可以携带字段信息,如下所示:

let root_span = span!(Level::INFO, "demo", greet = "hello world");
let _enter = root_span.enter();

info!("hello world");

// INFO demo{greet="hello world"}: tracing_demo: hello world

携带字段的值可以先使用 Empty 占用着,后面在赋值。

let root_span = span!(Level::INFO, "demo", greet = tracing::field::Empty);

let _enter = root_span.enter();

root_span.record("greet", "hello world");

info!("hello world");

// INFO demo{greet="hello world"}: tracing_demo: hello world

除了 info!,还有 trace!debug!warn!error! 这些 Event,这和我们打印日志的用法是一样的。它们其实都是基于 event! 的封装。

// 演示全部
let root_span = span!(Level::INFO, "demo");

let _enter = root_span.enter();

trace!("this is trace");
debug!("this is debug");
info!("this is info");
warn!("this is warn");
error!("this is error");
event!(Level::INFO, "this is event");

// TRACE demo: tracing_demo: this is trace
// DEBUG demo: tracing_demo: this is debug
// INFO demo: tracing_demo: this is info
// WARN demo: tracing_demo: this is warn
// ERROR demo: tracing_demo: this is error
// INFO demo: tracing_demo: this is event

这里我们要理清一点,Span 和 Event 是分别独立的,Event 的使用是不需要依赖 Span 提供的上下文信息,如下所示:

info!("this is info");

// INFO tracing_demo: this is info

可以看到,当不使用 Span 时,Event 打印出来的信息除了缺少 Span 提供的上下文信息外,没有什么区别。

Event 除了支持字符串的形式打印内容外,还支持结构化字段的形式打印内容。字段的值还可以是结构体,不过需要通过 Tracing 提供 debug 函数或 ? 语法糖才能打印出来,结构体的字段同样也能打印出来,需要通过 Tracing 提供 display 函数或 % 语法糖。

#[derive(Debug)]
struct Demo {
    message: String,
}

let demo = Demo {
    message: String::from("hello tom"),
};

// 字符串形式
info!("hello {}", "tom");
// 结构化字段形式
info!(greet = "hello tom");

info!(greet = "hello tom", demo = ?&demo);
info!(greet = "hello tom", demo = tracing::field::debug(&demo));

info!(greet ="hello tom", message = %&demo.message);
info!(
    greet = "hello tom",
    message = tracing::field::display(&demo.message)
);

// INFO tracing_demo: hello tom
// INFO tracing_demo: greet="hello tom"
// INFO tracing_demo: greet="hello tom" demo=Demo { message: "hello tom" }
// INFO tracing_demo: greet="hello tom" demo=Demo { message: "hello tom" }
// INFO tracing_demo: greet="hello tom" hello tom
// INFO tracing_demo: greet="hello tom" hello tom

Span 同样支持嵌套 Span,这才处理子逻辑的时候非常有用,如调用外部服务、数据查询等等。

let root_span = span!(Level::INFO, "root_span");
let _enter = root_span.enter();

info!("执行子 Span 前");

{
    let sub_span = span!(Level::INFO, "sub_span");
    let _enter = sub_span.enter();

    info!("执行子 Span");
} // 子 Span 自动退出(sub_span 生命周期结束)

info!("执行子 Span 后");

// INFO root_span: tracing_demo: 执行子 Span 前
// INFO root_span:sub_span: tracing_demo: 执行子 Span
// INFO root_span: tracing_demo: 执行子 Span 后

前面的 Span 用法都是手动创建的方式,Tracing 同样提供了自动创建 Span 的方式,主要是用于函数/方法的场景,这里会用到 #[instrument] 宏。

// Span 名称为函数名
// level 设置日志类型
#[instrument(level = "INFO")]
fn function1(arg1: u16) {
    info!("this is function1");

    function2(arg1);
}

// skip 可以不记录参数字段,处理调用带来的字段冗余
#[instrument(level = "INFO", skip(arg1))]
fn function2(arg1: u16) {
    info!("this is function2");
}

let root_span = span!(Level::INFO, "root_span");
let _enter = root_span.enter();

function1(1);

// INFO root_span:function1{arg1=1}: tracing_demo: this is function1
// INFO root_span:function1{arg1=1}:function2: tracing_demo: this is function2

进阶技巧

日志级别

Tracing 同样也支持基于日记级别打印内容,在 Subscriber 中可以设置日志级别,需要用到 tracing-subscriber 提供的 env-filter feature,修改 Cargo.toml

[dependencies]
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

修改 Subscriber 代码,将日志级别设置为 INFO:

fn init_tracing() {
    tracing_subscriber::registry()
        .with(LevelFilter::INFO)
        .with(fmt::layer())
        .init();
}

日志级别的设置会将 Span 和 Event 都影响到,如下所示:

// 日志级别为 INFO

let debug_span = span!(Level::DEBUG, "debug_span");
let _enter = debug_span.enter();
trace!("this is trace");
debug!("this is debug");
info!("this is info");
warn!("this is warn");
error!("this is error");

let info_span = span!(Level::INFO, "info_span");
let _enter = info_span.enter();
trace!("this is trace");
debug!("this is debug");
info!("this is info");
warn!("this is warn");
error!("this is error");

// INFO tracing_demo: this is info
// WARN tracing_demo: this is warn
// ERROR tracing_demo: this is error
// INFO info_span: tracing_demo: this is info
// WARN info_span: tracing_demo: this is warn
// ERROR info_span: tracing_demo: this is error

除了在代码中修改日志级别外,我们还可以通过环境变量修改日志级别,依旧是需要修改 Subscriber 代码:

fn init_tracing() {
    // 从环境变量 RUST_LOG(默认) 读取日志级别,默认设为 info
    // 如果想从其他环境变量中读取,使用 try_from_env 这个函数
    let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));

    tracing_subscriber::registry()
        .with(env_filter)
        .with(fmt::layer())
        .init();
}

执行 RUST_LOG=debug cargo run 测试,结果如下:

trace!("this is trace");
debug!("this is debug");
info!("this is info");
warn!("this is warn");
error!("this is error");

// DEBUG tracing_demo: this is debug
// INFO tracing_demo: this is info
// WARN tracing_demo: this is warn
// ERROR tracing_demo: this is error

异步场景

在异步场景下,不要直接采用手动创建 Span 的方式,这会导致上下文丢失。有两种方式,第一种就使用 #[instrument] 宏自动添加 Span,另一种就是使用 tracing::Instrument 来手动创建 Span。

添加 tokio 依赖提供异步运行时环境:

[dependencies]
tokio = { version = "1.51.0", features = ["full"] }

示例代码如下所示:

use tracing::Instrument; // <- 重点,别忘了引入

async fn async_fn1() {
    info!("这里是异步函数一");
}

#[instrument(level = "INFO")]
async fn async_fn2() {
    info!("这里是异步函数二");
}

async fn async_fn3() {
    info!("这里是异步函数三");
}

async_fn1().await;
tokio::spawn(async move {
    async_fn2().await;
});
tokio::spawn(async move {
    async_fn3()
        .instrument(tracing::info_span!("async_fn3"))
        .await;
});

tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;

// INFO tracing_demo: 这里是异步函数一
// INFO async_fn2: tracing_demo: 这里是异步函数二
// INFO async_fn3: tracing_demo: 这里是异步函数三

自定义日志格式

在生产环境中,常常需要用到 JSON 格式,这样便于日志收集(如 ELK、Loki),还可能会需要自定义时间戳、Span 信息等字段。这里我们来用到 JSON Layer 来进行自定义。首先我们需要修改依赖:

tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "chrono"] }
chrono = "0.4"

示例代码如下所示:

 use tracing::info;
use tracing_subscriber::fmt::time::ChronoUtc;
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};

fn init_tracing() {
    tracing_subscriber::registry()
        .with(
            fmt::layer()
                .json()
                // 原生 RFC3339 格式,生产环境最推荐
                .with_timer(ChronoUtc::rfc_3339())
                // 包含当前 Span 名称
                .with_current_span(true)
                // 包含 Span 链路列表,
                .with_span_list(true)
                // 扁平化事件字段(避免嵌套)
                .flatten_event(true),
        )
        .init();
}

fn main() {
    init_tracing();

    info!("hello world");
}

// {"timestamp":"2026-04-08T13:44:25.561593+00:00","level":"INFO","message":"hello world","target":"tracing_demo"}

输出日志文件

这里我们会用到 tracing-appender 来实现,添加依赖:

[dependencies]
tracing-appender = "0.2"

示例代码如下所示:

use anyhow::{Ok, Result};
use tracing::info;
use tracing_appender::{
    non_blocking::WorkerGuard,
    rolling::{RollingFileAppender, Rotation},
};
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};

fn init_tracing() -> Result<WorkerGuard> {
    let file_appender = RollingFileAppender::builder()
        .rotation(Rotation::DAILY) // 轮转策略:每天 00:00 切分新文件
        .filename_prefix("app") // 日志文件名前缀
        .filename_suffix("log") // 日志文件名后缀
        .build("./logs")?; // 日志存放目录(自动创建)

    // 非阻塞式写入
    // guard 必须保持存活,否则后台写入线程会结束
    let (non_blocking_writer, guard) = tracing_appender::non_blocking(file_appender);

    tracing_subscriber::registry()
        .with(fmt::layer())
        .with(
            fmt::layer()
                .json()
                .with_ansi(false)
                .with_writer(non_blocking_writer), // 使用非阻塞式写入
        )
        .init();

    // 返回 guard,必须在 main 函数中持有它
    Ok(guard)
}

fn main() -> Result<()> {
    // 初始化 tracing,必须将 guard 绑定到变量,否则会立即 drop 导致后台线程结束
    let _guard = init_tracing()?;

    info!("helo world");

    Ok(())
}

总结

看完这篇文章相信你已经大概知道怎么使用 Tracing 了,之后我还会再一篇文章来实现一个更加完整、更加贴近生产环境的实战案例,同时还会接入 Otel,确保你能立刻用于实际开发当中。