Crate minitrace

source ·
Expand description

minitrace is a high-performance, ergonomic, library-level timeline tracing library for Rust.

Unlike most tracing libraries which are primarily designed for instrumenting executables, minitrace also accommodates the need for library instrumentation. It stands out due to its extreme lightweight and fast performance compared to other tracing libraries. Moreover, it has zero overhead when not enabled in the executable, making it a worry-free choice for libraries concerned about unnecessary performance loss.

Getting Started

Libraries

Libraries should include minitrace as a dependency without enabling any extra features.

[dependencies]
minitrace = "0.6"

Libraries can attach their spans to the caller’s span (if caller has set local parent) via the API boundary.

use minitrace::prelude::*;

struct Connection {
    // ...
}

impl Connection {
    #[trace]
    pub fn query(sql: &str) -> Result<QueryResult, Error> {
        // ...
    }
}

Libraries can also create a new trace individually to record their work.

use minitrace::prelude::*;

pub fn send_request(req: HttpRequest) -> Result<(), Error> {
    let root = Span::root("send_request", SpanContext::random());
    let _guard = root.set_local_parent();

    // ...
}

Executables

Executables should include minitrace as a dependency with the enable feature set. To disable minitrace statically, simply don’t set the enable feature.

[dependencies]
minitrace = { version = "0.6", features = ["enable"] }

Executables should initialize a reporter implementation early in the program’s runtime. Span records generated before the implementation is initialized will be ignored. Before terminating, the reporter should be flushed to ensure all span records are reported.

use minitrace::collector::Config;
use minitrace::collector::ConsoleReporter;

fn main() {
    minitrace::set_reporter(ConsoleReporter, Config::default());

    // ...

    minitrace::flush();
}

Key Concepts

minitrace operates through three types: Span, LocalSpan, and Event, each representing a different type of tracing record. The macro trace is available to manage these types automatically. For Future instrumentation, necessary utilities are provided by FutureExt.

Span

A Span represents an individual unit of work. It contains:

  • A name
  • A start timestamp and duration
  • A set of key-value properties
  • A reference to a parent Span

A new Span can be started through Span::root(), requiring the trace id and the parent span id from a remote source. If there’s no remote parent span, the parent span id is typically set to its default value of zero.

Once we have the root Span, we can create a child Span using Span::enter_with_parent(), thereby establishing the reference relationship between the spans.

Span is thread-safe and can be sent across threads.

use minitrace::collector::Config;
use minitrace::collector::ConsoleReporter;
use minitrace::prelude::*;

minitrace::set_reporter(ConsoleReporter, Config::default());

{
    let root_span = Span::root("root", SpanContext::random());

    {
        let child_span = Span::enter_with_parent("a child span", &root_span);

        // ...

        // child_span ends here.
    }

    // root_span ends here.
}

minitrace::flush();

Sometimes, passing a Span through a function to create a child Span can be inconvenient. We can employ a thread-local approach to avoid an explicit argument passing in the function. In minitrace, Span::set_local_parent() and Span::enter_with_local_parent() serve this purpose.

Span::set_local_parent() method sets a local context of the Span for the current thread. Span::enter_with_local_parent() accesses the parent Span from the local context and creates a child Span with it.

use minitrace::prelude::*;

{
    let root_span = Span::root("root", SpanContext::random());
    let _guard = root_span.set_local_parent();

    foo();

    // root_span ends here.
}

fn foo() {
    // The parent of this span is `root`.
    let _child_span = Span::enter_with_local_parent("a child span");

    // ...

    // _child_span ends here.
}

Local Span

In a clear single-thread execution flow, where we can ensure that the Span does not cross threads, meaning:

  • The Span is not sent to or shared by other threads
  • In asynchronous code, the lifetime of the Span doesn’t cross an .await point

we can use LocalSpan as a substitute for Span to effectively reduce overhead and greatly enhance performance.

However, there is a precondition: The creation of LocalSpan must take place within a local context of a Span, which is established by invoking the Span::set_local_parent() method.

If the code spans multiple function calls, this isn’t always straightforward to confirm if the precondition is met. As such, it’s recommended to invoke Span::set_local_parent() immediately after the creation of Span.

After a local context of a Span is set using Span::set_local_parent(), use LocalSpan::enter_with_local_parent() to start a LocalSpan, which then becomes the new local parent.

If no local context is set, the LocalSpan::enter_with_local_parent() will do nothing.

use minitrace::collector::Config;
use minitrace::collector::ConsoleReporter;
use minitrace::prelude::*;

minitrace::set_reporter(ConsoleReporter, Config::default());

{
    let root = Span::root("root", SpanContext::random());
    let _guard = root.set_local_parent();

    {
        // The parent of this span is `root`.
        let _span1 = LocalSpan::enter_with_local_parent("a child span");

        foo();
    }
}

fn foo() {
    // The parent of this span is `span1`.
    let _span2 = LocalSpan::enter_with_local_parent("a child span of child span");
}

minitrace::flush();

Event

Event represents a single point in time where something occurred during the execution of a program.

An Event can be seen as a log record attached to a span.

use minitrace::collector::Config;
use minitrace::collector::ConsoleReporter;
use minitrace::prelude::*;

minitrace::set_reporter(ConsoleReporter, Config::default());

{
    let root = Span::root("root", SpanContext::random());
    let _guard = root.set_local_parent();

    Event::add_to_parent("event in root", &root, || []);
    {
        let _span1 = LocalSpan::enter_with_local_parent("a child span");

        Event::add_to_local_parent("event in span1", || [("key".into(), "value".into())]);
    }
}

minitrace::flush();

Macro

The attribute-macro trace helps to reduce boilerplate.

Note: For successful tracing a function using the trace macro, the function call should occur within a local context of a Span.

For more detailed usage instructions, please refer to trace.

use futures::executor::block_on;
use minitrace::collector::Config;
use minitrace::collector::ConsoleReporter;
use minitrace::prelude::*;

#[trace]
fn do_something(i: u64) {
    std::thread::sleep(std::time::Duration::from_millis(i));
}

#[trace]
async fn do_something_async(i: u64) {
    futures_timer::Delay::new(std::time::Duration::from_millis(i)).await;
}

minitrace::set_reporter(ConsoleReporter, Config::default());

{
    let root = Span::root("root", SpanContext::random());
    let _guard = root.set_local_parent();

    do_something(100);

    block_on(
        async {
            do_something_async(100).await;
        }
        .in_span(Span::enter_with_local_parent("aync_job")),
    );
}

minitrace::flush();

Reporter

Reporter is responsible for reporting the span records to a remote agent, such as Jaeger.

Executables should initialize a reporter implementation early in the program’s runtime. Span records generated before the implementation is initialized will be ignored.

For an easy start, minitrace offers a ConsoleReporter that prints span records to stderr. For more advanced use, crates like minitrace-jaeger, minitrace-datadog, and minitrace-opentelemetry are available.

By default, the reporter is triggered every 500 milliseconds. The reporter can also be triggered manually by calling flush(). See Config for customizing the reporting behavior.

use std::time::Duration;

use minitrace::collector::Config;
use minitrace::collector::ConsoleReporter;

minitrace::set_reporter(
    ConsoleReporter,
    Config::default().batch_report_interval(Duration::from_secs(1)),
);

minitrace::flush();

Performance

minitrace is designed to be fast and lightweight, considering four scenarios:

  • No Tracing: minitrace is not included as dependency in the executable, while the libraries has been instrumented. In this case, it will be completely removed from libraries, causing zero overhead.

  • Sample Tracing: minitrace is enabled in the executable, but only a small portion of the traces are enabled via Span::root(), while the other portion start with placeholders by Span::noop(). The overhead in this case is very small - merely an integer load, comparison, and jump.

  • Full Tracing with Tail Sampling: minitrace is enabled in the executable, and all traces are enabled. However, only a select few abnormal tracing records (e.g., P99) are reported. Normal traces can be dismissed by using Span::cancel() to avoid reporting. This could be useful when you are interested in examining program’s tail latency.

  • Full Tracing: minitrace is enabled in the executable, and all traces are enabled. All tracing records are reported. minitrace performs 10x to 100x faster than other tracing libraries in this case.

Modules

  • Collector and the collected spans.
  • This module provides tools to trace a Future.
  • Non thread-safe span with low overhead.
  • A “prelude” for crates using minitrace.

Macros

  • Get the source file location where the macro is invoked. Returns a &'static str.
  • Get the full path of the function where the macro is invoked. Returns a &'static str.
  • Get the name of the function where the macro is invoked. Returns a &'static str.

Structs

  • An event that represents a single point in time during the execution of a span.
  • A thread-safe span.

Functions

  • Flushes all pending span records to the reporter immediately.
  • Sets the reporter and its configuration for the current application.

Attribute Macros

  • An attribute macro designed to eliminate boilerplate code.