类似于 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
的日志等级设置为 info
和 debug
。
通过 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 的生成逻辑。
参考
- FasTrace 由 TiDB 团队开发,可参考 Make Tracing Great Again 介绍,采用 TSC 采集时间数据,参考 TiKV 高性能追踪实现解析。