跳到主要内容

日志与追踪

问题

Rust 应用中如何设计日志和分布式追踪方案?

答案

日志生态对比

Crate特点适用场景
log门面 crate,简单小项目、库
tracing结构化、Span、异步友好服务端应用(推荐)
env_loggerlog 的实现,按环境变量过滤CLI 工具
tracing-subscribertracing 的输出层配合 tracing
slog结构化日志高性能场景

tracing 基础用法

use tracing::{info, warn, error, debug, instrument, Level};
use tracing_subscriber::{fmt, EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};

fn init_tracing() {
tracing_subscriber::registry()
// 按环境变量过滤日志级别:RUST_LOG=info,my_crate=debug
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
// JSON 格式输出(生产环境)
.with(fmt::layer().json())
.init();
}

// #[instrument] 自动创建 Span,记录函数入参
#[instrument(skip(pool))] // skip 跳过不可序列化的参数
async fn get_user(pool: &PgPool, user_id: i64) -> Result<User, Error> {
info!(user_id, "查询用户");

let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(user_id)
.fetch_optional(pool)
.await?;

match user {
Some(u) => {
debug!(name = %u.name, "找到用户");
Ok(u)
}
None => {
warn!(user_id, "用户不存在");
Err(Error::NotFound)
}
}
}

Span 嵌套与上下文传递

use tracing::{info_span, Instrument};

async fn handle_request(req: Request) -> Response {
// 创建请求级 Span,包含 request_id
let span = info_span!("handle_request",
request_id = %uuid::Uuid::new_v4(),
method = %req.method(),
path = %req.uri().path(),
);

async {
// 在 Span 内执行,所有子操作自动关联此 Span
let user = get_user(&pool, user_id).await?;
let orders = get_orders(&pool, user_id).await?;
info!(order_count = orders.len(), "请求处理完成");
Ok(build_response(user, orders))
}
.instrument(span) // 将 Span 附加到 Future 上
.await
}

分布式追踪(OpenTelemetry)

use opentelemetry::trace::TracerProvider;
use opentelemetry_otlp::WithExportConfig;
use tracing_opentelemetry::OpenTelemetryLayer;

fn init_otel_tracing() {
// 配置 OTLP exporter(发送到 Jaeger/Tempo 等)
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint("http://localhost:4317")
.build()
.unwrap();

let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.build();

let tracer = provider.tracer("my-service");

tracing_subscriber::registry()
.with(EnvFilter::new("info"))
.with(fmt::layer())
.with(OpenTelemetryLayer::new(tracer)) // 追踪数据同时发到 OTLP
.init();
}

日志最佳实践

实践说明
结构化字段info!(user_id = 42, "查询用户") 而非字符串拼接
JSON 格式生产环境用 JSON,方便 ELK 采集和查询
按级别过滤RUST_LOG=warn,my_app=debug
敏感信息脱敏不要打印密码、token
request_id每个请求分配唯一 ID,贯穿日志链路
异步安全使用 .instrument(span) 确保 Span 跨 await

常见面试问题

Q1: tracing 和 log 有什么区别?

答案

  • log 是传统日志门面,只有 level + message,无结构化字段、无 Span 概念
  • tracing 支持结构化字段Span 树(请求上下文)、原生异步支持
  • tracing 兼容 log(可以用 tracing-log 桥接)
  • 新项目推荐 tracing,它已成为 Rust 生态事实标准

Q2: 如何做到请求级别的日志关联?

答案

  1. 在请求入口中间件创建 Span,附加 request_id 字段
  2. 所有后续处理在此 Span 内执行(.instrument(span)
  3. 子函数的 #[instrument] 自动成为子 Span
  4. 日志输出自带 Span 层级,可按 request_id 搜索完整链路

相关链接