Getting started with Tracing 开始使用 Tracing 日志

tracingcrate 是一个用来检测 Rust 程序收集结构化的,基于事件的诊断信息的框架。

在像 Tokio 这样的异步系统中,解释传统的日志信息可能非常具有挑战性。由于多个任务在同一线程上运行,因此关联的事件和日志行混合在一起,使得跟踪逻辑运行变得困难。tracing扩展了日志式诊断,允许库和应用来记录结构化的事件,以及具有时间性因果关系的附加信息——与日志消息不同的是,在tracing中的一个Span具有开始和结束时间,可以通过执行流进入和退出,并可以存在于相似跨度的潜逃树中。为了表示某一时刻发生的事情,tracing提供了事件的补充概念。SpanEvent都是结构化的,可以记录键入的数据和文本信息。

你可以使用tracing来:

安装

开始,我们需要添加tracingtracing-subscriber作为依赖:

[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"

tracingcrate 提供了使用发出追踪的 API。tracing-subscribercrate 提供了一些基本的实用程序,用于将这些追踪转发到外部监听器(比如stdout)。

订阅追踪

如果您正在编写一个可执行文件(而不是库),你需要注册一个追踪订阅者(tracing subscriber)。订阅者是处理应用和依赖发出的跟踪的类型,并可以执行例如计算指标,监视错误以及向外界重新发送跟踪(例如jurnaldstdout,或open-telemetry守护进程)等任务。

大多数情况下,你应该在main函数中注册你的跟踪订阅者越早越好。例如,由tracing-subscriber提供的FmtSubscriber类型会打印格式化的跟踪日志和事件到stdout,并可以像这样注册:

#[tokio::main]
pub async fn main() -> mini_redis::Result<()> {
    // 构造一个订阅者,以向 stdout 打印格式化跟踪日志
    let subscriber = tracing_subscriber::FmtSubscriber::new();
    // 在此之后,使用这个订阅者来进行触发日志
    tracing::subscriber::set_global_default(subscriber)?;
    ...
}

如果你运行这个程序,你可能会看到一些被 Tokio 触发的跟踪事件,但是你需要修改自己的应用来触发跟踪,以充分利用tracing

订阅者设置

在上面的示例中,我们已经使用默认设置设置了FmtSubscriber。其实,tracing-subscriber也提供了很多方法来设置FmtSubscriber,比如自定义输出格式,包括一些额外信息(比如线程 ID 或源代码位置)在日志中,并把日志写到不是stdout的其他地方。

例如:

#![allow(unused)]
fn main() {
// 开始设置 `fmt` 订阅者
let subscriber = tracing_subscriber::fmt()
    // 使用更紧凑的日志格式
    .compact()
    // 显示源代码文件路径
    .with_file(true)
    // 显示源代码所在行数
    .with_line_number(true)
    // 显示我们记录事件发生的线程ID
    .with_thread_ids(true)
    // 不要显示事件的目标路径(模块路径)
    .with_target(false)
    // 生成订阅者
    .finish();
}

有关于配置选项的详细信息,可以看tracing_subscriber::fmt的文档

除了tracing-subscriber中的FmtSubscriber,其他的Subscriber也可以实现它们自己记录tracing数据的方式。这包含替代输出格式,分析和聚合,以及与其他系统集成,例如分布式跟踪或日志聚合服务。许多 crate 都提供了Subscriber的实现。看这里来获取其他Subscriber的实现(不完整)列表。

最后,某些情况下,将记录跟踪的多种方式组合到一起,构建实现多种行为的Subscriber可能很有用。例如,tracing-subscribercrate 提供了Layertrait,可以表示与其他层组合在一起的订阅者组件。看这里了解使用Layer的细节。

触发 Span 和事件。

最简单触发 span 的方法是使用tracing提供的instrument预处理宏。,这可以重写函数体来触发 span,每次调用的时候都会;例如:

#![allow(unused)]
fn main() {
#[tracing::instrument]
fn trace_me(a: u32, b: u32) -> u32 {
    a + b
}
}

每次调用trace_me都会触发tracingSpan:

  1. 具有详细的info级别(“中间立场”的程度)
  2. 被命名为trace_me
  3. ab的值,是trace_me的参数

instrument属性是可高度配置的;例如,跟踪mini-redis-server中处理每个连接的方法:

#![allow(unused)]
fn main() {
use tracing::instrument;

impl Handler {
    /// 处理单个连接
    #[instrument(
        name = "Handler::run",
        skip(self),
        fields(
            // `%` 序列化了对等IP地址通过 `Display` trait
            peer_addr = %self.connection.peer_addr().unwrap()
        ),
    )]
    async fn run(&mut self) -> mini_redis::Result<()> {
        ...
    }
}
}

mini-redis-server现在会出发tracing Span,对于每个传入连接:

  1. 具有详细的info级别(“中间立场”的程度)
  2. 被命名为Hanler::run
  3. 有一些结构化的数据。
    • fields(...)指示发出 span应该在名为peer_addr字段中包含连接的SocketAddrfmt::Display表示形式。
    • skip(self)指示发出 span应该不记录Hanler的调试形式。

你还可以通过调用span!宏来手动构建Span,或任何其他级别的宏(error_span!, warn_span!, info_span!, debug_span!, trace_span)。

要触发事件,使用event!宏,或者任何其他级别的宏(error!, warn!, info!, debug!, trace!)。例如,记录客户端发送了格式错误的命令的日志:

#![allow(unused)]
fn main() {
// 转换 redis frame 到一个指令结构体。
// 如果 frame 不是一个可用的 redis 指令会返回错误
let cmd = match Command::from_frame(frame) {
    Ok(cmd) => cmd,
    Err(cause) => {
        // frame格式不正确无法解析
        // 这可能表明客户端有问题 (相对服务端来说)
        // 所以我们(1)触发一个warning
        //
        // 这里的宏语法是由 `tracing` crate 提供的
        // 它可被认为类似于
        //      tracing::warn! {
        //          cause = format!("{}", cause),
        //          "failed to parse command from frame"
        //      };
        // `tracing` 提供了结构化的日志,
        // 所以信息是通过key-value对“记录“的
        tracing::warn! {
            %cause,
            "failed to parse command from frame"
        };
        // ...然后 (2) 给客户端发回了error响应
        Command::from_error(cause)
    }
};
}

如果运行应用,你会看到为其处理的每个传入连接触发的 span 上下文装饰的事件。