Rust 后端可观测性:用 Tracing 让服务不再“黑盒”

举报
yd_232564628 发表于 2026/10/24 11:36:42 2026/10/24
【摘要】 前几篇我们把 Rust 后端从内存服务做到了持久化、认证和权限控制。但真正部署上线后,还有一个很容易被忽视的问题:服务出问题时,你能多快定位?日志、链路追踪、指标,这些听起来像运维的事,但开发阶段就应该考虑。Rust 生态里最成熟的方案是 tracing:它不只是“打印日志”,而是把事件、上下文、异步任务链路串在一起,让异步服务也能被看清楚。 为什么 Rust 后端需要 tracing?Ru...

前几篇我们把 Rust 后端从内存服务做到了持久化、认证和权限控制。但真正部署上线后,还有一个很容易被忽视的问题:服务出问题时,你能多快定位?

日志、链路追踪、指标,这些听起来像运维的事,但开发阶段就应该考虑。Rust 生态里最成熟的方案是 tracing:它不只是“打印日志”,而是把事件、上下文、异步任务链路串在一起,让异步服务也能被看清楚。

为什么 Rust 后端需要 tracing?

Rust 异步服务通常是多任务并发的。一个请求进来,可能会经过路由、认证、数据库查询、外部 HTTP 调用、缓存命中判断等多个环节。如果只用 println! 或简单日志,很容易遇到这些问题:

  • 多个请求日志混在一起,分不清哪条属于哪个请求
  • 异步任务切换后,上下文丢失
  • 错误只打印了 Err(...),但不知道触发路径
  • 线上问题只能靠猜,缺少结构化信息

tracing 的优势在于:它天然适配异步 Rust,每个 Span 都可以携带上下文,请求 ID、用户 ID、数据库耗时、错误原因都能被结构化地记录下来。

安装依赖

在之前的 todo_service 里新增:

cargo add tracing tracing-subscriber tracing-appender
cargo add tower-http --features trace

tracing 负责事件和 Span,tracing-subscriber 负责输出格式,tracing-appender 支持日志文件滚动,tower-http 可以把 HTTP 请求日志自动接入 tracing。

第一步:初始化 Subscriber

在 main 函数里初始化日志系统:

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

fn init_tracing() {
    let fmt_layer = tracing_subscriber::fmt::layer()
        .with_target(true)
        .with_thread_ids(true)
        .with_file(true)
        .with_line_number(true);

    tracing_subscriber::registry()
        .with(fmt_layer)
        .init();
}

然后在 main 开头调用:

#[tokio::main]
async fn main() {
    init_tracing();

    // 初始化数据库、路由、启动服务
}

这样所有 tracing::info!、tracing::warn!、tracing::error! 都会被正确输出。

第二步:记录结构化日志

不要只写:

println!("login failed");

可以改成:

tracing::info!(
    username = %payload.username,
    ip = %client_ip,
    "user login attempt"
);

登录失败时:

tracing::warn!(
    username = %payload.username,
    reason = "invalid password",
    "login failed"
);

这样日志不再是纯文本,而是带有字段的结构化事件。后续如果接入 JSON 输出、日志平台或告警系统,会方便很多。

第三步:用 Span 包裹请求

tracing 最强大的地方是 Span。它可以把一组相关事件归到同一个上下文中。

use tracing::Instrument;

async fn list_todos(
    State(state): State<AppState>,
    auth: AuthUser,
) -> Result<Json<Vec<Todo>>, AppError> {
    let span = tracing::info_span!(
        "list_todos",
        user_id = auth.user_id
    );

    list_todos_inner(state, auth)
        .instrument(span)
        .await
}

在内部函数里,你可以继续记录子事件:

async fn list_todos_inner(
    state: AppState,
    auth: AuthUser,
) -> Result<Json<Vec<Todo>>, AppError> {
    tracing::debug!("querying todos from database");

    let todos = sqlx::query_as!(
        Todo,
        "SELECT id, title, completed FROM todos WHERE user_id = $1 ORDER BY id DESC",
        auth.user_id
    )
    .fetch_all(&*state.db)
    .await?;

    tracing::info!(count = todos.len(), "todos fetched");

    Ok(Json(todos))
}

这样日志输出时,querying todos 和 todos fetched 都会挂在同一个 list_todos Span 下。排查问题时,你能清楚看到一个请求经历了哪些步骤。

第四步:自动记录 HTTP 请求

手动打日志当然可以,但每个 handler 都写一遍会很累。tower-http 提供了现成的 Trace 层:

use tower_http::trace::TraceLayer;

let app = Router::new()
    .route("/register", post(register))
    .route("/login", post(login))
    .route("/todos", get(list_todos))
    .layer(TraceLayer::new_for_http())
    .with_state(state);

它会自动记录:

  • 请求方法、路径
  • 响应状态码
  • 请求耗时
  • 错误信息

如果想进一步记录请求 ID,可以结合 tower-http 的 RequestId 层,把 ID 注入 tracing 上下文。这样每个请求日志都会带上同一个 request_id,多行日志也能串起来。

第五步:日志分级要克制

tracing 支持不同级别:

  • trace:最细粒度,通常用于调试
  • debug:开发阶段查看流程
  • info:关键业务事件,比如登录、创建资源
  • warn:可疑情况,比如密码错误、参数异常
  • error:明确失败,比如数据库连接断开

生产环境通常只输出 info 及以上,避免日志过多。开发阶段可以开到 debug,方便看流程。

可以通过环境变量控制:

RUST_LOG=todo_service=debug,tower_http=info cargo run

第六步:日志落盘和滚动

线上服务不能只依赖控制台输出。可以用 tracing-appender 把日志写入文件:

use tracing_appender::rolling;

let file_appender = rolling::daily("./logs", "todo_service.log");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);

let file_layer = tracing_subscriber::fmt::layer()
    .with_writer(non_blocking)
    .with_ansi(false);

这样每天会生成一个新的日志文件,避免单个文件无限增长。_guard 需要保持存活,否则日志可能丢失。

真实项目里建议这样接

一个比较实用的落地顺序是:

  1. 先接 tracing-subscriber,把 println! 替换掉
  2. 给关键接口加上 info_span
  3. 接入 tower-http TraceLayer,自动记录 HTTP 请求
  4. 给登录、注册、创建资源等业务动作打 info 日志
  5. 给失败路径打 warn 或 error
  6. 日志落盘,并设置合理的滚动策略
  7. 后续再考虑接入 OpenTelemetry、Prometheus、Jaeger 等系统

Rust 后端开发到这一步,会明显感觉服务从“能跑”变成了“可维护”。尤其是异步服务,请求并发上来之后,没有结构化日志和 Span 上下文,排查问题会非常痛苦。tracing 的好处是它不要求你一开始就搭建复杂的监控系统,哪怕只是把日志打清楚,也能让调试和运维轻松很多。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。