2次发布

0.3.1 2024年2月16日
0.3.0 2024年2月14日

#401 in 调试

每月47次下载
hitdns中使用

MIT/Apache

73KB
1K SLoC

ftlog

Build Status License Latest Version ftlog

日志受磁盘I/O和管道系统调用的影響。在低延迟至关重要的场景中(例如,高频交易),连续的日志调用可能会成为瓶颈。

ftlog通过将消息发送到专用日志线程并在主/工作线程中进行尽可能少的计算来缓解这个瓶颈。

ftlog可以将主/工作线程中的日志性能提高几倍。有关详细信息,请参阅性能部分。

使用方法

将以下内容添加到您的Cargo.toml

ftlog = "0.2"

main函数开始处配置和初始化ftlog

// ftlog re-export `log`'s macros, so no need to add `log` to dependencies
use ftlog::appender::FileAppender;
use ftlog::{debug, trace};
use log::{error, info, warn};

// minimal configuration with default setting

// When drops, the guard calls and waits `flush` to logger.
// With guard that share the lifetime of `main` fn, there is no need to manually call flush at the end of `main` fn.
let _guard = ftlog::builder().try_init().unwrap();

trace!("Hello world!");
debug!("Hello world!");
info!("Hello world!");
warn!("Hello world!");
error!("Hello world!");

更复杂但功能丰富的使用

use ftlog::{
    appender::{Duration, FileAppender, Period},
    FtLogFormatter, LevelFilter,
};

let time_format = time::format_description::parse_owned::<1>(
    "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:6]",
)
.unwrap();
// configurate logger
let _guard = ftlog::builder()
    // global max log level
    .max_log_level(LevelFilter::Info)
    // custom timestamp format
    .time_format(time_format)
    // set global log formatter
    .format(FtLogFormatter)
    // use bounded channel to avoid large memory comsumption when overwhelmed with logs
    // Set `false` to tell ftlog to discard excessive logs.
    // Set `true` to block log call to wait for log thread.
    // here is the default settings
    .bounded(100_000, false) // .unbounded()
    // define root appender, pass anything that is Write and Send
    // omit `Builder::root` will write to stderr
    .root(
        FileAppender::builder()
            .path("./current.log")
            .rotate(Period::Day)
            .expire(Duration::days(7))
            .build(),
    )
    // Do not convert to local timezone for timestamp, this does not affect worker thread,
    // but can boost log thread performance (higher throughput).
    .utc()
    // level filter for root appender
    .root_log_level(LevelFilter::Warn)
    // write logs in ftlog::appender to "./ftlog-appender.log" instead of "./current.log"
    .filter("ftlog::appender", "ftlog-appender", LevelFilter::Error)
    .appender("ftlog-appender", FileAppender::new("ftlog-appender.log"))
    .try_init()
    .expect("logger build or set failed");

有关更多信息(例如自定义格式),请参阅./examples

默认日志格式

2022-04-08 19:20:48.190+08 298ms INFO main src/ftlog.rs:14 我的日志消息

在这里,298ms表示日志调用(例如 log::info!("msg"))和实际打印到日志线程之间的延迟。通常这为0ms。

大延迟表示日志线程可能因过多的日志消息而被阻塞。

2022-04-10 21:27:15.996+08 0ms 2 INFO main src/main.rs:29 limit running3 !

上方的数字 2 表示丢弃了多少条日志消息。仅当单个日志调用的日志频率受限时才显示(例如,log::info!(limit=3000i64;"msg"))。

随机丢弃日志

使用 random_dropdrop 来指定随机丢弃日志的概率。默认情况下不丢弃任何消息。

log::info!(random_drop=0.1f32;"Random log 10% of log calls, keeps 90%");
log::info!(drop=0.99f32;"Random drop 99% of log calls, keeps 1%");

当格式化日志消息为字符串的成本太高时,这很有用,

当同时指定了 random_droplimit 时,ftlog 会在随机丢弃消息后限制日志。

log::info!(drop=0.99f32,limit=1000;
    "Drop 99% messages. The survived 1% messages are limit to at least 1000ms between adjacent log messages output"
);

自定义时间戳格式

ftlog 依赖于 time crate 来格式化时间戳。要使用自定义时间格式,首先构建一个有效的时间格式描述,然后通过 ftlog::time_format(&mut self) 将其传递给 ftlog 构建器。

如果在格式化时间戳时发生错误,ftlog 将回退到 RFC3339 时间格式。

示例

let format = time::format_description::parse_owned::<1>(
    "[year]/[month]/[day] [hour]:[minute]:[second].[subsecond digits:6]",
)
.unwrap();
let _guard = ftlog::builder().time_format(format).try_init().unwrap();
log::info!("Log with custom timestamp format");
// Output:
// 2023/06/14 11:13:26.160840 0ms INFO main [main.rs:3] Log with custom timestamp format

带有间隔的日志

ftlog 允许限制单个日志调用的写入频率。

如果上述行在 3000ms 内被多次调用,则只记录一次,并添加一个数字来反映丢弃的日志消息数量。

每个日志调用都有一个独立的间隔,因此我们可以为不同的日志调用设置不同的间隔。内部,ftlog 通过(模块名称、文件名、代码行)的组合记录最后打印时间。

示例

info!(limit=3000i64; "limit running {}s !", 3);

上述特定日志调用的最小间隔为 3000ms。

2022-04-10 21:27:10.996+08 0ms 0 INFO main [src/main.rs:29] limit running 3s !
2022-04-10 21:27:15.996+08 0ms 2 INFO main [src/main.rs:29] limit running 3s !

上方的数字 2 显示了自上次日志以来丢弃了多少条日志消息。

日志轮转

ftlog 支持本地时区中的日志轮转。可用的轮转周期包括

  • 分钟 Period::Minute
  • 小时 Period::Hour
  • Period::Day
  • Period::Month
  • Period::Year

日志轮转在 FileAppender 中配置,时间戳附加到文件名末尾

use ftlog::appender::{FileAppender, Period};

let logger = ftlog::builder()
    .root(
        FileAppender::builder()
            .path("./mylog.log")
            .rotate(Period::Minute)
            .build(),
    )
    .build()
    .unwrap();
let _guard = logger.init().unwrap();

如果日志文件配置为按分钟分割,则日志文件名具有以下格式:mylog-{MMMM}{YY}{DD}T{hh}{mm}.log。当按日分割时,日志文件名类似:mylog-{MMMM}{YY}{DD}.log

日志文件名示例

$ ls
# by minute
current-20221026T1351.log
# by hour
current-20221026T13.log
# by day
current-20221026.log
# by month
current-202210.log
# by year
current-2022.log
# omitting extension (e.g. "./log") will add datetime to the end of log filename
log-20221026T1351

清理过期的日志

启用日志轮转后,可以通过FileAppender::rotate_with_expire方法清理过期的日志以释放磁盘空间,或者在使用构建器时设置expire(Duration)

ftlog首先找到由ftlog生成的文件,并通过最后修改时间清理过期的日志。ftlog通过匹配文件名和添加的日期时间查找生成的日志。

注意:与该模式匹配的任何文件都将被删除。

use ftlog::{appender::{Period, FileAppender, Duration}};

// clean files named like `current-\d{8}T\d{4}.log`.
// files like `another-\d{8}T\d{4}.log` or `current-\d{8}T\d{4}` will not be deleted, since the filenames' stem do not match.
// files like `current-\d{8}.log` will remains either, since the rotation durations do not match.

// Rotate every day, clean stale logs that were modified 7 days ago on each rotation
let appender = FileAppender::rotate_with_expire("./current.log", Period::Day, Duration::days(7));
let logger = ftlog::builder()
    .root(appender)
    .build()
    .unwrap();
let _guard = logger.init().unwrap();

特性

  • tsc 使用TSC作为时钟源以获得更高的性能而不会损失精度。

    TSC在特定条件下提供了获取当前时间最准确且成本最低的方式

    1. CPU频率必须保持恒定
    2. 必须具有x86/x86_64架构的CPU,因为TSC是x86/x86_64特定的寄存器。
    3. 永不挂起

    当前特性进一步要求构建目标必须是必须为LINUX。否则将回退到快速但精度较低的实现。

时区

出于性能考虑,时区在构建日志器时检测一次,并在之后的每个日志消息中使用。部分原因是时区检测成本高昂,部分原因是Linux中多线程程序在底层系统调用中的不安全性。

还建议使用UTC,以进一步避免在每次日志消息中将时间戳转换为时区。

性能

Rust:1.67.0-nightly

消息类型 Apple M1 Pro, 3.2GHz AMD EPYC 7T83, 3.2GHz
ftlog 静态字符串 75 ns/iter 385 ns/iter
ftlog 使用i32 106 ns/iter 491 ns/iter
env_logger
输出到文件
静态字符串 1,674 ns/iter 1,142 ns/iter
env_logger
输出到文件
使用i32 1,681 ns/iter 1,179 ns/iter
env_logger
使用BufWriter输出到文件
静态字符串 279 ns/iter 550 ns/iter
env_logger
使用BufWriter输出到文件
使用i32 278 ns/iter 565 ns/iter

许可:MIT OR Apache-2.0

依赖关系

~3–4.5MB
~72K SLoC