Rust日志库tracing的使用

Rust日志库tracing的使用

tracing

1. 简介

tracing 是 Rust 官方推荐的 结构化、异步友善的日志与性能跟踪框架。
它由 Tokio 团队维护,是传统 log crate 的现取代代品。

2. 核心理念:事件 + span(范围)

  • 事件(event):相当于传统的日志行(info!、warn!、error!)。
  • span(范围):表明一段逻辑代码的生命周期(列如一次函数调用、一次请求处理、一次任务执行)。
    • • 可以嵌套
    • • 可以记录进入、退出时间
    • • 可以附加字段(key-value)

3. 常用宏

use anyhow::Result;
use tracing::{debug, info, span, Level};

#[tokio::main]
async fn main() -> Result<()> {
    info!("Application starting...");
    {
        let span = span!(Level::INFO, "process_data", user_id = 1000);
        let _enter = span.enter();
        info!("inside span");
    }
    debug!("this is debug log.");
    info!("Application shut down cleanly.");
    Ok(())
}

这些宏不会直接输出日志,而是发出 tracing 事件(Event)或范围(Span)。
实际的“打印/输出/存储”行为是由订阅者(subscriber)决定的。

tracing-subscriber:订阅者(负责「消费日志事件」)

1. 简介

tracing-subscriber 是 实现 Subscriber trait 的官方库,用于接收和处理由 tracing 产生的事件。
它决定:

  • • 日志如何被格式化(JSON、文本等)
  • • 日志如何被过滤(级别、目标等)
  • • 日志输出到哪里(控制台、文件、网络、数据库等)

你可以理解为:

tracing 负责“发出信号”,
tracing-subscriber 负责“接收并输出这些信号”。

2. 常用组件

  • FmtSubscriber:最常见的实现,打印到 stdout/stderr。
  • EnvFilter:基于环境变量或代码过滤日志。
  • Registry:注册全局订阅者,管理层次结构。

3. 示例

在前面的代码中加入初始化tracing_subscriber的代码

tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::from_default_env())
        .init(); // 全局初始化

with_env_filter(
EnvFilter::from_default_env()) 可以读取 RUST_LOG 环境变量,例如 RUST_LOG=debug

运行
RUST_LOG=info cargo run
这时候才会输出日志

2025-10-23T13:52:29.323906Z  INFO rust_demo: Application starting...
2025-10-23T13:52:29.324257Z  INFO process_data{user_id=1000}: rust_demo: inside span
2025-10-23T13:52:29.324350Z  INFO rust_demo: Application shut down cleanly.

4. Registry

前面的全局初始化日志时实现了FmtSubscriber 和 EnvFilter,但FmtSubscriber也可以作为 Layer 与其他 Layer(如过滤层)组合使用,提供更灵活的配置

use anyhow::Result;
use tracing::{debug, info, span, Level};
use tracing_subscriber::{fmt, prelude::*, registry, EnvFilter};

#[tokio::main]
async fn main() -> Result<()> {
    let fmt_layer = fmt::layer()
        .pretty() // 使用美丽格式
        .with_target(false); // 不显示 target
    let filter_layer = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?;

    registry()
        .with(filter_layer) // 先过滤
        .with(fmt_layer) // 再格式化输出
        .init();

    info!("Application starting...");
    {
        let span = span!(Level::INFO, "process_data", user_id = 1000);
        let _enter = span.enter();
        info!("inside span");
    }
    debug!("this is debug log.");
    info!("Application shut down cleanly.");
    Ok(())
}

运行
cargo run

  2025-10-23T14:04:11.770304Z  INFO  Application starting...
    at src/main.rs:23

  2025-10-23T14:04:11.770611Z  INFO  inside span
    at src/main.rs:27
    in process_data with user_id: 1000

  2025-10-23T14:04:11.770682Z  INFO  Application shut down cleanly.
    at src/main.rs:30

tracing-appender:日志输出到文件(负责「持久化日志」)

1. 简介

tracing-appender 提供了用于将日志写入 文件或滚动文件 的组件。

  • • 它不是单独的日志系统,而是 tracing-subscriber 的「输出后端」。
  • • 解决日志落地(写文件、切割文件)的问题。

2. 常用功能

  • 非滚动文件(non-rolling)
  • 按日期滚动(rolling daily)
  • 按小时滚动(rolling hourly)

3. 结合三者使用

use anyhow::Result;
use tracing::{debug, info, span, Level};
use tracing_appender::rolling;
use tracing_subscriber::{fmt, prelude::*, registry, EnvFilter};

#[tokio::main]
async fn main() -> Result<()> {
    // 每天滚动日志
    let file_appender = rolling::daily("logs", "app.log");
    let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);

    let file_layer = fmt::layer()
        .with_writer(non_blocking)
        .pretty() // 使用美丽格式
        .with_target(false); // 不显示 target
    let filter_layer = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?;

    // 创建一个 fmt 层用于控制台输出
    let stdout_layer = fmt::layer()
        .with_ansi(true) // 控制台输出可以有颜色
        .with_thread_ids(true)
        .with_thread_names(true)
        .with_line_number(true)
        .with_file(true)
        .with_level(true);

    registry()
        .with(filter_layer) // 先过滤
        .with(file_layer) // 输出文件
        .with(stdout_layer) // 输出控制台
        .init();

    info!("Application starting...");
    {
        let span = span!(Level::INFO, "process_data", user_id = 1000);
        let _enter = span.enter();
        info!("inside span");
    }
    debug!("this is debug log.");
    info!("Application shut down cleanly.");
    Ok(())
}

3. 结合三者使用

当fmt::layer()设置写入器是non_blocking时就是作为文件输出了,如果还想要控制台输出,则加上stdout_layer;这时候相对目录logs下会生成app.log.2025-10-23文件。

tracing-appender日志轮转的bug,不能按照本地时区轮转

[这里是tracing-appender该问题的issue](
https://github.com/tokio-rs/tracing/pull/3255)

1. 解决方法一

自定义实现MakeWriter trait来实现本地时区日志轮转

use anyhow::{Context, Result};
use chrono::{Datelike, Local};
use std::sync::atomic::{AtomicU32, Ordering};
use std::{
    fs::{self, File, OpenOptions},
    io::{self},
    path::{Path, PathBuf},
    sync::{Arc, Mutex},
};
use tracing_subscriber::fmt::format::Writer;
use tracing_subscriber::fmt::time::FormatTime;
use tracing_subscriber::{self, filter::EnvFilter, fmt, prelude::*, util::SubscriberInitExt};

// Customize the local time format
pub struct LocalTimer;

impl FormatTime for LocalTimer {
    fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result {
        write!(w, "{}", Local::now().format("%Y-%m-%d %H:%M:%S%.3f"))
    }
}

// =====================================================================
// Custom Local-Time Rolling Writer Implementation
// = ===================================================================

/// A writer that encapsulates `Arc<Mutex File>` implements `io: Write`.
struct LogFileWriter(Arc<Mutex<File>>);

impl io::Write for LogFileWriter {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.0.lock().unwrap().write(buf)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.0.lock().unwrap().flush()
    }
}

/// Implement the `MakeWriter` feature for custom file write and rotation logic.
struct LocalTimeRollingWriter {
    active_file: Arc<Mutex<File>>,
    last_rotation_day: AtomicU32, // Save the date (days) of the last rotation
    log_dir: PathBuf,
}

impl LocalTimeRollingWriter {
    pub fn new(log_dir: &str) -> Result<Self> {
        let log_path = PathBuf::from(log_dir);
        fs::create_dir_all(&log_path)
            .context(format!("Failed to create log directory: {log_path:?}"))?;

        let now = Local::now();

        // At initialization, we still try to open it directly app.log
        let initial_file_path = log_path.join("app.log");
        // Try to deal with possible old app.log files before opening
        Self::archive_old_app_log_if_needed(&log_path, &now)?;

        let file = OpenOptions::new()
            .create(true)
            .append(true) // Append Mode
            .open(&initial_file_path)
            .context(format!(
                "Failed to open initial log file: {initial_file_path:?}"
            ))?;

        Ok(LocalTimeRollingWriter {
            active_file: Arc::new(Mutex::new(file)),
            last_rotation_day: AtomicU32::new(now.day()),
            log_dir: log_path,
        })
    }

    // Check and archive yesterday's app.log before opening app.log
    fn archive_old_app_log_if_needed(log_dir: &Path, now: &chrono::DateTime<Local>) -> Result<()> {
        let app_log_path = log_dir.join("app.log");

        println!("INFO: Checking for old app.log at {app_log_path:?}");

        if app_log_path.exists() {
            let metadata = fs::metadata(&app_log_path)
                .context(format!("Failed to read metadata for {app_log_path:?}"))?;
            let modified_time = metadata
                .modified()
                .context(format!("Failed to get modified time for {app_log_path:?}"))?;

            let local_modified_time: chrono::DateTime<Local> = modified_time.into();
            let today_date = now.date_naive();
            let modified_date = local_modified_time.date_naive();

            // If the app.log is modified yesterday or earlier, archive it
            if modified_date < today_date {
                let archive_path = Self::get_archive_log_path(log_dir, &modified_date);
                println!(
                    "INFO: Archiving old app.log from {} to {archive_path:?}",
                    modified_date.format("%Y-%m-%d")
                );
                fs::rename(&app_log_path, &archive_path).context(format!(
                    "Failed to rename old app.log from {app_log_path:?} to {archive_path:?}"
                ))?;
            }
        }
        Ok(())
    }

    // Auxiliary function: Get the archived file name according to the date, for example app.2025-07-04.log
    fn get_archive_log_path(base_dir: &Path, date: &chrono::NaiveDate) -> PathBuf {
        let file_name = format!("app.{}.log", date.format("%Y-%m-%d"));
        base_dir.join(file_name)
    }

    // Handles rotation logic.
    // If opening a new file fails, it logs the error but does not replace the current active File.
    // This means the log will continue to be written to the old file.
    fn rotate_log_file(&self) -> Result<()> {
        let now = Local::now();
        let today = now.day();

        // Use unlocked atomic operations to read dates, avoiding long locks
        if self.last_rotation_day.load(Ordering::Relaxed) == today {
            return Ok(()); // The date has not changed, just return
        }
        // The date has been changed and rotation logic is implemented
        println!("INFO: Daily log rotation triggered for day: {}", today);

        // Archiving of old documents
        Self::archive_old_app_log_if_needed(&self.log_dir, &now)?;

        let new_file_path = self.log_dir.join("app.log");
        // Open a new file, the new file is always app.log, if it fails, `? ` will propagate the error upstream
        let new_file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&new_file_path)
            .context(format!(
                "Failed to open new daily log file {new_file_path:?}"
            ))?;

        // It is updated only after all operations have succeeded active_file, last_rotation_day
        *self.active_file.lock().unwrap() = new_file;
        self.last_rotation_day.store(today, Ordering::Relaxed);

        println!("INFO: New log file opened: {new_file_path:?}");

        Ok(())
    }
}

// Implement MakeWriter for LocalTimeRollingWriter
impl<'a> fmt::MakeWriter<'a> for LocalTimeRollingWriter {
    type Writer = LogFileWriter;
    fn make_writer(&self) -> Self::Writer {
        if let Err(e) = self.rotate_log_file() {
            eprintln!(
                "[Log Rotation Error] Failed to rotate log file, logging will continue on the old file. Error: {e:?}"
            );
        }
        LogFileWriter(Arc::clone(&self.active_file))
    }
}

// =====================================================================
// Log Initialization Function
pub fn init_logging() -> Result<()> {
    // 1. Create a custom local time rolling file writer
    let custom_file_writer = LocalTimeRollingWriter::new("logs")?;

    // 2. Create a fmt layer for file output
    let file_layer = fmt::layer()
        .with_ansi(false)
        .with_writer(custom_file_writer)
        .with_target(true)
        .with_timer(LocalTimer)
        .with_thread_ids(true)
        .with_thread_names(true)
        .with_line_number(true)
        .with_file(true)
        .with_level(true)
        .with_filter(EnvFilter::new("info"));

    // 3. Create a fmt layer for console output
    let stdout_layer = fmt::layer()
        .with_ansi(true)
        .with_timer(LocalTimer)
        .with_thread_ids(true)
        .with_thread_names(true)
        .with_line_number(true)
        .with_file(true)
        .with_level(true)
        .with_filter(EnvFilter::new("info"));

    // 4. Combine two layers and initialize global subscribers
    tracing_subscriber::registry()
        .with(stdout_layer)
        .with(file_layer)
        .init();

    Ok(())
}

2. 解决方法二

使用社区logroller crate来实现

[dependencies]
logroller = "0.1
use anyhow::{Context, Result};
use chrono::Local;
use logroller::{Compression, LogRollerBuilder, Rotation, RotationAge, TimeZone};
use std::fs::{self};
use std::path::PathBuf;

use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::fmt::format::Writer;
use tracing_subscriber::fmt::time::FormatTime;
use tracing_subscriber::{self, filter::EnvFilter, fmt, prelude::*, util::SubscriberInitExt};

// 自定义本地时间格式
pub struct LocalTimer;

impl FormatTime for LocalTimer {
    fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result {
        write!(w, "{}", Local::now().format("%Y-%m-%d %H:%M:%S%.3f"))
    }
}

// =====================================================================
// Log Initialization Function
// =====================================================================

/// 初始化应用程序的 tracing 日志系统。
///
/// 配置包括:
/// - 控制台输出层,使用本地时间、线程ID/名称、文件名/行号和日志级别。
/// - 文件输出层,使用 tracing-appender 按天轮转(文件名如 app.YYYY-MM-DD.log),并在初始化时压缩旧日志文件。
/// - 注意:压缩使用 Gz 格式,仅在初始化时执行(不实时)。
pub fn init_logging() -> Result<WorkerGuard> {
    let log_dir = PathBuf::from("logs");
    fs::create_dir_all(&log_dir).context(format!("Failed to create log directory: {log_dir:?}"))?;

    // 使用 logroller 创建按本地时区每天轮转的文件 appender
    let appender = LogRollerBuilder::new("logs", "app") // 目录和基础文件名(会生成 app.YYYY-MM-DD.log)
        .rotation(Rotation::AgeBased(RotationAge::Daily)) // 每天轮转
        .suffix("log".to_string())
        .time_zone(TimeZone::Local) // 使用本地时区(东八区)
        .compression(Compression::Gzip) // 自动压缩旧文件为 .gz
        .max_keep_files(30) // 可选:保留最近 30 个文件,防止无限增长
        .build()
        .context("Failed to build logroller appender")?;

    // 创建非阻塞 writer(异步写入)
    let (non_blocking, guard) = tracing_appender::non_blocking(appender);

    // 创建一个 fmt 层用于文件输出
    let file_layer = fmt::layer()
        .with_ansi(false) // 文件输出一般不需要 ANSI 颜色
        .with_writer(non_blocking) // 使用 tracing-appender 的 writer
        .with_target(true)
        .with_timer(LocalTimer) // 使用定义的本地时间格式
        .with_thread_ids(true)
        .with_thread_names(true)
        .with_line_number(true)
        .with_file(true)
        .with_level(true)
        .with_filter(EnvFilter::new("info")); // 文件日志一般使用 info 级别

    // 创建一个 fmt 层用于控制台输出
    let stdout_layer = fmt::layer()
        .with_ansi(true) // 控制台输出可以有颜色
        .with_timer(LocalTimer)
        .with_thread_ids(true)
        .with_thread_names(true)
        .with_line_number(true)
        .with_file(true)
        .with_level(true)
        .with_filter(EnvFilter::new("debug")); // 控制台日志一般使用 debug 级别

    // 将两个层组合起来并初始化全局订阅者
    tracing_subscriber::registry()
        .with(stdout_layer)
        .with(file_layer)
        .init();

    Ok(guard)
}

main.rs 调用

let _guard = logging::init_logging().context("Failed to initialize logging")?;

主线程需持有guard,不然guard会在init_logging调用完后drop掉导致 worker 线程立即停止(不会写日志到文件中)

3. 总结

方法一好处是不依赖外部crate,但代码复杂冗余不好维护;方法二好处是代码简洁,但依赖外部crate。最后推荐方法二先过渡,等待官方修复。

结语

tracing是日志事件生产者,提供宏与 API,生成结构化事件和 span;tracing-subscriber 是日志事件消费者,订阅、过滤、格式化并输出日志 ;tracing-appender是日志持久化工具,将日志写入文件(可滚动)。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容