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,确保你能立刻用于实际开发当中。