Skip to content

Tracing

1. 什么是 tracing

tracing 是 Tokio 生态的结构化日志与诊断框架,解决传统日志的三大问题:

问题 传统 log tracing
异步上下文丢失 无法追踪请求链路 Span 自动传播上下文
日志难以查询 纯文本字符串 结构化键值字段
性能监控 只能打印时间戳 Span 自带开始/结束时间
tracing(门面层)
    │
    ▼
Subscriber(后端实现)
    ├── tracing-subscriber(控制台/文件)
    ├── tracing-opentelemetry(分布式追踪)
    └── 自定义 Subscriber

2. 环境准备

Cargo.toml

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

常用扩展库

[dependencies]
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tracing-appender   = "0.2"   # 异步写文件、日志滚动
tracing-opentelemetry = "0.27" # OpenTelemetry 集成
opentelemetry       = "0.27"

3. 核心概念

三个核心抽象

Span(跨度):表示一段有时间范围的操作,可以嵌套

请求处理 [Span]
  └── 数据库查询 [Span]
        └── SQL 执行 [Span]

Event(事件):某一时刻发生的事情,类似传统日志的一条记录

Subscriber(订阅者):接收 Span 和 Event,决定如何处理(打印、发送到远端等)

日志级别

ERROR > WARN > INFO > DEBUG > TRACE
级别 用途
ERROR 必须处理的错误
WARN 潜在问题,程序仍可运行
INFO 关键业务流程节点
DEBUG 开发调试信息
TRACE 极细粒度,生产一般不开启

4. 基础用法:事件(Event)

最小示例

use tracing::{error, warn, info, debug, trace};

fn main() {
    // 初始化默认订阅者(输出到控制台)
    tracing_subscriber::fmt::init();

    error!("发生严重错误");
    warn!("磁盘空间不足 {}%", 10);
    info!("服务启动,端口 {}", 8080);
    debug!("收到请求: {:?}", "GET /");
    trace!("进入函数 handle_request");
}

输出示例:

2026-05-14T10:00:00.000Z  INFO tracing_demo: 服务启动,端口 8080
2026-05-14T10:00:00.001Z DEBUG tracing_demo: 收到请求: "GET /"

带结构化字段的事件

// 键值对字段(推荐)
info!(port = 8080, host = "localhost", "服务启动");

// 调试格式
info!(request = ?req, "收到请求");

// Display 格式
info!(user = %username, "用户登录");

// 表达式字段
let duration_ms = elapsed.as_millis();
info!(duration_ms, status = 200, "请求完成");

? 使用 Debug 格式,% 使用 Display 格式,不加前缀则字段必须实现 Value trait(基础类型均已实现)。


5. Span:结构化上下文

创建和进入 Span

use tracing::{span, Level, info};

fn handle_request(request_id: &str) {
    // 创建 span
    let span = span!(Level::INFO, "handle_request", request_id);

    // 进入 span(之后的 event 都会附带这个 span 的上下文)
    let _guard = span.enter();

    info!("开始处理");  // 自动关联 handle_request span
    do_work();
    info!("处理完成");

    // _guard drop 时自动退出 span
}

instrument 宏(最常用)

use tracing::instrument;

#[instrument]
fn process(user_id: u64, action: &str) {
    // 函数参数自动成为 span 字段
    info!("执行操作");
}

// 等价于:
// span!(Level::INFO, "process", user_id, action)

instrument 高级配置

use tracing::instrument;

#[instrument(
    level = "debug",          // span 级别
    name = "my_process",      // 自定义 span 名称
    skip(password),           // 跳过敏感字段
    skip_all,                 // 跳过所有参数
    fields(                   // 额外添加字段
        request_id = %uuid::Uuid::new_v4(),
        user.id = user_id,
    ),
    err,                      // 自动记录 Err 到 span
    ret,                      // 自动记录返回值
)]
fn login(username: &str, password: &str, user_id: u64) -> Result<(), String> {
    info!("用户尝试登录");
    Ok(())
}

手动操作 Span 字段

use tracing::{info_span, Instrument};

let span = info_span!("request", request_id = tracing::field::Empty);

// 稍后填充字段
span.record("request_id", &"req-123");

Span 嵌套

#[instrument]
async fn handle_request(id: u64) {
    info!("开始处理请求");

    // 子 span 自动继承父 span 上下文
    fetch_from_db(id).await;
}

#[instrument]
async fn fetch_from_db(id: u64) {
    debug!("查询数据库");  // 输出中会看到父子关系
}

6. Subscriber:日志后端

Subscriber 是 tracing 的核心扩展点,决定如何处理 Span 和 Event。

use tracing::subscriber;

// 设置全局 Subscriber(程序启动时调用一次)
subscriber::set_global_default(my_subscriber)
    .expect("设置 subscriber 失败");

作用域 Subscriber

use tracing::subscriber::with_default;

with_default(my_subscriber, || {
    // 只在此闭包内使用 my_subscriber
    info!("只被 my_subscriber 处理");
});

7. tracing-subscriber 详解

fmt Subscriber(控制台输出)

use tracing_subscriber::fmt;

// 最简初始化
fmt::init();

// 完整配置
fmt::Subscriber::builder()
    .with_max_level(tracing::Level::DEBUG) // 最大日志级别
    .with_target(true)                     // 显示模块路径
    .with_thread_ids(true)                 // 显示线程 ID
    .with_thread_names(true)               // 显示线程名
    .with_file(true)                       // 显示文件名
    .with_line_number(true)                // 显示行号
    .with_level(true)                      // 显示级别
    .with_ansi(true)                       // 彩色输出
    .compact()                             // 紧凑格式
    .init();

EnvFilter:动态过滤

use tracing_subscriber::{fmt, EnvFilter};

fmt::Subscriber::builder()
    .with_env_filter(
        EnvFilter::from_default_env()  // 读取 RUST_LOG 环境变量
            .add_directive("my_app=debug".parse().unwrap())
            .add_directive("my_app::db=trace".parse().unwrap())
    )
    .init();
# 环境变量控制日志级别
RUST_LOG=debug cargo run
RUST_LOG=my_app=info,my_app::db=debug cargo run
RUST_LOG=warn,tokio=error cargo run

EnvFilter 语法

# 全局级别
RUST_LOG=debug

# 指定 crate
RUST_LOG=my_app=info

# 指定模块
RUST_LOG=my_app::handlers=debug

# 指定 span
RUST_LOG=[request]=info

# 组合
RUST_LOG=warn,my_app=debug,my_app::db=trace

Registry + Layer 组合(推荐生产用法)

use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

tracing_subscriber::registry()
    .with(EnvFilter::from_default_env())
    .with(tracing_subscriber::fmt::layer())
    .init();

8. 字段与结构化日志

字段格式速查

let user_id: u64 = 42;
let user = User { name: "Alice".into() };
let err = std::io::Error::last_os_error();

info!(
    // 基本类型(直接值)
    user_id,
    count = 10,
    latency_ms = 42u64,

    // Display 格式(%)
    addr = %"127.0.0.1:8080",

    // Debug 格式(?)
    user = ?user,
    error = ?err,

    // 布尔
    success = true,

    // 消息(必须是最后一个位置参数)
    "请求处理完成"
);

在 Span 上动态记录字段

use tracing::{info_span, field};

let span = info_span!(
    "http_request",
    status = field::Empty,   // 占位符,稍后填充
    latency_ms = field::Empty,
);

let _guard = span.enter();

// ... 处理请求 ...

span.record("status", 200);
span.record("latency_ms", elapsed.as_millis() as u64);

9. 异步代码中的 tracing

问题:异步任务间 Span 上下文丢失

// 错误:enter() 返回的 guard 跨 await 会导致 panic 或上下文丢失
async fn bad_example() {
    let span = info_span!("my_span");
    let _guard = span.enter();
    some_async_fn().await; //  guard 不能跨越 await
}

正确方式:.instrument()

use tracing::Instrument;

async fn good_example() {
    let span = info_span!("my_span");

    // 方式一:Future::instrument
    some_async_fn().instrument(span).await;

    // 方式二:#[instrument] 宏(自动处理)
}

#[instrument]  // 推荐:自动处理异步 span 传播
async fn handle_connection(conn_id: u64) {
    info!("处理连接");
    fetch_data().await;  // span 自动传播
    info!("连接处理完成");
}

tokio::spawn 中传播 Span

use tracing::Instrument;

async fn spawn_with_span() {
    let span = info_span!("parent_task");

    tokio::spawn(
        async move {
            info!("子任务运行中");  // 继承 parent_task span
        }
        .instrument(span),
    );
}

10. 与 log 生态集成

许多库使用 log crate,通过桥接层统一输出到 tracing。

[dependencies]
tracing-log = "0.2"
// 初始化时启用 log 桥接
tracing_subscriber::registry()
    .with(EnvFilter::from_default_env())
    .with(tracing_subscriber::fmt::layer())
    .init();

// tracing-subscriber 默认已内置 log 桥接
// 或手动初始化:
tracing_log::LogTracer::init().unwrap();

之后所有 log::info!log::error! 等调用都会路由到 tracing。


11. 输出到文件

滚动日志文件

[dependencies]
tracing-appender = "0.2"
use tracing_appender::{non_blocking, rolling};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

fn main() {
    // 按天滚动,写入 /var/log/myapp/app.log.2026-05-14
    let file_appender = rolling::daily("/var/log/myapp", "app.log");

    // non_blocking 避免 I/O 阻塞主线程(必须保持 _guard 存活)
    let (non_blocking_writer, _guard) = non_blocking(file_appender);

    tracing_subscriber::registry()
        .with(EnvFilter::from_default_env())
        // 文件层(不带颜色)
        .with(
            tracing_subscriber::fmt::layer()
                .with_writer(non_blocking_writer)
                .with_ansi(false),
        )
        // 控制台层(带颜色)
        .with(tracing_subscriber::fmt::layer())
        .init();

    // _guard 在 main 退出前不能 drop,否则日志会丢失
    tracing::info!("程序启动");

    // ...
}

滚动策略

// 按小时滚动
rolling::hourly("logs/", "app.log");

// 按天滚动
rolling::daily("logs/", "app.log");

// 按分钟滚动
rolling::minutely("logs/", "app.log");

// 不滚动(单文件)
rolling::never("logs/", "app.log");

12. JSON 格式输出

JSON 格式便于日志收集系统(ELK、Loki 等)解析。

[dependencies]
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

tracing_subscriber::registry()
    .with(EnvFilter::from_default_env())
    .with(
        tracing_subscriber::fmt::layer()
            .json()                         // 启用 JSON 格式
            .with_current_span(true)        // 包含当前 span 信息
            .with_span_list(true)           // 包含 span 链路
    )
    .init();

输出示例:

{
  "timestamp": "2026-05-14T10:00:00.000Z",
  "level": "INFO",
  "target": "my_app::handlers",
  "span": { "name": "handle_request", "request_id": "req-123" },
  "fields": { "message": "请求完成", "status": 200, "latency_ms": 42 }
}

13. OpenTelemetry 集成

Cargo.toml

[dependencies]
tracing-opentelemetry    = "0.27"
opentelemetry            = "0.27"
opentelemetry_sdk        = { version = "0.27", features = ["rt-tokio"] }
opentelemetry-otlp       = { version = "0.27", features = ["tonic"] }

初始化(发送到 OTLP 收集器)

use opentelemetry::trace::TracerProvider;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::runtime;
use tracing_opentelemetry::OpenTelemetryLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

async fn init_tracing() -> opentelemetry_sdk::trace::TracerProvider {
    let exporter = opentelemetry_otlp::SpanExporter::builder()
        .with_tonic()
        .with_endpoint("http://localhost:4317")
        .build()
        .unwrap();

    let provider = opentelemetry_sdk::trace::TracerProvider::builder()
        .with_batch_exporter(exporter, runtime::Tokio)
        .build();

    let tracer = provider.tracer("my_app");

    tracing_subscriber::registry()
        .with(EnvFilter::from_default_env())
        .with(tracing_subscriber::fmt::layer())
        .with(OpenTelemetryLayer::new(tracer))
        .init();

    provider
}

#[tokio::main]
async fn main() {
    let provider = init_tracing().await;

    run_app().await;

    // 程序退出前刷新缓冲区
    provider.shutdown().unwrap();
}

14. 实战:Tokio + Tonic 全链路追踪

完整初始化

use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
use tracing_appender::{non_blocking, rolling};

pub fn init_tracing() -> impl Drop {
    let file_appender = rolling::daily("logs", "app.log");
    let (file_writer, guard) = non_blocking(file_appender);

    tracing_subscriber::registry()
        .with(
            EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| EnvFilter::new("info")),
        )
        // 控制台:人类可读格式
        .with(
            tracing_subscriber::fmt::layer()
                .with_target(true)
                .with_level(true),
        )
        // 文件:JSON 格式供日志系统收集
        .with(
            tracing_subscriber::fmt::layer()
                .json()
                .with_writer(file_writer)
                .with_ansi(false),
        )
        .init();

    guard  // 必须保持存活
}

gRPC 服务中使用

use tonic::{Request, Response, Status};
use tracing::{info, instrument, warn};

pub struct MyService;

#[tonic::async_trait]
impl MyProto for MyService {
    #[instrument(
        skip(self, request),
        fields(
            rpc = "CreateUser",
            request_id = tracing::field::Empty,
        ),
        err
    )]
    async fn create_user(
        &self,
        request: Request<CreateUserRequest>,
    ) -> Result<Response<User>, Status> {
        // 从 metadata 提取 request_id 并记录到 span
        if let Some(id) = request.metadata().get("x-request-id") {
            tracing::Span::current().record(
                "request_id",
                id.to_str().unwrap_or("invalid"),
            );
        }

        let req = request.into_inner();
        info!(username = %req.username, "创建用户");

        if req.username.is_empty() {
            warn!("用户名为空,拒绝请求");
            return Err(Status::invalid_argument("username 不能为空"));
        }

        // 数据库操作自动继承 span 上下文
        let user = save_to_db(&req).await?;

        info!(user_id = user.id, "用户创建成功");
        Ok(Response::new(user))
    }
}

#[instrument(fields(sql = "INSERT INTO users"))]
async fn save_to_db(req: &CreateUserRequest) -> Result<User, Status> {
    debug!("执行数据库写入");
    // ...
}

HTTP 中间件(axum)

use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer};
use tracing::Level;

let app = axum::Router::new()
    .route("/", axum::routing::get(handler))
    .layer(
        TraceLayer::new_for_http()
            .make_span_with(
                DefaultMakeSpan::new()
                    .level(Level::INFO)
                    .include_headers(true),
            )
            .on_response(
                DefaultOnResponse::new()
                    .level(Level::INFO)
                    .latency_unit(tower_http::LatencyUnit::Millis),
            ),
    );

15. 常见陷阱

陷阱 1:异步代码中 enter() 跨 await

//  错误:MutexGuard/Guard 跨 await 会 panic(debug 模式)
async fn bad() {
    let span = info_span!("task");
    let _guard = span.enter();
    tokio::time::sleep(Duration::from_secs(1)).await; // panic!
}

// ✓ 正确:用 .instrument() 或 #[instrument]
async fn good() {
    async {
        info!("在 span 内");
        tokio::time::sleep(Duration::from_secs(1)).await;
    }
    .instrument(info_span!("task"))
    .await;
}

陷阱 2:non_blocking 的 guard 提前 drop

fn main() {
    let (writer, _guard) = non_blocking(file_appender);
    //             ^^^^^^ 匿名变量!立即 drop,日志全部丢失

    // ✓ 正确:绑定命名变量
    let (writer, _file_guard) = non_blocking(file_appender);

    // ... 程序运行 ...
    // _file_guard 在 main 结束时才 drop,确保日志刷盘
}

隐患 3:忘记调用 init()

//  只配置不初始化,什么都不会输出
tracing_subscriber::fmt::Subscriber::builder()
    .with_max_level(Level::DEBUG)
    .build(); // 没有 .init()!

// ✓ 正确
tracing_subscriber::fmt::init();
// 或
tracing_subscriber::fmt::Subscriber::builder()
    .with_max_level(Level::DEBUG)
    .init(); // 必须调用

陷阱 4:#[instrument] 在 impl 块中需要 Self: Debug

// 如果 Self 没有实现 Debug,默认会包含 self 字段
#[instrument]
async fn handler(&self, req: Request) {}

// ✓ 跳过 self
#[instrument(skip(self))]
async fn handler(&self, req: Request) {}

陷阱 5:全局 Subscriber 只能设置一次

// set_global_default 第二次调用会返回错误
// 测试中每个测试都调用 init() 会 panic

// 测试中使用局部 Subscriber
#[test]
fn test_something() {
    let subscriber = tracing_subscriber::fmt().finish();
    let _guard = tracing::subscriber::set_default(subscriber);
    // 测试代码
}

快速参考

需求 API
记录一条日志 info!("msg") / error!("msg")
带字段的日志 info!(key = val, "msg")
给函数加 span #[instrument]
给异步 Future 加 span future.instrument(span)
动态填充 span 字段 span.record("key", value)
控制台输出初始化 tracing_subscriber::fmt::init()
环境变量控制级别 RUST_LOG=debug
写入滚动日志文件 tracing_appender::rolling::daily(...)
JSON 格式输出 .json() on fmt::layer()
多层输出(控制台+文件) registry().with(layer1).with(layer2).init()
桥接旧 log crate tracing_subscriber 默认已内置