tracing-appender日志保存到文件
默认输出到终端
use tracing::info;
fn main() {
tracing_subscriber::fmt()
.with_writer(std::io::stdout)
.init();
info!("Hello, world!");
}
日志输出到文件
日志写入流程
Event
|
v
fmt Layer
|
v
MakeWriter
|
v
Writer
直接使用OpenOptions创建文件
共享文件句柄
with_writer(file) - 共享同一个文件描述符
- 优点:无额外系统调用开销,文件描述符数量少
- 也就是说每一个event都会使用同一个file文件句柄,然后写入日志
- 所有event串行化排队写入
use tracing::info;
use std::fs::OpenOptions;
fn main() {
let file = OpenOptions::new()
.write(true) // 可以写
.append(true) // 追加
.create(true) // 不存在创建
.open("test.log").unwrap();
tracing_subscriber::fmt()
.with_writer(file) // 输出到日志文件中
.with_ansi(false)
.init();
info!("Hello, world!");
}
独享文件句柄
.with_writer(move || file.try_clone().unwrap()) — 每次新建描述符
- 优点:多线程时各线程使用独立描述符,减少锁竞争
- 每次日志都有 dup(Unix)或 DuplicateHandle(Windows)+ close 的系统调用开销
- 高并发时,频繁的创建/销毁描述符会成为性能瓶颈,反而比共享单个File更慢
use tracing::info;
use std::fs::OpenOptions;
fn main() {
let file = OpenOptions::new()
.write(true) // 可以写
.append(true) // 追加
.create(true) // 不存在创建
.open("test.log").unwrap();
tracing_subscriber::fmt()
.with_writer(move|| file.try_clone().unwrap()) // 输出到日志文件中
.with_ansi(false)
.init();
info!("Hello, world!");
}
总结
| 维度 | with_writer(file) |
with_writer(move || file.try_clone().unwrap()) |
|---|---|---|
| 句柄 | 所有事件共享同一个 File |
每个事件临时创建新的 File(用完即关) |
| 并发行为 | 串行化排队写入 | 各线程可并发发起 write 系统调用 |
| 系统调用 | 只有 write |
每次 dup + write + close |
| 性能 | 低频率日志更优 | 高并发下额外开销大,通常更差 |
| 原子性 | 单条日志完整输出 | 单条日志通常完整(tracing 会一次性 write),但多线程日志之间无全局顺序保证 |
tracing-appender
tracing-appender主要解决的是日志写入阻塞业务线程的问题
当
nfo!()被调用时,tracing-subscriber会直接执行file.write()。
如果磁盘繁忙、机械硬盘寻道慢、日志量很大,单次写入数据多、系统IO压力大
当前业务线程会被阻塞,影响请求处理延迟
cargo add tracing-appender
基础非阻塞写入
必须保留
_guard,它负责后台线程的生命周期,drop时会flush并关闭线程
use std::fs::OpenOptions;
use tracing::info;
use tracing_appender::non_blocking;
fn main() {
let file = OpenOptions::new()
.write(true) // 可以写
.append(true) // 追加
.create(true) // 不存在创建
.open("test.log").unwrap();
let (non_blocking, _guard) = non_blocking(file);
tracing_subscriber::fmt()
.with_writer(non_blocking)
.with_ansi(false)
.init();
info!("Hello, world!");
}
配合日志轮转
tracing-appender 中的 max_log_files 确实存在已知的问题。它的主要表现是在程序重启时,清理旧日志的逻辑会多删除一个文件,导致实际保留的文件数量比设定的 max_log_files 少一个
- Rotation::MINUTELY 每分钟
- Rotation::HOURLY 每小时
- Rotation::DAILY 每天
- Rotation::NEVER 不轮转
RollingFileAppender::new()
use std::fs::OpenOptions;
use tracing::info;
use tracing_appender::{
non_blocking,
rolling::{RollingFileAppender,Rotation}
};
fn main() {
// 按天轮转
let appender = RollingFileAppender::new(Rotation::DAILY, "logs", "app.log");
let (non_blocking, _guard) = non_blocking(appender);
tracing_subscriber::fmt()
.with_writer(non_blocking)
.with_ansi(false)
.init();
info!("Hello, world!");
}
# 会在log目录下创建日志
## 每天轮转
logs/app.log.2026-08-06
## 每小时轮转
logs/app.log.2026-08-06-15
## 每分钟轮转
logs/app.log.2026-08-06-15-56
## 不轮转
logs/app.log
RollingFileAppender::builder()
use std::fs::OpenOptions;
use tracing::info;
use tracing_appender::{
non_blocking,
rolling::{RollingFileAppender,Rotation}
};
fn main() {
// 按天轮转
let appender = RollingFileAppender::builder()
.rotation(Rotation::MINUTELY)
.filename_prefix("test")// 文件名
.filename_suffix("log") // 文件后缀
.max_log_files(2) // 只保留最近2个文件
.build("logs") // 文件夹
.unwrap();
let (non_blocking, _guard) = non_blocking(appender);
tracing_subscriber::fmt()
.with_writer(non_blocking)
.with_ansi(false)
.init();
info!("Hello, world!");
}
logs/test.2026-08-06-16-18.log
多次启动问题
⚠️⚠️⚠️设置分钟轮转,但是在同一分钟内多次启动,会把旧的一份删除
[package]
name = "test_tracing"
version = "0.1.0"
edition = "2024"
[dependencies]
chrono = "0.4.45"
tracing = "0.1.44"
tracing-appender = "0.2.5"
tracing-subscriber = { version = "0.3.23", features = ["env-filter", "json", "local-time"] }
tokio = { version = "1.53.1", features = ["full"] }
axum = "0.8.9"
16:05 ── 第一次运行(你的第一次 cargo run)
目录: [16-05.log](刚创建,当前活跃文件)
16:08 ── 第二次运行(你的第二次 cargo run)
扫描到: [16-05.log](已存在的旧文件)
当前时间 16:08,创建新文件 16-08.log
历史文件: [16-05.log](1个)≤ max_log_files(2)
不删除
目录: [16-05.log, 16-08.log]
16:08 ── 第三次运行(你的第三次 cargo run)
扫描到: [16-05.log, 16-08.log]
当前时间 16:08, 复用 16-08.log
历史文件:[16-05.log, 16-08.log] # ⚠️它把16-08.log 也算入进入去了
删除16-05.log # 有bug
当前文件:[16-08.log] # 最后只有16-08.log
结合axum
⚠️如果服务重启(如cargo run)、tracing_appender会检测log目录下是否达到max_log_files的值
如果达到了,那么会删除旧的一个,即便最新的一个是可以复用的。
- 设置
max_log_files的时候+1- 需要保留7天的,设置
max_log_files(8)
- 需要保留7天的,设置
use tracing::info;
use tracing_appender::{
non_blocking,
rolling::{RollingFileAppender,Rotation}
};
use axum::{
Router,
routing::get
};
#[tokio::main]
async fn main() {
// 每分钟轮转
let appender = RollingFileAppender::builder()
.rotation(Rotation::MINUTELY)
.filename_prefix("test")// 文件名
.filename_suffix("log") // 文件后缀
.max_log_files(2) // 只保留最近2个文件
.build("logs") // 文件夹
.unwrap();
let (non_blocking, _guard) = non_blocking(appender);
tracing_subscriber::fmt()
.with_writer(non_blocking)
.with_ansi(false)
.init();
let app = Router::new()
.route("/", get(get_handle));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn get_handle() -> &'static str {
info!("Hello, world!");
"hello"
}

浙公网安备 33010602011771号