跳过正文

tracing

··3368 字
Rust Rust-Crate
目录
rust crate - 这篇文章属于一个选集。
§ 7: 本文

推荐参考材料

  1. Rust telemetry workshop
  2. Request coalescing in async Rust

核心概念:

  1. Span :记录程序的执行流程,有 enter 和 exit 操作,当 enter 后,当前线程会关联一个 span context,所有该 context 中记录的 event 都属于该 span。

           use tracing::{span, Level};
           // 必选参数:Level 和 Span id(name)
           let span = span!(Level::TRACE, "my_span");
    
           // enter() 返回一个 RAII guard,当它被 drop 时,span 自动退出。
           let _enter = span.enter();
           // 在 span 上下文记录 event。
    
  2. Events :Event 表示特定时刻的事件,通常位于 span context 中(非必须)。

           use tracing::{event, span, Level};
    
           // Event 可以位于 span context 之外
           event!(Level::INFO, "something happened");
    
           let span = span!(Level::INFO, "my_span");
           let _guard = span.enter();
           // 在 my_span context 中记录一个 event。
           event!(Level::DEBUG, "something happened inside my_span");
    
  3. Subscribers :当 span 和 event 发生时,它们被实现 Subscriber trait 的对象所记录或聚合。Subscribers 提供了 enabled()/event()/enter()/exit() 等方法。

1 span!/event!()
#

使用 span!() 来创建特定 Level 和 id 的 span,然后调用它的 enter() 方法来创建一个 span context,后续该 span 对象被 drop 前,所有 event 都属于该 span。

  • span 的 enter() 可以嵌套, 子 span 打印 event 时会自动显示父 span;
  • span 的 fields 会自动被其中的 event 继承;
  • event 和 span 都需要指定 Level 级别;
use tracing::{span, Level};

let span = span!(Level::TRACE, "my span");
let _enter = span.enter();
// Any trace events that occur before the guard is dropped will occur
// within the span.  Dropping the guard will exit the span.

event 可以没有 span ,例如在创建和 enter span 前创建的 event,这时 target span 默认为所在的 crate::module。event 可以使用 target: "span_name"parent: &span 来指定父span。

tracing::error_span!("myerrorspan", ?s);
tracing::error!(target: "myerrorspan", ?s, s.field_a, "just a debug message2");

对于自定义函数,可以使用 #[instrument] 属性宏来自动创建 span,它使用函数名作为 span name, 函数参数将作为 span 的 fields。函数内的 event 自动位于这个 span context 中。

  • skip():要去掉的参数名;
  • fields(): 指定要新加的 field=value;

由于 #[instrument] 默认将所有函数参数添加到日志输出,对于 HTTP Handler 函数来说,可能包含敏感信息(如用户提交的表单或 Body 数据等),可以使用 secrecy crate 来解决这个问题(它通过重新定义嵌入的字段类型的 Debug trait 来实现隐藏数据输出)。

use tracing::{Level, event, instrument};

#[instrument]
pub fn my_function(my_arg: usize) {
    // 这个 event 位于 my_function span 下,具有 my_arg field
    event!(Level::INFO, "inside my_function!");
    // ...
}

#[instrument(level="debug", target="this_crate::some_span", name="my_instrumented_span")]
async fn do_something_async() {
    // do some work
}

#[instrument(fields(http.uri = req.uri(), http.method = req.method()))]
pub fn handle_request<B>(req: http::Request<B>) -> http::Response<B> {
    // ... handle the request ...
}

// instrument 支持丰富的配置参数
impl Handler {
    #[instrument(
        name = "Handler::run",
        skip(self),
        fields(
            // `%` serializes the peer IP addr with `Display`
            peer_addr = %self.connection.peer_addr().unwrap()
        ),
    )]
    async fn run(&mut self) -> mini_redis::Result<()> {
        //...
    }
}

//! src/routes/subscriptions.rs
// [...]
#[tracing::instrument(
    name = "Adding a new subscriber",
    skip(form, pool),
    fields(
        request_id = %Uuid::new_v4(),
        subscriber_email = %form.email,
        subscriber_name= %form.name
    )
)]
pub async fn subscribe(form: web::Form<FormData>, pool: web::Data<PgPool>,) -> HttpResponse {
    match insert_subscriber(&pool, &form).await
    {
        Ok(_) => HttpResponse::Ok().finish(),
        Err(_) => HttpResponse::InternalServerError().finish()
    }
}

对于不能使用 #[instrument] 的第三方函数,可以使用 span 的 in_scope() 方法:

use tracing::info_span;
let json = info_span!("json.parse").in_scope(|| serde_json::from_slice(&buf))?;

使用 event!() 宏来记录 event,level 是必须的,但 message 是可选的:

use tracing::{event, Level};

// level 和 message, 字符串字面量默认为 message,故 message key 可以忽略。
event!(Level::INFO, "something has happened!");
// 等效为
event!(Level::INFO, message = "something has happened!")

event!(Level::INFO, key = "value") // 没有 message(为空)

span 和 event 都需要指定 Level,其它可选:

  1. message:可选;
  2. name: 默认为 ;
  3. target:默认为 module path,如 mycreate::my_module;
  4. parent:current span;
// span 必选: Level 和 span name/id
let span = span!(Level::TRACE, "my span");

// event:Level 必选, message 可选
event!(Level::INFO, "something has happened!");
event!(Level::INFO);  // 没有 message

// event 可选:parent span
event!(parent: &span, Level::INFO, "something has happened!");

// event 可选:target
event!(target: "app_events", Level::INFO, "something has happened!");

// event 可选:name,默认为 file:line
event!(name: "some_info", Level::INFO, "something has happened!");

span 和 event 还可以使用逗号分割的 field_name=field_value 来指定自定义属性:

  • span 的 field 都会被 event 继承;
// 对于 span, 必须在 span name/id 后添加 field=value
tracing::error_span!("myerrorspan", ?s);
// tracing::error_span!(?s, "myerrorspan"); // 错误

// 对于 event, 分两种情况:
// 1. 如果有 message, 自定义 field 都必须位于 message 之前;
// 2. 如果没有 message, 则可以使用 field=value 形式来定义任意 field.
tracing::error!(target: "myerrorspan", ?s, s.field_a, "just a debug message2");
tracing::error!(target: "myerrorspan", ?s, s.field_a, a = "b");  // 没有 message

// 在 enter _span drop 前, 后续的 event 都自动关联该 span
let _span = tracing::error_span!("my_enter_span", ?s).entered();
tracing::error!(?s, s.field_a, a = "c");

let user = "ferris";
span!(Level::TRACE, "login", user);
// 等效于
span!(Level::TRACE, "login", user = user);

let user = "ferris";
let email = "[email protected]";
// field name 中间可以有 .
span!(Level::TRACE, "login", user, user.email = email);

let user = User { name: "ferris", email: "[email protected]", };
// span 具有两个 fields `user.name = "ferris"` 和 `user.email = "[email protected]"`.
span!(Level::TRACE, "login", user.name, user.email);

value 名称前使用 ?和 % 来指定 Debug 或 Display 显示方式:

  • 对于自定义类型,没有实现 tracing::Value trait 时,不能直接显示,否则报错:the trait `tracing::Value` is not implemented for `XX`;
  • 但是可以为自定义类型实现 Debug 或 Display,然后来显示。
#[derive(Debug)]
struct MyStruct { field: &'static str, }
let my_struct = MyStruct { field: "Hello world!"};

// ?使用 Debug
event!(Level::TRACE, greeting = ?my_struct);
// 等效于
event!(Level::TRACE, greeting = tracing::field::debug(&my_struct));

// % 使用 Display
event!(Level::TRACE, greeting = %my_struct.field);
// 等效于
event!(Level::TRACE, greeting = tracing::field::display(&my_struct.field)

// ?和 % 也可以用在 key 名称前
event!(Level::TRACE, %my_struct.field);

如果为 span field 指定特殊的 tracing::field::Empty 值,则后续可以再设置:

use tracing::{trace_span, field};

// 创建一个 span, 具有两个 field,其中 greeting 具有值,而 parting
// 需要后续设置值
let span = trace_span!("my_span", greeting = "hello world", parting = field::Empty);

// 后续使用 span.record() 方法为 Empty field 指定具体的值
span.record("parting", &"goodbye world!");

event message 可以使用 format 字符串(span name/id 不支持格式化),这也是 event message 必须位于 key=value 后面的原因:后续的参数是 format 字符串的参数:

let question = "the ultimate question of life, the universe, and everything";
let answer = 42;
event!(
    Level::DEBUG,
    question.answer = answer,
    question.tricky = true,
    "the answer to {} is {}.", question, answer  // message
);

为了方便创建指定 Level 的 span 和 event, 可以使用带 level 的特殊宏, 如:

  • event : trace!, debug!, info!, warn!, error!
  • span : trace_span!, debug_span!, info_span!, warn_span!, error_span!

对于 span 的 enter/exit event,默认在 trace 级别才打印(-> 和 <-),可以设置 RUST_LOG=trace cargo run 来查看。

[.. INFO zero2prod] Adding a new subscriber.; request_id=f349b0fe..
[email protected] subscriber_name=le guin
[.. TRACE zero2prod] -> Adding a new subscriber.
[.. INFO zero2prod] request_id f349b0fe.. - Saving new subscriber details
in the database
[.. INFO zero2prod] request_id f349b0fe.. - New subscriber details have
been saved
[.. TRACE zero2prod] <- Adding a new subscriber.
[.. TRACE zero2prod] -- Adding a new subscriber.
[.. INFO actix_web] .. "POST /subscriptions HTTP/1.1" 200 .

2 metadata
#

span 和 event 都带有 Metadata 类型信息:

impl<'a> Metadata<'a>

pub const fn new(
    name: &'static str, // 缺省:file:line
    target: &'a str, // 缺省:module path
    level: Level,
    fields: FieldSet,
    kind: Kind // 可选值为 EVENT,SPAN,HINT
    // 下面可选的 metadata 指定源码位置:
    file: Option<&'a str>,
    line: Option<u32>,
    module_path: Option<&'a str>,
) -> Metadata<'a>

Subscriber::enable() 方法可以使用 span 或 event 关联的 Metadata 信息来对它们进行过滤。

3 log crate 互操作
#

创建 event 的 trace!, debug!, info! 等宏名称和 log crate 提供的记录日志的宏名称相同, 可以直接替换使用, tracing 的 event 包含了更丰富的结构化信息。

use std::{error::Error, io};
use tracing::{debug, error, info, span, warn, Level};

#[tracing::instrument]
pub fn shave(yak: usize) -> Result<(), Box<dyn Error + 'static>> {
    debug!(excitement = "yay!", "hello! I'm gonna shave a yak.");
    if yak == 3 {
        warn!("could not locate yak!");
        return Err(io::Error::new(io::ErrorKind::Other, "shaving yak failed!").into());
    } else {
        debug!("yak shaved successfully");
    }
    Ok(())
}

pub fn shave_all(yaks: usize) -> usize {
    let _span = span!(Level::TRACE, "shaving_yaks", yaks).entered();

    info!("shaving yaks");

    let mut yaks_shaved = 0;
    for yak in 1..=yaks {
        let res = shave(yak);
        debug!(yak, shaved = res.is_ok());

        if let Err(ref error) = res {
            error!(yak, error = error.as_ref(), "failed to shave yak!");
        } else {
            yaks_shaved += 1;
        }
        debug!(yaks_shaved);
    }
    yaks_shaved
}

tracing crate 支持与 log crate 互操作:

  1. tracing 可以 emit log crate 消费的 log records;
  2. tracing subscribers 也可以将 log crate emit 的 log records 当作 tracing events 来消费 (需要使用 tracing-log crate);

生成 log records:tracing 将 span 和 event 转换为 log record。需要配置如下两个feature:

  • log feature: 在 没有激活 tracing Subscriber 的情况下将 tracing event/span 转换为 log record;
  • log-always feature: 即使激活了 tracing Subscriber, 也将 tracing event/span 转换为 log record;

生成的 log record 包含 span/event 的 fileds 和 metadata(如 target,level,module path,file,line 和 number 等)。而且 span 的 entered/exited/close()也会创建 log record,它们的 log target 为 tracing::span。

消费 log records:=tracing-log crate= 提供了兼容层,它让 tracing subscriber 使用 tracing event 来消费和记录 log records 。适合于没有使用 tracing event 而使用 log crate 的 APIs 来记录日志的库(特别是一些同步库),否则会导致这些日志记录不被输出打印。

//! src/main.rs
//! [...]
use tracing::subscriber::set_global_default;
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};

// path 是标识符,不支持短横杠,需要将 cargo package name 的短横杠转为下划线。
use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry};
use tracing_log::LogTracer;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    // Redirect all `log`'s events to our subscriber
    LogTracer::init().expect("Failed to set logger");

    let env_filter = EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| EnvFilter::new("info"));

    let formatting_layer = BunyanFormattingLayer::new(
        "zero2prod".into(),
        std::io::stdout
    );

    let subscriber = Registry::default()
        .with(env_filter)
        .with(JsonStorageLayer)
        .with(formatting_layer);

    set_global_default(subscriber).expect("Failed to set subscriber");
    // [...]
}

4 subscriber
#

tracing::subscriber::Subscriber trait 定义了收集 trace/event 的函数接口:

  • enabled() : 根据 Metadata 来判断是否要记录该 record;
  • new_span(): 根据 Attributes 返回一个 span ID;
  • enter(): 开启一个新的 span;
  • exit(): 退出(完成)一个 span;

tracing crate 并没有提供该 trait 的实现,而是由其它 crate 来实现,如 tracing-subscriber crate。

pub trait Subscriber: 'static {
    // Required methods
    fn enabled(&self, metadata: &Metadata<'_>) -> bool;
    fn new_span(&self, span: &Attributes<'_>) -> Id;
    fn record(&self, span: &Id, values: &Record<'_>);
    fn record_follows_from(&self, span: &Id, follows: &Id);
    fn event(&self, event: &Event<'_>);
    fn enter(&self, span: &Id);
    fn exit(&self, span: &Id);

    // Provided methods
    fn on_register_dispatch(&self, subscriber: &Dispatch) { ... }
    fn register_callsite(&self, metadata: &'static Metadata<'static> ) -> Interest { ... }
    fn max_level_hint(&self) -> Option<LevelFilter> { ... }
    fn event_enabled(&self, event: &Event<'_>) -> bool { ... }
    fn clone_span(&self, id: &Id) -> Id { ... }
    fn drop_span(&self, _id: Id) { ... }
    fn try_close(&self, id: Id) -> bool { ... }
    fn current_span(&self) -> Current { ... }
    unsafe fn downcast_raw(&self, id: TypeId) -> Option<*const ()> { ... }
}

tracing::subscriber::set_default()/set_global_default()/with_default() 方法用于设置当前线程或全局的 subscriber(一般由 subscriber 的 init() 等方法自动设置):

  • set_default():为 当前线程 设置缺省的 Subscribe 实现;
  • set_global_default():为程序 所有线程 设置缺省的 Subscribe 实现;
  • with_default() :为闭包代码指定使用的缺省 Subscribe 实现;
// FooSubscriber 实现了 Subscriber
let my_subscriber = FooSubscriber::new();

// 全局 subscriber
tracing::subscriber::set_global_default(my_subscriber)
    .expect("setting tracing default failed");

// 局部闭包 Subscribe,可以按需创建多个 subscriber,分别来使用。
tracing::subscriber::with_default(my_subscriber, || {
    // 闭包内的 trace/event 将被 my_subscriber 记录
})

set_global_default() 的底层创建一个 tracing::dispatcher::Dispatch 对象,然后设置它为全局 dispatcher:

pub fn set_global_default<S>(subscriber: S) -> Result<(), SetGlobalDefaultError>
    where S: Subscriber + Send + Sync + 'static,
{
    crate::dispatcher::set_global_default(crate::Dispatch::new(subscriber))
}

tracing::dispatcher::Dispatch 对象负责发送 trace 数据给 Subscriber 实现

  • set_default():为 当前线程 设置缺省的 Dispatch;
  • set_global_default():为 程序所有线程 设置缺省的 Dispatch;
  • with_default() :为闭包代码指定使用的缺省 Dispatch;
  • get_default():返回当前线程使用的 Dispatch;
pub struct Dispatch { /* private fields */ }
// 从 Subscriber 实现来创建 Dispatch
pub fn new<S>(subscriber: S) -> Dispatch where S: Subscriber + Send + Sync + 'static,

// 创建一个 dispatch
use dispatcher::Dispatch;
let my_subscriber = FooSubscriber::new();
let my_dispatch = Dispatch::new(my_subscriber);

// 使用方式 1: 为闭包函数设置缺省 Subscribe
dispatcher::with_default(&my_dispatch, || {
    // my_subscriber is the default
});

// 使用方式 2: 为全局所有线程设置缺省 Subscribe
dispatcher::set_global_default(my_dispatch)
    .expect("global default was already set!");
// `my_subscriber` is now the default

// 使用方式 3: 为当前线程设置缺省 Subscribe
dispatcher::set_default(my_dispatch)
    .expect("default was already set!");
rust crate - 这篇文章属于一个选集。
§ 7: 本文

相关文章

anyhow
··1874 字
Rust Rust-Crate
anyhow crate 提供了自定义 Error 类型和 Result 类型,Error 类型自带 backtrace 和 context,支持用户友好的格式化信息输出。
bytes
··2922 字
Rust Rust-Crate
bytes 提供了高效的 zero-copy 连续内存区域的共享和读写能力。
chrono
··4023 字
Rust Rust-Crate
chrono 提供了丰富的 Date/Time 类型和相关操作。
hyper
··861 字
Rust Rust-Crate
hyper 是高性能的异步 HTTP 1/2 底层库。