Rust 常用日志三方库介绍

2022-11-09 rust language

类似于 JAVA 中的 slf4j 库,Rust 中对应了 log 库,其提供了基本的标准接口,由官方进行维护,这里简单介绍。

简介

这个 log 库及其对应的 API 已经成为了事实上的标准,已经被其它日志框架所使用,在 GitHub Log 中除了官方提供的标准实现外,还介绍了一些常用的三方库,例如 env_logger simplelog 等。

所以,只要在使用这个库提供的标准接口,即使后面切换其它的库也很方便。

使用非常简单,只需要在 Cargo.toml 中增加如下依赖即可。

[dependencies]
log = "0.4"

其定义了一个 Log 特征。

pub trait Log: Sync + Send {
    fn enabled(&self, metadata: &Metadata<'_>) -> bool;
    fn log(&self, record: &Record<'_>);
    fn flush(&self);
}

另外,还提供了一整套标准的宏便于记录日志,包括了 trace! debug! info! warn! error! 等,其按照级别依次递增。

示例

可以直接通过如下方式使用。

use log::{trace, info, warn};

fn main() {
    trace!("trace");
    info!("info");
    warn!("warn");
}

不过此时即使运行也没有内容输出,因为这只是一个框架,没有具体的实现,可以搭配简单的 env_logger 使用。

env_logger

通常用于研发阶段,作为简单的日志输出,可以通过 RUST_LOG 环境变量控制,其中,模块声明采用逗号分隔各项,格式类似于 path::to::module=log_level,例如:

RUST_LOG="warn,test::foo=info,test::foo::bar=debug" ./test

也就是默认日志级别为 warn,将模块 foo 和其嵌套的模块 foo::bar 的日志等级设置为 infodebug

通过 cargo add log env_logger 命令添加,如果只想在测试用例中使用,可以通过 cargo add --dev env_logger 添加。

在代码中,需要通过 env_logger::init() 进行初始化。

源码解析

类似上述的 env_logger 或者很多简单日志库的实现,实际上,在 log 中提供了 Log 特征,不同的库必须要实现该特征,只是不同库会提供不同的接口。

pub enum Level {
    Error = 1,
    Warn,
    Info,
    Debug,
    Trace,
}
pub struct Metadata<'a> {
    level: Level,
    target: &'a str,
}
pub struct Record<'a> {
    metadata: Metadata<'a>,
    args: fmt::Arguments<'a>,
    module_path: Option<MaybeStaticStr<'a>>,
    file: Option<MaybeStaticStr<'a>>,
    line: Option<u32>,
    #[cfg(feature = "kv")]
    key_values: KeyValues<'a>,
}
pub trait Log: Sync + Send {
    fn enabled(&self, metadata: &Metadata) -> bool;
    fn log(&self, record: &Record);
    fn flush(&self);
}

上述的 Log 特征很简单,核心包含了配置日志级别,以及输出日志等接口,其中 Metadata 指定了那个模块对应的日志级别,而 Record 同时提供了一些常用的接口,例如 level() target() 等。

除此之外,在 log 库中还提供了如下的初始化以及配置函数,通常是在库初始化时调用。

pub fn set_boxed_logger(logger: Box<dyn Log>) -> Result<(), SetLoggerError> {
    set_logger_inner(|| Box::leak(logger))
}
pub fn set_logger(logger: &'static dyn Log) -> Result<(), SetLoggerError> {
    set_logger_inner(|| logger)
}
pub fn set_max_level(level: LevelFilter) {
    MAX_LOG_LEVEL_FILTER.store(level as usize, Ordering::Relaxed);
}

在调用 info debug 等宏时,实际上最终调用的还是 Log::log 函数。

Tracing

可以通过 cargo add tracing 添加依赖,如果是二进制则同时需要一个订阅者,可以通过 cargo add tracing-subscriber 添加,用来订阅打印日志信息。

如下的示例中同时还需要 log 库打印日志。

use tracing_subscriber::{fmt, prelude::*};

fn main() {
    tracing_subscriber::registry().with(fmt::layer()).init();

    let test = 42;
    tracing::info!(test, "Hello World");
}
// Output
// 2024-08-13T01:29:53.584766Z  INFO hello: Hello World test=42

上述只是打印了简单的日志,不过 trace 最常用的是在异步、分布式上下文中使用,为了进行跟踪,可以使用如下两种对象:

  • Span 记录某个时间段发生的问题。
  • Event 某个时间点发生的事件,可以单独使用,不过通常在 Span 上下文中使用。

使用 Span 可以关联其它 Span 构成层级关系。

use tracing::{event, span, Level};
use tracing_subscriber::{fmt, prelude::*};

fn main() {
    //tracing_subscriber::fmt::init(); // 两种初始化方式相同,这种更简单些
    tracing_subscriber::registry().with(fmt::layer()).init();

    let span1 = span!(Level::INFO, "HiSpan");
    let _guard1 = span1.enter(); // auto release
    event!(Level::INFO, "Some event happened");

    // equals to span2.follows_from(span1)
    let span2 = span!(parent: &span1, Level::INFO, "SpanIn");
    let _guard2 = span2.enter();
    event!(Level::INFO, "Some event happened again");
}

除了上述方式,还可以通过 #[instrument] 生成,详见 Tracing Instrument 中的介绍。

另外,还需要通过 Collector 接受数据,上述的 subscriber 就是一个简单的 Collector 实现。

常用三方库

整理常用的三方库。

  • tracing-chrome 用来生成 JSON 数据可以在 chrome://tracing 中打开。
  • tracing-flame 可以生成火焰图,需要配合 inferno 使用,命令详见如下。

可以通过 tracing-flame 生成火焰图。

cargo install inferno
cat tracing.folded | inferno-flamegraph > tracing-flamegraph.svg
cat tracing.folded | inferno-flamegraph --flamechart > tracing-flamechart.svg

源码解析

tracing-core 中定义了 Subscriber 特征,在某个时间点的线程中只能有一个 Subscriber 实例,从而可以确保生成唯一的 SpanID 标识,如果要输出到多个目标,可以使用 Layer 实现。

其中官方的 console-subscriber 就是 tracing-subscriber 的一个 Layer 实现,会在内存中聚合相关的数据。

Callsite

tracing-core::callsite 中定义的静态变量,通过宏 event/span 编译过程中生成,也就意味着每个 event/span 都会关联,对应了调用发起时的位置,同时会保存如下信息:

  • Metadata 用来描述 event/span 信息,包括了名称 (API)、Target(可API指定,默认模块名)、级别 (API)、自定义字段等。
  • Identifier 唯一标识对应 event/span 信息,实际就是 Callsite 的地址。
  • Interest 通过 register_callsite() 注册到 Subscriber 时返回是否启用,缓存结果减少计算量。

tracing-subscriber 包在上述的 init() 函数中会通过如下 tracing-core 中的函数设置全局的分发器,而且其需要实现 Subscriber 特征。

// [tracing-core] src/dispatcher.rs
#[derive(Clone)]
enum Kind<T> {
    Global(&'static (dyn Subscriber + Send + Sync)),
    Scoped(T),
}
pub struct Dispatch {
    subscriber: Kind<Arc<dyn Subscriber + Send + Sync>>,
}
fn set_global_default(dispatcher: Dispatch) -> Result<(), SetGlobalDefaultError>;

[tracing-subscriber] src/registry 定义了 Registry 结构体,其实现了 Subscriber 特征,包含了 ID 的生成逻辑。

参考