17个版本

0.2.14 2024年5月23日
0.2.12 2023年12月21日
0.2.11 2023年11月30日
0.2.7 2023年6月14日
0.2.1 2022年11月25日

#108 in 调试

Download history 99/week @ 2024-04-25 75/week @ 2024-05-02 130/week @ 2024-05-09 68/week @ 2024-05-16 232/week @ 2024-05-23 87/week @ 2024-05-30 113/week @ 2024-06-06 88/week @ 2024-06-13 142/week @ 2024-06-20 104/week @ 2024-06-27 135/week @ 2024-07-04 62/week @ 2024-07-11 67/week @ 2024-07-18 98/week @ 2024-07-25 77/week @ 2024-08-01 63/week @ 2024-08-08

316 每月下载量
用于 logo

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(),
    )
    // timezone of log message timestamp, use local by default
    // .local_timezone()
    // or use fiexed timezone for better throughput, since retrieving timezone is a time consuming operation
    // this does not affect worker threads (that call log), but can boost log thread performance (higher throughput).
    .fixed_timezone(time::UtcOffset::current_local_offset().unwrap())
    // 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}{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 生成的文件,然后根据最后修改时间清理旧日志。通过文件名和添加的日期时间匹配来查找生成的日志。

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

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–4MB
~70K SLoC