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)
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"
}
posted @ 2026-08-10 10:09  lxd670  阅读(4)  评论(0)    收藏  举报