日志与追踪
问题
Rust 应用中如何设计日志和分布式追踪方案?
答案
日志生态对比
| Crate | 特点 | 适用场景 |
|---|---|---|
log | 门面 crate,简单 | 小项目、库 |
tracing | 结构化、Span、异步友好 | 服务端应用(推荐) |
env_logger | log 的实现,按环境变量过滤 | CLI 工具 |
tracing-subscriber | tracing 的输出层 | 配合 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: 如何做到请求级别的日志关联?
答案:
- 在请求入口中间件创建 Span,附加
request_id字段 - 所有后续处理在此 Span 内执行(
.instrument(span)) - 子函数的
#[instrument]自动成为子 Span - 日志输出自带 Span 层级,可按
request_id搜索完整链路