18个版本

0.5.0 2024年1月24日
0.4.1 2023年12月13日
0.3.5 2022年12月15日
0.3.4 2022年11月22日
0.1.8 2021年3月26日

过程宏类别中排名第1472

Download history 4841/week @ 2024-03-14 5359/week @ 2024-03-21 5202/week @ 2024-03-28 6873/week @ 2024-04-04 4452/week @ 2024-04-11 4573/week @ 2024-04-18 6126/week @ 2024-04-25 3911/week @ 2024-05-02 6783/week @ 2024-05-09 4288/week @ 2024-05-16 3757/week @ 2024-05-23 4345/week @ 2024-05-30 1887/week @ 2024-06-06 1979/week @ 2024-06-13 3378/week @ 2024-06-20 1918/week @ 2024-06-27

每月下载量达9,773
6个crate中使用(通过usdt

Apache-2.0

145KB
2.5K SLoC

usdt

用USDT探针拂去你的Rust。

概述

usdt将静态定义的DTrace探针暴露给Rust代码。用户编写一个提供者定义,可以使用D语言或直接在Rust代码中编写。然后,提供者的探针可以编译成触发探针的Rust代码。这些探针通过dtrace命令行工具可见。

将D探针定义转换为Rust的机制有三种。

  1. 一个build.rs脚本
  2. 一个类似于函数的过程宏,usdt::dtrace_provider
  3. 一个属性宏,usdt::provider

生成的代码在所有情况下都是相同的,尽管第三种方法比前两种提供更多的灵活性。有关详细信息,请参阅下面,但简要地说,第三种形式支持实现serde::Seralize的任何类型的探针参数。这些不同版本分别在crate probe-test-{build,macro,attr}中展示。

注意:此crate使用内联汇编来实现其魔法。有关在Rust 1.59之前以及在macOS 1.66之前使用它的讨论,请参阅注释

示例

本包中的probe-test-build二进制crate实现了一个完整的示例,使用编译时代码生成。

起点是一个名为 "test.d" 的 D 脚本。它的样子看起来像:

provider my_provider {
	probe start_work(uint8_t);
	probe stop_work(char*, uint8_t);
};

该脚本定义了一个名为 test 的单一提供者,包含两个探测点,分别是 startstop,并使用不同的参数集。目前支持整数原始类型、整数类型的指针以及 &str。注意,使用 char* 来表示 Rust 风格的 UTF-8 字符串。如果您需要字节数组,请使用 uint8_t*int8_t*

必须将此提供者定义转换为 Rust 代码,这可以通过一个简单的构建脚本完成。

use usdt::Builder;

fn main() {
	Builder::new("test.d").build().unwrap();
}

这将在目录 OUT_DIR 中生成一个文件,该文件包含触发的探测点的生成 Rust 宏。除非更改,否则该文件将与提供者定义文件同名,因此在这种情况下是 test.rs

在 Rust 代码中使用探测点的样子如下,位于 probe-test-build/src/main.rs

//! An example using the `usdt` crate, generating the probes via a build script.

use std::thread::sleep;
use std::time::Duration;

use usdt::register_probes;

// Include the Rust implementation generated by the build script.
include!(concat!(env!("OUT_DIR"), "/test.rs"));

fn main() {
    let duration = Duration::from_secs(1);
    let mut counter: u8 = 0;

    // NOTE: One _must_ call this function in order to actually register the probes with DTrace.
    // Without this, it won't be possible to list, enable, or see the probes via `dtrace(1)`.
    register_probes().unwrap();

    loop {
        // Call the "start_work" probe which accepts a u8.
        my_provider::start_work!(|| (counter));

        // Do some work.
        sleep(duration);

        // Call the "stop_work" probe, which accepts a &str and a u8.
        my_provider::stop_work!(|| ("the probe has fired", counter));

        counter = counter.wrapping_add(1);
    }
}

注意:在 1.59(以及在 macOS 上的 1.66)之前,需要 nightly 特性。请参见 注释 以获取讨论。

还可以看到,Rust 代码通过 include! 宏直接包含。探测定义被转换为 Rust 宏,并以提供者的名称命名模块,并以探测点命名的宏。在我们的例子中,第一个探测点被转换为一个宏 my_provider::start_work!

重要:请注意,应用程序 必须 调用 usdt::register_probes() 来实际注册探测点与 DTrace。如果不这样做,将不会影响应用程序的功能,但将无法使用 dtrace(1) 工具列出、启用或以其他方式查看探测点。

通过运行示例并按名称列出预期探测点,我们可以看到这是如何与 DTrace 连接的。

$ cargo run

在另一个终端中,使用以下命令列出匹配的探测点:

$ sudo dtrace -l -n my_provider*:::
   ID   PROVIDER            MODULE                          FUNCTION NAME
 2865  test14314  probe-test-build _ZN16probe_test_build4main17h906db832bb52ab01E [probe_test_build::main::h906db832bb52ab01] start_work
 2866  test14314  probe-test-build _ZN16probe_test_build4main17h906db832bb52ab01E [probe_test_build::main::h906db832bb52ab01] stop_work

探测参数

可以看到,探测宏是以闭包而不是以探测参数直接调用的。这有两个目的。

首先,它表明探测参数可能不会被评估。DTrace 为已定义的探测生成 "is-enabled" 探测点,这是一种简单的方法来检查探测是否当前已启用。如果探测已启用,则仅解包参数,因此用户 不得 依赖于副作用。闭包有助于表明这一点。

第二点是效率。同样,如果探测未启用,则不会评估参数。闭包仅在内部 之后 验证探测已启用后才会被评估,这样就可以避免如果探测被禁用时的参数打包的无效工作。

过程宏版本

该 crate 的过程宏版本可以在 probe-test-macro 示例中看到,该示例与上面的示例几乎相同。然而,没有 build.rs 脚本,因此,在 include! 宏的位置,找到了过程宏

dtrace_provider!("test.d");

此宏生成与上面相同的宏,但在源代码本身编译时进行。对于某些用例来说,这可能更容易,因为没有构建脚本。然而,过程宏有一些缺点。理解它们的内部结构可能很困难,尤其是在出现问题时。此外,即使提供者定义未更改,宏也会在每次编译时运行。对于小型提供者定义来说,这可能微不足道,但当定义许多探测时,用户可能会注意到编译时间明显增加。

可序列化类型

如上所述,定义提供者的三种形式几乎是等效的。唯一的区别在于支持实现 serde::Serialize 的类型。这使用 DTrace 的 JSON 功能 -- 任何可序列化的类型都可以使用 serde_json::to_string() 序列化为 JSON,并且可以使用 json 函数在 DTrace 脚本中解包和检查字符串。例如,假设我们有一个类型

#[derive(serde::Serialize)]
pub struct Arg {
    val: u8,
    data: Vec<String>,
}

以及一个探测定义

#[usdt::provider]
mod my_provider {
    use crate::Arg;
    fn my_probe(_: &Arg) {}
}

类型为 Arg 的值可以用于生成的探测宏。在 DTrace 脚本中,可以查看参数中的数据,如下所示

dtrace -n 'my_probe* { printf("%s", json(copyinstr(arg0), "ok.val")); }' # prints `Arg::val`.

json 函数还支持嵌套对象和数组索引,因此也可以做

dtrace -n 'my_probe* { printf("%s", json(copyinstr(arg0), "ok.data[0]")); }' # prints `Arg::data[0]`.

有关更多详细信息和使用方法,请参阅 probe-test-attr 示例。

序列化可能失败

请注意,在上述示例中,访问的 JSON 块的第一个键是 "ok"。这是因为 serde_json::to_string 函数可能失败,返回一个 Result。这会自然地映射到 JSON 中

  • Ok(_) => {"ok": _}
  • Err(_) => {"err": _}

在错误情况下,返回的 Error 使用其 Display 实现进行格式化。这不是一个学术问题。很容易构建成功编译但运行时无法序列化的类型,即使是对那些 #[derive(Serialize)] 的类型也是如此。有关详细信息,请参阅 此问题

关于注册的注意事项

请注意,上面的示例在 main 的顶部调用了 usdt::register_probes() 函数。此方法实际上是注册探测与 DTrace 内核模块。这对希望对其代码进行度量的库开发人员来说是一个难题,因为其库的消费者可能会忘记(或选择不)调用此函数。此问题有潜在的工作方案(init-sections,其他魔法),但每种方案都有重大的权衡。因此,目前的建议是

鼓励库开发人员重新导出 usdt::register_probes(或调用它的函数),并记录给用户,指出应调用此函数以确保注册探测。

注意

《usdt》crate需要内联汇编,这个特性在Rust 1.59版本中得到稳定。在之前的版本中,需要nightly工具链才能导入这个特性。出于兼容性考虑,crate中还包含一个空的操作实现,它生成所有相同的探测宏,但体为空(因此不需要内联汇编)。可以在构建crate时通过传递--no-default-features标志,或在[dependencies]表格中设置default-features = false来选择它。

库开发者可以使用usdt作为可选依赖项,通过特性进行控制,例如命名为usdt-probes或类似名称。这个特性会隐含usdt/asm特性,但默认情况下可以使用不执行任何操作的实现。例如,您的Cargo.toml可能包含以下内容:

[dependencies]
usdt = { version = "*", optional = true, default-features = false }

# ... later

[features]
usdt-probes = ["usdt/asm"]

这样用户可以选择在他们的工具链足够新或旧nightly工具链上启用了asm特性的情况下启用探测。

Rust asm特性

在1.59之前的工具链上,内联汇编特性(即使启用了特性)也是不可用的。这适用于调用探测宏的代码,包括在实现usdt的地方。这些生成的探测宏必须在一个模块中,该模块使用>=1.59的工具链构建,或者存在feature(asm)配置。

工具链版本和asm_sym特性

在macOS(其中链接器参与USDT探测创建)上,在1.66之前的工具链上,需要asm_sym特性(在2021年11月之前的nightly工具链中需要,除了asm之外;参见这个问题)。对于此类工具链,该特性可以仅包括在macOS中,例如,使用#![cfg_attr(target_os = "macos", feature(asm_sym))],或者无条件地包括在所有平台上。

asm_sym特性的添加带来了一个不幸的问题。现在无法使用在添加该特性之前和之后编译的usdtcrate(或任何定义探测的crate)。在前一种情况下,如果我们包含了asm_sym特性,我们会得到关于未知特性的错误,如果我们省略了特性,我们会在较新的编译器中得到关于特性门后面功能的错误。

幸运的是,随着asm_sym在1.66中的稳定,使用这个crate应该会变得更加简单。

参考文献

依赖项

~3.5–4.5MB
~87K SLoC