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是日志持久化工具,将日志写入文件(可滚动)。



















暂无评论内容