一、从println!开始的日志之旅

刚开始接触Rust时,我们最熟悉的日志方式莫过于println!宏了。简单粗暴,直接输出到控制台,调试时特别方便。比如下面这段代码:

fn main() {
    let name = "张三";
    let age = 25;
    println!("用户信息 - 姓名: {}, 年龄: {}", name, age); // 直接打印到标准输出
}

这种方式在小项目或快速原型开发中很实用,但随着项目规模扩大,问题就暴露出来了:

  1. 无法区分日志级别(如调试、错误等)
  2. 缺乏结构化输出,难以用工具分析
  3. 性能较差(每次调用都会立即刷新IO)

二、引入log crate标准化日志

Rust生态中的log crate提供了日志抽象层。它定义了error!warn!info!debug!trace!五个级别,配合不同的日志实现后端(如env_logger)就能实现基础分级日志:

use log::{info, error};

fn process_data(input: &str) -> Result<(), String> {
    info!("开始处理数据: {}", input); // 信息级别日志
    if input.is_empty() {
        error!("输入数据为空!");      // 错误级别日志
        return Err("空输入".into());
    }
    Ok(())
}

fn main() {
    env_logger::init(); // 初始化env_logger后端
    let _ = process_data("测试数据");
}

通过设置环境变量控制日志级别:

RUST_LOG=info cargo run

优点

  • 统一的日志接口
  • 可配置的日志级别过滤
  • 支持多种后端实现

不足

  • 仍缺乏结构化字段
  • 需要手动选择后端实现

三、结构化日志实战

slogtracing是Rust中两个主流的结构化日志方案。我们以tracing为例展示如何记录带上下文的日志:

use tracing::{info_span, instrument, Level};
use tracing_subscriber::fmt::format::FmtSpan;

#[instrument] // 自动记录函数参数和耗时
fn process_order(order_id: u64, amount: f64) {
    let span = info_span!("processing_order", order_id, amount); // 创建带字段的span
    let _enter = span.enter();
    
    tracing::event!(Level::INFO, "订单处理中"); // 结构化事件
    // ...业务逻辑
}

fn main() {
    // 初始化tracing订阅者
    tracing_subscriber::fmt()
        .with_span_events(FmtSpan::CLOSE) // 记录span生命周期
        .init();

    process_order(12345, 99.99);
}

执行后会输出类似JSON的结构:

{
  "timestamp": "2023-08-20T12:00:00Z",
  "level": "INFO",
  "fields": {
    "message": "订单处理中",
    "order_id": 12345,
    "amount": 99.99,
    "target": "my_app"
  }
}

关键技术点

  1. Span上下文跟踪
  2. 自动字段注入
  3. 与异步运行时(如tokio)深度集成

四、生产环境日志方案

完整的日志系统需要考虑:

4.1 日志收集架构

use tracing_appender::{non_blocking, rolling};
use tracing_subscriber::{layer::SubscriberExt, Registry};
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};

fn setup_logging() {
    // 1. 按天轮转的日志文件
    let file_appender = rolling::daily("/var/log", "app.log");
    let (non_blocking_writer, _guard) = non_blocking(file_appender);
    
    // 2. 格式化层
    let formatting_layer = BunyanFormattingLayer::new("app".into(), non_blocking_writer);
    
    // 3. 注册全局订阅者
    let subscriber = Registry::default()
        .with(JsonStorageLayer)
        .with(formatting_layer);
    
    tracing::subscriber::set_global_default(subscriber).unwrap();
}

4.2 性能优化技巧

  • 使用非阻塞I/O(如上面的non_blocking
  • 采样控制日志量:
use tracing_subscriber::filter::{EnvFilter, LevelFilter};

EnvFilter::builder()
    .with_default_directive(LevelFilter::INFO.into())
    .parse("app=debug,hyper=warn") // 模块级控制

4.3 与监控系统集成

通过OpenTelemetry将日志与指标、追踪关联:

use opentelemetry::global;
use tracing_opentelemetry::OpenTelemetryLayer;

let tracer = opentelemetry_jaeger::new_pipeline()
    .with_service_name("app")
    .install_batch(opentelemetry::runtime::Tokio)?;

let telemetry_layer = OpenTelemetryLayer::new(tracer);
tracing_subscriber::registry()
    .with(telemetry_layer)
    .init();

五、技术选型指南

方案 适用场景 性能 学习曲线
println! 快速调试/小型脚本
log + env_logger 传统应用日志
tracing 异步系统/需要上下文追踪
slog 复杂结构化日志需求

注意事项

  1. 避免在热点路径记录高频日志
  2. 生产环境务必设置日志轮转策略
  3. 敏感信息需要脱敏处理

六、演进路线总结

println!到结构化日志的演进,反映了软件开发从"能运行"到"易维护"的成熟过程。现代Rust日志系统已经能够:

  • 通过tracing实现请求级上下文跟踪
  • 使用OpenTelemetry实现可观测性三支柱统一
  • 借助异步IO达到百万级日志/秒的处理能力

未来随着tracing生态的完善,Rust在分布式系统日志领域将展现更大优势。