# Rust tracing_subscriber 生产环境最佳实践指南


# Rust tracing_subscriber 生产环境最佳实践指南

这是一份基于 **Registry + Layer** 模式的现代 `tracing_subscriber` 使用指南。这种模式是目前 Rust 生产环境的最佳实践，它允许你将日志"分流"：**给人看的（控制台）**和**给机器看的（JSON/文件）**分开处理。

## 为什么选择这种模式？

在开发环境中，我们需要清晰易读的彩色日志来快速定位问题；而在生产环境中，我们需要结构化的 JSON 日志以便于日志收集系统（如 ELK、Datadog）进行聚合分析。Registry + Layer 模式完美解决了这个需求。

---

## 1. 依赖配置 (Cargo.toml)

首先，你需要引入必要的 crate 和 feature。

```toml
[dependencies]
# 核心库
tracing = "0.1"

# 订阅者实现
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "local-time"] }

# 文件输出 (非阻塞写入，生产环境必备)
tracing-appender = "0.2"

# 时间处理 (可选，用于自定义时间格式)
time = { version = "0.3", features = ["formatting", "macros"] }
```

**Feature 说明：**
- `env-filter`: 支持通过环境变量控制日志级别
- `json`: 支持 JSON 格式输出
- `local-time`: 使用本地时区而非 UTC

---

## 2. 终极配置代码：控制台(Pretty) + 文件(JSON)

这段代码展示了如何构建一个生产级可用的日志系统：

### 配置要点

1.  **控制台**：输出漂亮的、带颜色的文本日志，方便开发调试。
2.  **文件**：输出结构化的 JSON 日志，按天轮转，非阻塞写入，方便 ELK/Datadog 收集。
3.  **非阻塞写入**：使用 `non_blocking` 包装器，避免文件 I/O 阻塞主线程（特别是 Tokio 运行时）。

### 完整实现代码

```rust
use tracing::{info, warn, error, instrument};
use tracing_subscriber::{registry, prelude::*, fmt, EnvFilter};
use std::path::Path;

fn main() {
    // ============================================================
    // 1. 配置非阻塞文件写入器 (Non-blocking File Appender)
    // ============================================================
    // 每天轮转一个新文件，文件名如: app.log.2024-01-01
    let file_appender = tracing_appender::rolling::daily("./logs", "app.log");

    // 关键：使用 non_blocking 包装，避免文件 I/O 阻塞主线程（特别是 Tokio 运行时）
    // _guard 必须持有到程序结束，否则日志可能丢失
    let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);

    // ============================================================
    // 2. 定义 JSON 文件层 (给机器看)
    // ============================================================
    let json_file_layer = fmt::layer()
        .json()                       // 开启 JSON 格式
        .with_writer(non_blocking)    // 写入到文件
        .with_file(true)              // 记录文件名
        .with_line_number(true)       // 记录行号
        .with_thread_ids(true)        // 记录线程 ID
        .with_target(false)           // JSON 中通常不需要 target (模块路径)，因为有 file
        .flatten_event(true)          // 建议：将字段平铺在根对象中，而不是嵌套在 "fields" 对象里
        .with_filter(EnvFilter::new("info")); // 文件日志级别：通常是 INFO 或 WARN

    // ============================================================
    // 3. 定义控制台层 (给人看)
    // ============================================================
    let console_layer = fmt::layer()
        .pretty()                     // 漂亮的格式 (多行，带颜色)
        .with_writer(std::io::stdout) // 写入标准输出
        .with_target(false)           // 保持清爽，隐藏详细模块路径
        .with_filter(EnvFilter::from_default_env()); // 读取 RUST_LOG 环境变量

    // ============================================================
    // 4. 注册所有层
    // ============================================================
    registry()
        .with(json_file_layer)
        .with(console_layer)
        .init();

    // ============================================================
    // 5. 测试日志
    // ============================================================
    perform_task("user_123", 500);
}

// 使用 instrument 自动记录函数进入/退出和参数
#[instrument(fields(request_id = "req-abc-123"))]
fn perform_task(user: &str, amount: u32) {
    info!(action = "payment", status = "processing", "开始处理支付任务");

    // 模拟一些工作
    std::thread::sleep(std::time::Duration::from_millis(100));

    if amount > 1000 {
        warn!(amount, "金额较大，需要人工审核");
    }

    error!(error_code = 5001, "连接数据库失败");
}
```

### 关键注意事项

⚠️ **关于 `_guard` 变量**：
- `_guard` 变量必须被持有到程序结束，否则日志可能丢失
- 在实际应用中，通常将其存储在结构体字段中或使用 `std::mem::forget`
- 如果在 Tokio 运行时中使用，确保 guard 的生命周期正确管理

---

## 3. JSON 输出详解

在上面的配置中，`json_file_layer` 产生的结果会像这样（为了方便阅读，我格式化了，实际是一行）：

```json
{
  "timestamp": "2024-01-07T10:00:00.123456Z",
  "level": "INFO",
  "message": "开始处理支付任务",
  "user": "user_123",
  "amount": 500,
  "request_id": "req-abc-123",
  "action": "payment",
  "status": "processing",
  "file": "src/main.rs",
  "line": 55,
  "thread_id": 1
}
```

### 字段来源说明

| 字段 | 来源 | 说明 |
|------|------|------|
| `user`, `amount` | 函数参数 | 由 `#[instrument]` 自动捕获 |
| `request_id` | `#[instrument(fields(...))]` | 自定义字段 |
| `action`, `status` | `info!` 宏 | 日志事件中的键值对 |
| `file`, `line` | `with_file(true)` | 代码位置信息 |
| `thread_id` | `with_thread_ids(true)` | 线程标识 |

### 关键配置项说明

**`.json()`**: 启用 JSON 格式化器。

**`.flatten_event(true)`**: **强烈推荐**
- `false` (默认): 自定义字段会被放在 `fields` 对象里：`{"message": "...", "fields": {"key": "value"}}`
- `true`: 字段直接在根层级：`{"message": "...", "key": "value"}`。这让日志查询（如在 Kibana 中）更容易。

**`.with_current_span(false)` / `.with_span_list(false)`**: 如果你发现 JSON 日志太长（包含太多层级的 span 信息），可以关闭这些选项来精简日志。

### 日志级别控制

通过 `EnvFilter` 可以灵活控制日志级别：

```rust
// 方式1: 固定级别
.with_filter(EnvFilter::new("info"))

// 方式2: 从环境变量读取 (推荐)
.with_filter(EnvFilter::from_default_env())
// 使用: RUST_LOG=info,my_crate=debug ./app

// 方式3: 复杂过滤
.with_filter(EnvFilter::new("my_crate=debug,tokio=warn"))
```

---

## 4. 如何正确打日志 (结构化)

使用 `tracing` 时，**不要**像使用 `println!` 那样拼接字符串。要使用**键值对**。

### ❌ 错误示例 (旧习惯)

```rust
info!("用户 {} 登录成功，IP: {}", username, ip);
```

**问题：**
- `message` 字段是一串长文本，难以索引搜索
- 无法对特定字段进行聚合分析
- 日志查询效率低

### ✅ 正确示例 (结构化)

```rust
info!(user = username, ip_address = ip, "用户登录成功");
```

**优势：**
- JSON 中会有独立的 `user` 和 `ip_address` 字段
- 可以轻松查询特定用户的操作
- 支持按 IP 地址聚合分析
- 日志查询效率高

### ✅ 使用 `#[instrument]` 宏

这个宏非常强大，它会自动创建一个 Span，并将函数的参数自动作为字段记录下来。

```rust
#[instrument(skip(data))] // skip 表示不记录 data 参数的内容
fn process(id: usize, data: Vec<u8>) {
    info!("开始处理数据");
    // ...
}

// 调用时自动记录 id 参数
process(123, vec![1, 2, 3]);
```

**`#[instrument]` 的常用选项：**

- `skip(param)`: 跳过记录某个参数（如敏感数据）
- `fields(key = value)`: 添加自定义字段
- `level = Level::DEBUG`: 设置 span 的日志级别
- `target = "custom_target"`: 自定义目标模块

### 实际应用示例

```rust
#[instrument(
    skip(password),  // 跳过敏感字段
    fields(
        user_id = %user.id,
        user_role = %user.role
    )
)]
async fn login(user: &User, password: &str) -> Result<Session, AuthError> {
    info!("用户尝试登录");

    match authenticate(user, password).await {
        Ok(session) => {
            info!(session_id = %session.id, "登录成功");
            Ok(session)
        }
        Err(e) => {
            error!(error = %e, "登录失败");
            Err(e)
        }
    }
}
```

---

## 5. 最佳实践总结

### 开发环境 vs 生产环境

| 场景 | 输出目标 | 格式 | 日志级别 | 用途 |
|------|---------|------|---------|------|
| **开发环境** | 控制台 (stdout) | Pretty (彩色多行) | DEBUG | 快速定位问题 |
| **生产环境** | 文件 (JSON) | JSON (单行) | INFO/WARN | 日志收集分析 |

### 性能优化建议

1. **务必使用 `non_blocking`**
   - 高并发下，阻塞式文件 I/O 会严重影响性能
   - 使用 `tracing_appender::non_blocking` 包装文件写入器

2. **合理设置日志级别**
   - 开发环境: `DEBUG`
   - 生产环境: `INFO` 或 `WARN`
   - 通过环境变量动态控制: `RUST_LOG=info,my_crate=debug`

3. **避免过度记录**
   - 使用 `skip()` 跳过敏感数据或大对象
   - 关闭不必要的 span 信息: `with_current_span(false)`

### 格式规范

坚持使用结构化日志 (`key=value`)，充分利用 JSON 的优势：

```rust
// ✅ 推荐
info!(user_id = 123, action = "login", status = "success", "用户登录成功");

// ❌ 不推荐
info!("用户 123 登录成功");
```

### 常见问题

**Q: 日志丢失了怎么办？**
A: 确保 `_guard` 变量被正确持有到程序结束。在 Tokio 应用中，可以使用 `std::mem::forget` 或将其存储在结构体中。

**Q: 如何在异步代码中使用？**
A: `tracing` 完全兼容异步代码，`#[instrument]` 可以用于 `async fn`，span 会正确地跨 await 点传播。

**Q: 如何集成 OpenTelemetry？**
A: `tracing-opentelemetry` 提供了完整的集成，可以将 tracing span 转换为 OpenTelemetry spans。

## 参考资源

- [tracing 官方文档](https://docs.rs/tracing/)
- [tracing-subscriber 官方文档](https://docs.rs/tracing-subscriber/)
- [tracing-appender 官方文档](https://docs.rs/tracing-appender/)

