15.4 异步日志

在异步 Web 服务中,传统的日志方式难以追踪跨任务的请求上下文。Rust 的 tracing 生态提供了一套结构化、高性能且支持异步上下文传播的日志与诊断框架。结合 tracing-subscriber,我们可以轻松实现带请求 ID、层级 span 和结构化字段的日志输出,极大提升调试和监控能力。

核心概念:Event 与 Span

  • Event:表示一个瞬时事件,如“任务创建成功”;
  • Span:表示一段有持续时间的操作,如“处理 /tasks POST 请求”,可嵌套并携带上下文。

在异步环境中,tracing 能自动将 span 与 tokio 任务关联,确保即使多个请求并发执行,日志仍能正确归属。

添加依赖

Cargo.toml 中添加:

[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2"  # 可选:用于文件输出

初始化全局订阅器

src/main.rsmain 函数开头初始化日志系统:

use tracing_subscriber::{EnvFilter, fmt};

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

    fmt::Subscriber::builder()
        .with_env_filter(filter)
        .with_line_number(true)
        .with_file(true)
        .init();
}

此配置:

  • 从环境变量 RUST_LOG 读取日志级别(如 RUST_LOG=debug);
  • 默认级别为 info
  • 输出包含文件名和行号,便于定位。

调用:

#[tokio::main]
async fn main() {
    init_tracing();
    tracing::info!("Starting server on port 3000");
    // ... 启动服务
}

在 Handler 中记录日志

使用 #[instrument] 属性宏自动为函数创建 span:

use tracing::instrument;

#[instrument(skip(service), fields(task.title = %payload.title))]
pub async fn create_task(
    State(service): State<TaskService>,
    Json(payload): Json<CreateTask>,
) -> Result<Json<Task>, AppError> {
    tracing::debug!("Validating input");
    let task = service.create_task(payload).await?;
    tracing::info!(task.id = task.id, "Task created successfully");
    Ok(Json(task))
}
  • skip(service) 避免尝试格式化不可显示的服务对象;
  • fields(...) 在 span 创建时注入静态字段;
  • tracing::info! 等宏记录事件,并自动关联当前 span。

跨异步边界的上下文传播

由于 tracingtokio 深度集成,即使 handler 内部调用多个异步函数,span 上下文也会自动传递:

#[instrument]
async fn save_to_db(task: CreateTask, pool: &PgPool) -> Result<Task, SqlxError> {
    tracing::trace!("Executing INSERT query");
    // SQLx 查询...
}

所有日志将嵌套在 create_task 的 span 下,形成清晰的调用树。

结构化日志输出

tracing 默认输出为人类可读格式,也可配置为 JSON(便于日志收集系统):

fmt::Subscriber::builder()
    .json()  // 启用 JSON 格式
    .with_env_filter(filter)
    .init();

示例 JSON 日志:

{
  "timestamp": "2025-06-15T10:23:45Z",
  "level": "INFO",
  "fields": {
    "message": "Task created successfully",
    "task.id": 42
  },
  "target": "task_api::handlers::tasks"
}

高级用法:自定义中间件记录请求日志

可在 Axum 中间件中为每个请求创建顶层 span:

use axum::{
    body::Body,
    http::{Request, StatusCode},
    response::Response,
    middleware::Next,
};
use tracing::info_span;
use tower_http::trace::TraceLayer;

// 使用 tower_http 的 TraceLayer(基于 tracing)
let app = Router::new()
    .route("/tasks", post(create_task))
    .layer(
        TraceLayer::new_for_http()
            .make_span_with(|req: &Request<Body>| {
                info_span!(
                    "http_request",
                    method = %req.method(),
                    uri = %req.uri(),
                    version = ?req.version()
                )
            })
    );

这会为每个 HTTP 请求生成一个 span,并记录方法、URI 等元数据。

小结

通过 tracingtracing-subscriber,我们为 Web 服务引入了现代化的可观测性基础。结构化日志不仅便于本地调试,也为生产环境的监控、告警和分布式追踪(结合 OpenTelemetry)铺平道路。合理使用 span 和 event,能让复杂异步系统的执行路径一目了然,显著降低运维成本。

#Rust 入门教程 分享于 5 天前

内容由 AI 创作和分享,仅供参考