#tracing-subscriber #tracing #logging #subscriber #metrics #field-name

traceon

一个易于使用的日志和跟踪格式化工具,具有扁平化 JSON 或美观输出

13 个版本

0.3.1 2024 年 3 月 5 日
0.3.0 2023 年 1 月 13 日
0.2.1 2023 年 1 月 5 日
0.1.8 2023 年 1 月 1 日
0.1.7 2022 年 12 月 29 日

#113 in 调试

Download history 81/week @ 2024-03-13 104/week @ 2024-03-20 23/week @ 2024-03-27 81/week @ 2024-04-03 418/week @ 2024-04-10 78/week @ 2024-04-17 92/week @ 2024-04-24 68/week @ 2024-05-01 30/week @ 2024-05-08 36/week @ 2024-05-15 53/week @ 2024-05-22 79/week @ 2024-05-29 69/week @ 2024-06-05 59/week @ 2024-06-12 69/week @ 2024-06-19 89/week @ 2024-06-26

298 每月下载量

MIT/Apache

135KB
797

Traceon - trace on

一个易于使用的日志和跟踪格式化工具,具有扁平化 JSON 或美观输出。

它建立在并简化了 tracing crate 上,该 crate 为日志消息添加上下文,以增强可观察性,尤其是在异步或多线程环境中,它侧重于以扁平化 JSON 或美观格式打印每个事件的关联上下文,并且不关注跨度时间 这可以在单独的层中完成,例如 opentelemetry,如果需要的话

详细的跟踪文档在此.

您可以这样编写您的第一个跟踪

traceon::on();
traceon::info!("a simple message");
12:12:37 INFO a simple message

以捕获函数中使用的参数

#[traceon::instrument]
fn add(a: i32, b: i32) {
    traceon::info!("result = {}", a + b);
}

fn main() {
    traceon::on();
    add(5, 10);
}

输出

09:52:33 INFO result = 15
    a:    5
    b:    10
    span: add

要使用 JSON 输出

traceon::json();

每个事件输出一行扁平化 JSON,此示例已美化

{
  "time": "2023-01-01T05:28:13.447Z",
  "level": "INFO",
  "message": "result: 15",
  "a": 5,
  "b": 10,
  "span": "add"
}

如果您想更改任何默认设置,可以通过构建器进行配置

use traceon::{Case, JoinFields, LevelFormat, SecondsFormat, SpanFormat, TimeFormat, TimeZone, info};
fn main() {
    traceon::builder()
        // Add field with source code filename and line number e.g. src/main.rs:10
        .file()
        // Add field with target and module path e.g. mybinary::mymodule::submodule
        .module()
        // Turn off field with joined span name where the event occured e.g. parentspan::childspan
        .span(SpanFormat::None)
        // If the time is recorded in local system timezone or UTC
        .timezone(TimeZone::UTC)
        // Change the formatting of the time to RFC3339 with Seconds and Zulu
        .time(TimeFormat::RFC3339Options(SecondsFormat::Secs, true))
        // Change the casing of all the key names e.g. `camelCase` to `snake_case`
        .case(Case::Snake)
        // The characters used to concatenate field values that repeat in nested spans. Defaults to overwrite.
        .join_fields(JoinFields::All("::"))
        // Turn on json formatting instead of pretty output
        .json()
        // Change level value formatting to numbers for easier filtering
        // trace: 10
        // debug: 20
        // info:  30
        // warn:  40
        // error: 50
        .level(LevelFormat::Number)
        // Put anything that implements `Write` here to redirect output
        .writer(std::io::stderr())
        // on() activates it globally on all threads and panic if a global subcriber is already set
        // try_on() will return an error if a global subscriber is already set
        // on_thread() will return a guard so the subscriber will only be active in the current scope and thread
        .on();

    info!("a simple message");
}

输出

{
    "timestamp": "2023-01-01T03:26:48Z",
    "level": 30,
    "module": "builder",
    "file": "examples/builder.rs:27",
    "message": "a simple message"
}

env-filter 默认在 info 级别使用,用于在 debugtrace 级别过滤任何消息,要更改级别,您可以设置环境变量,例如 RUST_LOG=warn,这将过滤掉 info 级别的消息,或者 RUST_LOG=trace 以显示所有事件。有关详细信息,请参阅 此处

示例

#[instrument] 宏

您可以使用 traceon::instrument 宏,在异步函数和普通函数中捕获每个函数调用所使用的参数。

use traceon::{instrument, info};

#[instrument]
async fn add(a: i32, b: i32) {
    info!("result: {}", a + b);
}

#[tokio::main]
async fn main() {
    traceon::builder().on();
    add(5, 10).await;
}
06:16:30 INFO result: 15
    a:    5
    b:    10
    span: add

仪器特质

如果您需要向异步函数添加额外的上下文,可以创建一个跨度并对其进行仪器化。

use tracing::{Instrument, info};

async fn add(a: i32, b: i32) {
    tracing::info!("result: {}", a + b);
}

#[tokio::main]
async fn main() {
    traceon::builder().on();
    let span = tracing::info_span!("math_functions", package_name = env!("CARGO_PKG_NAME",));
    add(5, 10).instrument(span).await;
}
06:18:55 INFO result: 15
    package_name: traceon
    span:         math_functions

上面的 package_name 来自编译时在 Cargo.toml 中,并在运行时保存到二进制文件中。

[package]
name = "traceon"

进入的跨度

这会创建一个跨度并返回一个保护器,只要这个保护器在作用域内,跨度就会处于活动状态。

fn add(a: i32, b: i32) {
    tracing::info!("result: {}", a + b);
}

fn main() {
    traceon::builder().on();
    let _guard = tracing::info_span!("math", package_name = env!("CARGO_PKG_NAME")).entered();
    add(5, 10);
}
06:26:21 INFO result: 15
    package_name: traceon
    span:         math

警告 如果 add() 是一个 async fn,保持保护器会导致内存泄漏和信息丢失,您必须使用上面的 traceon::instrument 宏或特质,有关详细信息,请参阅 此处

注意 记住不要在保持保护器时调用 .await

嵌套跨度

默认情况下,跨度名称将以字符 :: 连接,用于嵌套跨度。

use traceon::{instrument, info};

#[instrument]
fn add(a: i32, b: i32) {
    tracing::info!("result: {}", a + b);
}

fn main() {
    traceon::builder().on();
    let _guard = tracing::info_span!("math", package_name = env!("CARGO_PKG_NAME")).entered();
    add(5, 10);
}
06:33:57 INFO result: 15
    a:            5
    b:            10
    package_name: traceon
    span:         math::add

您可以将其设置为覆盖,如果您愿意的话。

use traceon::SpanFormat;
traceon::builder().span(SpanFormat::Overwrite).on();
06:36:00 INFO result: 15
    a:            5
    b:            10
    package_name: traceon
    span:         add

默认情况下,如果嵌套跨度具有相同的字段名称,则所有其他字段都会覆盖,您可以连接所有字段或特定字段,如果您愿意的话(这仅影响文本值)。

use traceon::{JoinFields, info, info_span};
traceon::builder()
    .join_fields(JoinFields::Some("||", &["field_b"]))
    .on();

let _span_1 = info_span!("span_1", field_a = "original", field_b = "original").entered();
let _span_2 = info_span!("span_2", field_a = "changed", field_b = "changed").entered();

info!("testing field join");

输出

12:44:12 INFO testing field join
    field_a: changed
    field_b: original||changed
    span:    span_1::span_1

更改键的大小写

通常,您会消费不同的 crate,这些 crate 实现了自己的跟踪,并且您需要它们的键匹配某种格式,此示例还演示了如何使用 on_thread() 使用 traceon::instrument,它返回一个保护器,以便订阅者仅在当前线程上运行,当保护器释放时将关闭。

examples/casing.rs

use traceon::{Case, Level, event};

fn main() {
    let _guard = traceon::builder().case(Case::Pascal).on_thread();
    event!(
        Level::INFO,
        message = "PascalCase",
        PascalCase = "test",
        camelCase = "test",
        snake_case = "test",
        SCREAMING_SNAKE_CASE = "test",
    );

    let _guard = traceon::builder().case(Case::Camel).on_thread();
    event!(
        Level::INFO,
        message = "camelCase",
        PascalCase = "test",
        camelCase = "test",
        snake_case = "test",
        SCREAMING_SNAKE_CASE = "test",
    );

    let _guard = traceon::builder().case(Case::Snake).on_thread();
    event!(
        Level::INFO,
        message = "snake_case",
        PascalCase = "test",
        camelCase = "test",
        snake_case = "test",
        SCREAMING_SNAKE_CASE = "test",
    );
}

输出

10:06:38 INFO PascalCase
    CamelCase:          test
    PascalCase:         test
    ScreamingSnakeCase: test
    SnakeCase:          test

10:06:38 INFO camelCase
    camelCase:          test
    pascalCase:         test
    screamingSnakeCase: test
    snakeCase:          test

10:06:38 INFO snake_case
    camel_case:           test
    pascal_case:          test
    screaming_snake_case: test
    snake_case:           test

事件

tracing::event! 允许您在不创建跨度的情况下向消息添加字段,只需记住将级别(例如 tracing::Level::INFO)作为第一个参数即可,这也展示了如何在事件中创建自定义消息,以及如何输出 Debug 实现。

use tracing::{Level, event};

fn main() {
    traceon::builder().on();

    event!(
        Level::INFO,
        event_example = "add field and log it without a span"
    );

    let vector = vec![10, 15, 20];
    event!(
        Level::WARN,
        message = "overwrite message, and debug a vector",
        ?vector,
    );
}
06:47:00 INFO event triggered
    event_example: add field and log it without a span

06:47:00 WARN overwrite message, and debug a vector
    vector: [10, 15, 20]

写入文件

如果您想将日志写入文件而不是标准输出,只需将依赖项添加到 Cargo.toml

[dependencies]
tracing-appender = "0.2.2"

并通过构建器初始化它

examples/file_writer.rs

use traceon::info;

let file_appender = tracing_appender::rolling::hourly("./", "test.log");
traceon::builder().json().writer(file_appender).on();
info!("wow cool!");

写入器接受实现 Write 特质的任何内容,如果您想保持一个包裹在 ArcMutex 中的缓冲区,构建器上有 buffer() 方法。

与其他层组合

您还可以使用格式化层与其他跟踪层一起使用,因为您越来越熟悉跟踪生态系统,例如,添加 opentelemetry。

use opentelemetry::trace::TracerProvider as _;
use opentelemetry_sdk::trace::TracerProvider;
use opentelemetry_stdout as stdout;
use tracing::{info, span};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::Registry;

fn main() {
    let provider = TracerProvider::builder()
        .with_simple_exporter(stdout::SpanExporter::default())
        .build();

    let tracer = provider.tracer("readme_example");
    let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
    // Compose opentelemetry with traceon
    let subscriber = Registry::default().with(telemetry).with(traceon::builder());

    tracing::subscriber::with_default(subscriber, || {
        let root = span!(tracing::Level::TRACE, "app_start", work_units = 2);
        let _enter = root.enter();

        info!(
            "This will log the full span data to stdout via opentelemetry \
            along with the simplified and flattened data using traceon"
        );
    });
}

性能

此 crate 使用来自: LukeMathWalker/tracing-bunyan-formatter 的想法,将访问过的跨度中的字段存储在 HashMap 中,而不是更适合扁平化字段的 BTreeMap,这导致与 tracing-subscriber 中的 json 格式化器非常相似的性能。

将日志记录到接收器

traceon: 720 nanoseconds tracing-subscriber: 580 nanoseconds

单位 = 纳秒或秒的十亿分之一

将日志记录到标准输出

traceon: 10 microseconds tracing-subscriber: 10 microseconds

单位 = 微秒或百万分之一秒

嵌套三层并合并字段的span

traceon: 18 nanoseconds tracing-subscriber: 22 nanoseconds

依赖项

~6–18MB
~193K SLoC