About

This guide introduces emit. You may also want to look at:

What is emit?

emit is a framework for adding diagnostics to your Rust applications with a simple, powerful data model and an expressive syntax inspired by Message Templates.

Diagnostics in emit are represented as events which combine:

  • extent: The point in time when the event occurred, or the span of time for which it was active.
  • template: A user-facing description of the event that supports property interpolation.
  • properties: A bag of structured key-value pairs that capture the context surrounding the event. Properties may be simple primitives like numbers or strings, or arbitrarily complex structures like maps, sequences, or enums.

Using emit's events you can:

  • log structured events.
  • trace function execution and participate in distributed tracing.
  • surface live metrics.
  • build anything you can represent as a time-oriented bag of data.

Who is emit for?

emit is for Rust applications, it's not intended to be used in public libraries. In general, libraries shouldn't use a diagnostics framework anyway, but emit's opinionated data model, use of dependencies, and procedural macros, will likely make it unappealing for Rust libraries.

Design goals

emit's guiding design principle is low ceremony, low cognitive-load. Diagnostics are our primary focus, but they're probably not yours. Configuration should be straightforward, operation should be transparent, and visual noise in instrumented code should be low.

These goals result in some tradeoffs that may affect emit's suitability for your needs:

  • Simplicity over performance. Keeping the impact of diagnostics small is still important, but not at the expense of usability or simplicity.
  • Not an SDK. emit has a hackable API you can tailor to your needs but is also a small, complete, and cohesive set of components for you to use out-of-the-box.

Supported Rust toolchains

emit works on stable versions of Rust, but can provide more accurate compiler messages on nightly toolchains. If you're using rustup, you can easily install a nightly build of Rust from it.

Stability

emit follows the regular semver policy of other Rust libraries with the following additional considerations:

  • Changes to the interpretation of events, such as the use of new extensions, are considered breaking.
  • Breaking changes to emit_core are not planned.
  • Breaking changes to emit itself, its macros, and emitters, may happen infrequently. Major changes to its API are not planned though. We're aware that, as a diagnostics library, you're likely to spread a lot of emit code through your application, so even small changes can have a big impact.

As an application developer, you should be able to rely on the stability of emit to not to get in the way of your everyday programming.

Getting started

Add emit to your Cargo.toml, along with an emitter to write diagnostics to:

[dependencies.emit]
version = "0.11.0-alpha.21"

[dependencies.emit_term]
version = "0.11.0-alpha.21"

Initialize emit at the start of your main.rs using emit::setup(), and ensure any emitted diagnostics are flushed by calling blocking_flush() at the end:

extern crate emit;
extern crate emit_term;

fn main() {
    // Configure `emit` to write events to the console
    let rt = emit::setup()
        .emit_to(emit_term::stdout())
        .init();

    // Your app code goes here

    // Flush any remaining events before `main` returns
    rt.blocking_flush(std::time::Duration::from_secs(5));
}

Start peppering diagnostics through your application with emit's macros.

Logging events

When something of note happens, use debug! or info! to log it:

#![allow(unused)]
fn main() {
extern crate emit;
let user = "user-123";
let item = "product-123";

emit::info!("{user} added {item} to their cart");
}

When something fails, use warn! or error!:

#![allow(unused)]
fn main() {
extern crate emit;
let user = "user-123";
let err = std::io::Error::new(
    std::io::ErrorKind::Other,
    "failed to connect to the remote service",
);

emit::warn!("updating {user} cart failed: {err}");
}

Tracing functions

Add #[span] to a significant function in your application to trace its execution:

#![allow(unused)]
fn main() {
extern crate emit;
#[emit::span("add {item} to {user} cart")]
async fn add_item(user: &str, item: &str) {
    // Your code goes here
}
}

Any diagnostics emitted within a traced function will be correlated with it. Any other traced functions it calls will form a trace hierarchy.

Next steps

To learn more about using emit, see the Producing events section.

To learn emit's architecture in more detail, see the Reference section.

You may also want to explore:

Producing events

This section describes how to use emit to produce diagnostics in your applications. That includes:

Logging

Logs provide immediate feedback on the operation of your applications. Logs let you capture events that are significant in your application's domain, like orders being placed. Logs also help you debug issues in live applications, like lost order confirmation emails.

The emit! macro

In emit, logs are events; a combination of timestamp, message template, and properties. When something significant happens in the execution of your applications, you can log an event for it:

#![allow(unused)]
fn main() {
extern crate emit;
fn confirm_email(user: &str, email: &str) {
    emit::emit!("{user} confirmed {email}");
}
}
Event {
    mdl: "my_app",
    tpl: "{user} confirmed {email}",
    extent: Some(
        "2024-10-01T22:21:08.136228524Z",
    ),
    props: {
        "email": "user-123@example.com",
        "user": "user-123",
    },
}

emit also defines macros for emitting events at different levels for filtering:

  • debug! for events supporting live debugging.
  • info! for most informative events.
  • warn! for errors that didn't cause the calling operation to fail.
  • error! for errors that caused the calling operation to fail.

See Levels for details.


an example log rendered to the console

An example log produced by emit rendered to the console

Logging data model

The data model of logs is an extension of emit's events. Log events include the following well-known properties:

  • lvl: a severity level assigned to the event.
    • "debug": a weakly informative event for live debugging.
    • "info": an informative event.
    • "warn": a weakly erroneous event for non-critical operations.
    • "error": an erroneous event.
  • err: an error associated with the event.

There's some overlap between the logs data model and other extensions. Span events, for example, also support attaching levels and errors through lvl and err.

Attaching properties to events

Properties can be attached to log events by including them in the message template:

#![allow(unused)]
fn main() {
extern crate emit;
let user = "Rust";

emit::emit!("Hello, {user}");
}
Event {
    mdl: "my_app",
    tpl: "Hello, {user}",
    extent: Some(
        "2024-10-02T21:59:35.084177500Z",
    ),
    props: {
        "user": "Rust",
    },
}

Properties can also be attached after the template:

#![allow(unused)]
fn main() {
extern crate emit;
let user = "Rust";

emit::emit!("Saluten, {user}", lang: "eo");
}
Event {
    mdl: "my_app",
    tpl: "Saluten, {user}",
    extent: Some(
        "2024-10-02T21:59:56.406474900Z",
    ),
    props: {
        "lang": "eo",
        "user": "Rust",
    },
}

See Template syntax and rendering for more details.

Capturing complex values

Properties aren't limited to strings; they can be arbitrarily complex structured values. See the following sections and Value data model for more details.

Using fmt::Debug

If you want to log a type that implements Debug, you can apply the #[as_debug] attribute to it to capture it with its debug format:

#![allow(unused)]
fn main() {
extern crate emit;
#[derive(Debug)]
struct User<'a> {
    name: &'a str,
}

emit::emit!(
    "Hello, {user}",
    #[emit::as_debug]
    user: User {
        name: "Rust",
    }
);
}
Event {
    mdl: "my_app",
    tpl: "Hello, {user}",
    extent: Some(
        "2024-10-02T22:03:23.588049400Z",
    ),
    props: {
        "user": User {
            name: "Rust",
        },
    },
}

Using serde::Serialize

If you want to log a type that implements Serialize, you can apply the #[as_serialize] attribute to it to capture it as a structured value:

#![allow(unused)]
fn main() {
extern crate emit;
#[macro_use] extern crate serde;
#[derive(Serialize)]
struct User<'a> {
    name: &'a str,
}

emit::emit!(
    "Hello, {user}",
    #[emit::as_serde]
    user: User {
        name: "Rust",
    }
);
}
Event {
    mdl: "my_app",
    tpl: "Hello, {user}",
    extent: Some(
        "2024-10-02T22:05:05.258099900Z",
    ),
    props: {
        "user": User {
            name: "Rust",
        },
    },
}

Attaching errors to events

In Rust, errors are typically communicated through the Error trait. If you attach a property with the err well-known property to an event, it will automatically try capture it using its Error implementation:

#![allow(unused)]
fn main() {
extern crate emit;
fn write_to_file(bytes: &[u8]) -> std::io::Result<()> { Err(std::io::Error::new(std::io::ErrorKind::Other, "the file is in an invalid state")) }
if let Err(err) = write_to_file(b"Hello") {
    emit::warn!("file write failed: {err}");
}
}
Event {
    mdl: "emit_sample",
    tpl: "file write failed: {err}",
    extent: Some(
        "2024-10-02T21:14:40.566303000Z",
    ),
    props: {
        "err": Custom {
            kind: Other,
            error: "the file is in an invalid state",
        },
        "lvl": warn,
    },
}

Emitters may treat the err property specially when receiving diagnostic events, such as by displaying them more prominently.

You can also use the #[as_error] attribute on a property to capture it using its Error implementation.

The #[span] macro can automatically capture errors from fallible functions. See Fallible functions for details.

Log levels

Levels describe the significance of a log event in a coarse-grained way that's easy to organize and filter on. emit doesn't bake in the concept of log levels directly, but supports them through the lvl well-known property. emit defines four well-known levels; a strong and a weak variant of informative and erroneous events:

  • "debug": for events supporting live debugging. These events are likely to be filtered out, or only retained for a short period.
  • "info": for most events. These events are the core diagnostics of your applications that give you a good picture of what's happening.
  • "warn": for recoverable or non-esential erroneous events. They may help explain some unexpected behavior or point to issues to investigate.
  • "error": for erroneous events that cause degraded behavior and need to be investigated.

When emitting events, you can use a macro corresponding to a given level to have it attached automatically:

emit's levels are intentionally very coarse-grained and aren't intended to be extended. You can define your own levelling scheme in your applications if you want.

Ambient context

emit supports enriching events automatically with properties from the ambient environment.

Using the #[span] macro

The most straightforward way to add ambient context is using the #[span] macro. Any properties captured by the span will also be added to events emitted during its execution:

#![allow(unused)]
fn main() {
extern crate emit;
#[emit::span("greet {user}", lang)]
fn greet(user: &str, lang: &str) {
    match lang {
        "el" => emit::emit!("Γεια σου, {user}"),
        "en" => emit::emit!("Hello, {user}"),
        "eo" => emit::emit!("Saluton, {user}"),
/*
        ..
*/    _ => (),
    }
}

greet("Rust", "eo");
}
Event {
    mdl: "my_app",
    tpl: "Saluton, {user}",
    extent: Some(
        "2024-10-02T20:53:29.580999000Z",
    ),
    props: {
        "user": "Rust",
        "span_id": b26bfe938b77eb19,
        "lang": "eo",
        "user": "Rust",
        "trace_id": 7fc2efc824915dc180c29a79af358e78,
    },
}

Note the presence of the lang property on the events produced by emit!. They appear because they're added to the ambient context by the #[span] attribute on greet().

See Tracing for more details.

Manually

Ambient context can be worked with directly. emit stores its ambient context in an implementation of the Ctxt trait. Properties in the ambient context can be added or removed using Frames. You may want to work with context directly when you're not trying to produce spans in a distributed trace, or when your application doesn't have a single point where attributes could be applied to manipulate ambient context.

When converted to use ambient context manually, the previous example looks like this:

#![allow(unused)]
fn main() {
extern crate emit;
fn greet(user: &str, lang: &str) {
    // Get a frame over the amient context that pushes the `lang` property
    let mut frame = emit::Frame::push(
        emit::ctxt(),
        emit::props! {
            lang,
        },
    );

    // Make the frame active
    // While this guard is in scope the `lang` property will be present
    // When this guard is dropped the `lang` property will be removed
    // Frames may be entered and exited multiple times
    let _guard = frame.enter();

    // The rest of the function proceeds as normal
    match lang {
        "el" => emit::emit!("Γεια σου, {user}"),
        "en" => emit::emit!("Hello, {user}"),
        "eo" => emit::emit!("Saluton, {user}"),
/*
        ..
*/     _ => (),
    }
}

greet("Rust", "eo");
}
Event {
    mdl: "my_app",
    tpl: "Saluton, {user}",
    extent: Some(
        "2024-10-02T21:01:50.534810000Z",
    ),
    props: {
        "user": "Rust",
        "lang": "eo",
    },
}

When using ambient context manually, it's important that frames are treated like a stack. They need to be exited in the opposite order they were entered in.

Tracing

When your application executes key operations, you can emit span events that cover the time they were active. Any other operations involved in that execution, or any other events emitted during it, will be correlated through identifiers to form a hierarchical call tree. Together, these events form a trace, which in distributed systems can involve operations executed by other services. Traces are a useful way to build a picture of service dependencies in distributed applications, and to identify performance problems across them.

The #[span] macro

emit supports tracing operations through attribute macros on functions. These macros use the same syntax as those for emitting regular events:

#![allow(unused)]
fn main() {
extern crate emit;
#[emit::span("wait a bit", sleep_ms)]
fn wait_a_bit(sleep_ms: u64) {
    std::thread::sleep(std::time::Duration::from_millis(sleep_ms))
}

wait_a_bit(1200);
}
Event {
    mdl: "my_app",
    tpl: "wait a bit",
    extent: Some(
        "2024-04-27T22:40:24.112859000Z".."2024-04-27T22:40:25.318273000Z",
    ),
    props: {
        "evt_kind": span,
        "span_name": "wait a bit",
        "span_id": 71ea734fcbb4dc41,
        "trace_id": 6d6bb9c23a5f76e7185fb3957c2f5527,
        "sleep_ms": 1200,
    },
}

When the annotated function returns, a span event for its execution is emitted. The extent of a span event is a range, where the start is the time the function began executing, and the end is the time the function returned.

Asynchronous functions are also supported:

#![allow(unused)]
fn main() {
extern crate emit;
async fn sleep<T>(_: T) {}
async fn _main() {
#[emit::span("wait a bit", sleep_ms)]
async fn wait_a_bit(sleep_ms: u64) {
    sleep(std::time::Duration::from_millis(sleep_ms)).await
}

wait_a_bit(1200).await;
}
}

emit also defines macros for emitting spans at different levels for filtering:

The level of a span may also depend on its execution. See Fallible functions and Manual span completion for details.


an example trace in Zipkin

A trace produced by this example application in Zipkin.

Tracing data model

The data model of spans is an extension of emit's events. Span events include the following well-known properties:

  • evt_kind: with a value of "span" to indicate that the event is a span.
  • span_name: a name for the operation the span represents. This defaults to the template.
  • span_id: an identifier for this specific invocation of the operation.
  • parent_id: the span_id of the operation that invoked this one.
  • trace_id: an identifier shared by all events in a distributed trace. A trace_id is assigned by the first operation.

Attaching properties to spans

Properties added to the span macros are added to an ambient context and automatically included on any events emitted within that operation:

#![allow(unused)]
fn main() {
extern crate emit;
#[emit::span("wait a bit", sleep_ms)]
fn wait_a_bit(sleep_ms: u64) {
    std::thread::sleep(std::time::Duration::from_millis(sleep_ms));

    emit::emit!("waiting a bit longer");

    std::thread::sleep(std::time::Duration::from_millis(sleep_ms));
}
}
Event {
    mdl: "my_app",
    tpl: "waiting a bit longer",
    extent: Some(
        "2024-04-27T22:47:34.780288000Z",
    ),
    props: {
        "trace_id": d2a5e592546010570472ac6e6457c086,
        "sleep_ms": 1200,
        "span_id": ee9fde093b6efd78,
    },
}
Event {
    mdl: "my_app",
    tpl: "wait a bit",
    extent: Some(
        "2024-04-27T22:47:33.574839000Z".."2024-04-27T22:47:35.985844000Z",
    ),
    props: {
        "evt_kind": span,
        "span_name": "wait a bit",
        "trace_id": d2a5e592546010570472ac6e6457c086,
        "sleep_ms": 1200,
        "span_id": ee9fde093b6efd78,
    },
}

Any operations started within a span will inherit its identifiers:

#![allow(unused)]
fn main() {
extern crate emit;
#[emit::span("outer span", sleep_ms)]
fn outer_span(sleep_ms: u64) {
    std::thread::sleep(std::time::Duration::from_millis(sleep_ms));

    inner_span(sleep_ms / 2);
}

#[emit::span("inner span", sleep_ms)]
fn inner_span(sleep_ms: u64) {
    std::thread::sleep(std::time::Duration::from_millis(sleep_ms));
}
}
Event {
    mdl: "my_app",
    tpl: "inner span",
    extent: Some(
        "2024-04-27T22:50:50.385706000Z".."2024-04-27T22:50:50.994509000Z",
    ),
    props: {
        "evt_kind": span,
        "span_name": "inner span",
        "trace_id": 12b2fde225aebfa6758ede9cac81bf4d,
        "span_parent": 23995f85b4610391,
        "sleep_ms": 600,
        "span_id": fc8ed8f3a980609c,
    },
}
Event {
    mdl: "my_app",
    tpl: "outer span",
    extent: Some(
        "2024-04-27T22:50:49.180025000Z".."2024-04-27T22:50:50.994797000Z",
    ),
    props: {
        "evt_kind": span,
        "span_name": "outer span",
        "sleep_ms": 1200,
        "span_id": 23995f85b4610391,
        "trace_id": 12b2fde225aebfa6758ede9cac81bf4d,
    },
}

Notice the span_parent of inner_span is the same as the span_id of outer_span. That's because inner_span was called within the execution of outer_span.

Capturing complex values

Properties aren't limited to strings; they can be arbitrarily complex structured values. See the following sections and Value data model for more details.

Using fmt::Debug

If you want to log a type that implements Debug, you can apply the #[as_debug] attribute to it to capture it with its debug format:

#![allow(unused)]
fn main() {
extern crate emit;
#[derive(Debug)]
struct User<'a> {
    name: &'a str,
}

#[emit::span("greet {user}", #[emit::as_debug] user)]
fn greet(user: &User) {
    println!("Hello, {}", user.name);
}
}

Using serde::Serialize

If you want to log a type that implements Serialize, you can apply the #[as_serialize] attribute to it to capture it as a structured value:

#![allow(unused)]
fn main() {
extern crate emit;
#[macro_use] extern crate serde;
#[derive(Serialize)]
struct User<'a> {
    name: &'a str,
}

#[emit::span("greet {user}", #[emit::as_serde] user)]
fn greet(user: &User) {
    println!("Hello, {}", user.name);
}
}

Tracing fallible functions

The ok_lvl and err_lvl control parameters can be applied to span macros to assign a level based on whether the annotated function returned Ok or Err:

#![allow(unused)]
fn main() {
extern crate emit;
#[emit::span(
    ok_lvl: emit::Level::Info,
    err_lvl: emit::Level::Error,
    "wait a bit",
    sleep_ms,
)]
fn wait_a_bit(sleep_ms: u64) -> Result<(), std::io::Error> {
    if sleep_ms > 500 {
        return Err(std::io::Error::new(std::io::ErrorKind::Other, "the wait is too long"));
    }

    std::thread::sleep(std::time::Duration::from_millis(sleep_ms));

    Ok(())
}

let _ = wait_a_bit(100);
let _ = wait_a_bit(1200);
}
Event {
    mdl: "my_app",
    tpl: "wait a bit",
    extent: Some(
        "2024-06-12T21:43:03.556361000Z".."2024-06-12T21:43:03.661164000Z",
    ),
    props: {
        "lvl": info,
        "evt_kind": span,
        "span_name": "wait a bit",
        "trace_id": 6a3fc0e46bfa1da71537e39e3bf1942c,
        "span_id": f5bcc5821c6c3227,
        "sleep_ms": 100,
    },
}
Event {
    mdl: "my_app",
    tpl: "wait a bit",
    extent: Some(
        "2024-06-12T21:43:03.661850000Z".."2024-06-12T21:43:03.661986000Z",
    ),
    props: {
        "lvl": error,
        "err": Custom {
            kind: Other,
            error: "the wait is too long",
        },
        "evt_kind": span,
        "span_name": "wait a bit",
        "trace_id": 3226b70b45ff90f92f4feccee4325d4d,
        "span_id": 3702ba2429f9a7b7,
        "sleep_ms": 1200,
    },
}

Mapping error types

Attaching errors to spans requires they're either &str, &(dyn std::error::Error + 'static), or impl std::error::Error. Error types like anyhow::Error don't satisfy these requirements so need to be mapped.

The err control parameter can be used to map the error type of a fallible span into one that can be captured:

#![allow(unused)]
fn main() {
extern crate emit;
extern crate anyhow;
#[emit::span(
    ok_lvl: emit::Level::Info,
    err: anyhow_err,
    "wait a bit",
    sleep_ms,
)]
fn wait_a_bit(sleep_ms: u64) -> Result<(), anyhow::Error> {
    if sleep_ms > 500 {
        return Err(anyhow::Error::msg("the wait is too long"));
    }

    std::thread::sleep(std::time::Duration::from_millis(sleep_ms));

    Ok(())
}

fn anyhow_err(err: &anyhow::Error) -> &(dyn std::error::Error + 'static) {
    err.as_ref()
}

let _ = wait_a_bit(100);
let _ = wait_a_bit(1200);
}

The err control parameter accepts an expression that implements Fn(&E) -> U, which can either be provided as a closure inline, or as an external function like anyhow_err in the above example.

If your error type can't be mapped, you can also fall back to just providing a static string description as the error value:

#![allow(unused)]
fn main() {
extern crate emit;
#[emit::span(
    ok_lvl: emit::Level::Info,
    err: (|_| "wait a bit failed"),
    "wait a bit",
    sleep_ms,
)]
fn wait_a_bit(sleep_ms: u64) -> Result<(), ()> {
    if sleep_ms > 500 {
        return Err(());
    }

    std::thread::sleep(std::time::Duration::from_millis(sleep_ms));

    Ok(())
}

let _ = wait_a_bit(100);
let _ = wait_a_bit(1200);
}

Manual span creation

Span events may be created manually without using the #[span] attribute:

#![allow(unused)]
fn main() {
extern crate emit;
// Create a new span context that is a child of the current one
// This context can be freely copied or stored elsewhere
let ctxt = emit::SpanCtxt::current(emit::ctxt())
    .new_child(emit::rng());

// Push the span onto the current context when you're about to execute
// some code within it
ctxt.push(emit::ctxt())
    .call(move || {
        let timer = emit::Timer::start(emit::clock());

        // Your code goes here
        let sleep_ms = 1200;
        std::thread::sleep(std::time::Duration::from_millis(sleep_ms));

        // Make sure you complete the span in the frame.
        // This is especially important for futures, otherwise the span may
        // complete before the future does
        emit::emit!(
            extent: timer,
            "wait a bit",
            evt_kind: "span",
            sleep_ms,
        );
    });
}

Trace and span ids don't need to be managed by emit if you have another scheme in mind. In these cases, they can be attached as regular properties to the span event:

#![allow(unused)]
fn main() {
extern crate emit;
let timer = emit::Timer::start(emit::clock());

// Your code goes here
let sleep_ms = 1200;
std::thread::sleep(std::time::Duration::from_millis(sleep_ms));

emit::emit! {
    extent: timer,
    "wait a bit",
    evt_kind: "span",
    trace_id: "4bf92f3577b34da6a3ce929d0e0e4736",
    span_id: "00f067aa0ba902b7",
    sleep_ms,
}
}

Note that when emitting spans as regular events that you still thread the trace context around somehow, otherwise other events emitted within its execution won't be correlated with it.

Manual span completion

The guard control parameter can be applied to span macros to bind an identifier in the body of the annotated function for the [Span] that's created for it. This span can be completed manually, changing properties of the span along the way:

#![allow(unused)]
fn main() {
extern crate emit;
#[emit::span(guard: span, "wait a bit", sleep_ms)]
fn wait_a_bit(sleep_ms: u64) {
    std::thread::sleep(std::time::Duration::from_millis(sleep_ms));

    if sleep_ms > 500 {
        span.complete_with(|span| {
            emit::warn!(
                when: emit::filter::always(),
                evt: span,
                "wait a bit took too long",
            );
        });
    }
}

wait_a_bit(100);
wait_a_bit(1200);
}
Event {
    mdl: "my_app",
    tpl: "wait a bit",
    extent: Some(
        "2024-04-28T21:12:20.497595000Z".."2024-04-28T21:12:20.603108000Z",
    ),
    props: {
        "evt_kind": span,
        "span_name": "wait a bit",
        "trace_id": 5b9ab977a530dfa782eedd6db08fdb66,
        "sleep_ms": 100,
        "span_id": 6f21f5ddc707f730,
    },
}
Event {
    mdl: "my_app",
    tpl: "wait a bit took too long",
    extent: Some(
        "2024-04-28T21:12:20.603916000Z".."2024-04-28T21:12:21.808502000Z",
    ),
    props: {
        "evt_kind": span,
        "span_name": "wait a bit",
        "lvl": warn,
        "trace_id": 9abad69ac8bf6d6ef6ccde8453226aa3,
        "sleep_ms": 1200,
        "span_id": c63632332de89ac3,
    },
}

Take care when completing spans manually that they always match the configured filter. This can be done using the when control parameter like in the above example. If a span is created it must be emitted, otherwise the resulting trace will be incomplete.

Propagating span context across threads

Ambient span properties are not shared across threads by default. This context needs to be fetched and sent across threads manually:

#![allow(unused)]
fn main() {
extern crate emit;
fn my_operation() {}
std::thread::spawn({
    let ctxt = emit::Frame::current(emit::ctxt());

    move || ctxt.call(|| {
        // Your code goes here
    })
});
}

This same process is also needed for async code that involves thread spawning:

extern crate emit;
mod tokio { pub fn spawn(_: impl std::future::Future) {} }
fn main() {
tokio::spawn(
    emit::Frame::current(emit::ctxt()).in_future(async {
        // Your code goes here
    }),
);
}

Async functions that simply migrate across threads in work-stealing runtimes don't need any manual work to keep their context across those threads.

Propagating span context across services

Span context can be used in distributed applications to correlate their operations together. When services call eachother, they propagate their span context to the callee so it can act as if it were part of that context instead of generating its own. That just makes sure trace ids and span parents line up.

Propagation and sampling are tied together. If a service decides not to sample a given trace then it must propagate that decision to downstream services. Otherwise you'll end up with a broken trace.

emit supports span context propagation via W3C traceparents using emit_traceparent or the OpenTelemetry SDK.

Using emit_traceparent for propagation

emit_traceparent is a library that implements trace sampling and propagation.

When an incoming request arrives, you can push the incoming traceparent onto the current context:

#![allow(unused)]
fn main() {
extern crate emit;
extern crate emit_traceparent;
// 1. Pull the incoming traceparent
//    If the request doesn't specify one then use an empty sampled context
let traceparent = emit_traceparent::Traceparent::try_from_str("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
    .unwrap_or_else(|_| emit_traceparent::Traceparent::current());

// 2. Push the traceparent onto the context and execute your handler within it
traceparent.push().call(handle_request);

#[emit::span("incoming request")]
fn handle_request() {
    // Your code goes here
}
}
Event {
    mdl: "my_app",
    tpl: "incoming request",
    extent: Some(
        "2024-10-16T10:04:24.783410472Z".."2024-10-16T10:04:24.783463852Z",
    ),
    props: {
        "evt_kind": span,
        "span_name": "incoming request",
        "trace_id": 4bf92f3577b34da6a3ce929d0e0e4736,
        "span_id": d6ae3ee046c529d9,
        "span_parent": 00f067aa0ba902b7,
    },
}

When making outbound requests, you can pull the traceparent from the current context and format it as a header:

#![allow(unused)]
fn main() {
extern crate emit_traceparent;
use std::collections::HashMap;
let mut headers = HashMap::<String, String>::new();

// 1. Get the current traceparent
let traceparent = emit_traceparent::Traceparent::current();

if traceparent.is_valid() {
    // 2. Add the traceparent to the outgoing request
    headers.insert("traceparent".into(), traceparent.to_string());
}
}

Using the OpenTelemetry SDK for propagation

If you're using the OpenTelemetry SDK with emit_opentelemetry, it will handle propagation for you.

Manual propagation

When an incoming request arrives, you can push the trace and span ids onto the current context:

#![allow(unused)]
fn main() {
extern crate emit;
// Parsed from the incoming call
let trace_id = "12b2fde225aebfa6758ede9cac81bf4d";
let span_id = "23995f85b4610391";

let frame = emit::Frame::push(emit::ctxt(), emit::props! {
    trace_id,
    span_id,
});

frame.call(handle_request);

#[emit::span("incoming request")]
fn handle_request() {
    // Your code goes here
}
}
Event {
    mdl: "my_app",
    tpl: "incoming request",
    extent: Some(
        "2024-04-29T05:37:05.278488400Z".."2024-04-29T05:37:05.278636100Z",
    ),
    props: {
        "evt_kind": span,
        "span_name": "incoming request",
        "span_parent": 23995f85b4610391,
        "trace_id": 12b2fde225aebfa6758ede9cac81bf4d,
        "span_id": 641a578cc05c9db2,
    },
}

This pattern of pushing the incoming trace and span ids onto the context and then immediately calling a span annotated function ensures the incoming span_id becomes the span_parent in the events emitted by your application, without emitting a span event for the calling service itself.

When making outbound requests, you can pull the trace and span ids from the current context and format them as needed:

#![allow(unused)]
fn main() {
extern crate emit;
use emit::{Ctxt, Props};

let (trace_id, span_id) = emit::ctxt().with_current(|props| {
    (
        props.pull::<emit::TraceId, _>(emit::well_known::KEY_TRACE_ID),
        props.pull::<emit::SpanId, _>(emit::well_known::KEY_SPAN_ID),
    )
});

if let (Some(trace_id), Some(span_id)) = (trace_id, span_id) {
    // Added to the outgoing call
}
}

Sampling and filtering traces

Sampling is a tool to help limit the volume of ingested trace data. It's typically applied when a trace begins by making an upfront decision about whether to produce and/or emit the trace. This is usually called "head sampling" and is limited to probablistic methods. Tail sampling, or deciding whether to ingest a trace after it's completed is much harder to implement, because there's no absolute way to know when a particular trace is finished, or how long it will take.

Sampling and propagation are tied together. If a service decides not to sample a given trace then it must propagate that decision to downstream services. Otherwise you'll end up with a broken trace.

Using emit_traceparent for sampling

emit_traceparent is a library that implements trace sampling and propagation. Using setup_with_sampler, you can configure emit with a function that's run at the start of each trace to determine whether to emit it or not. Any other diagnostics produced within an unsampled trace will be discarded along with it.

This example is a simple sampler that includes one in every 10 traces:

extern crate emit;
extern crate emit_term;
extern crate emit_traceparent;
use std::sync::atomic::{AtomicUsize, Ordering};
fn main() {
    let rt = emit_traceparent::setup_with_sampler({
        let counter = AtomicUsize::new(0);

        move |_| {
            // Sample 1 in every 10 traces
            counter.fetch_add(1, Ordering::Relaxed) % 10 == 0
        }
    })
    .emit_to(emit_term::stdout())
    .init();

    // Your code goes here

    rt.blocking_flush(std::time::Duration::from_secs(5));
}

Using the OpenTelemetry SDK for sampling

If you're using the OpenTelemetry SDK, emit_opentelemetry will respect its sampling.

Manual sampling

You can use emit's filters to implement sampling. This example excludes all diagnostics produced outside of sampled traces, and only includes one in every 10 traces:

extern crate emit;
extern crate emit_term;
use std::sync::atomic::{AtomicUsize, Ordering};
use emit::{Filter, Props};

fn main() {
    let rt = emit::setup()
        .emit_when({
            // Only include events in sampled traces
            let is_in_sampled_trace = emit::filter::from_fn(|evt| {
                evt.props().get("trace_id").is_some() && evt.props().get("span_id").is_some()
            });

            // Only keep 1 in every n traces
            let one_in_n_traces = emit::filter::from_fn({
                let counter = AtomicUsize::new(0);

                move |evt| {
                    // If the event is not a span then include it
                    let Some(emit::Kind::Span) = evt.props().pull::<emit::Kind, _>("evt_kind")
                    else {
                        return true;
                    };

                    // If the span is not the root of a new trace then include it
                    if evt.props().get("span_parent").is_some() {
                        return true;
                    };

                    // Keep 1 in every 10 traces
                    counter.fetch_add(1, Ordering::Relaxed) % 10 == 0
                }
            });

            is_in_sampled_trace.and_when(one_in_n_traces)
        })
        .emit_to(emit_term::stdout())
        .init();

    // Your code goes here

    rt.blocking_flush(std::time::Duration::from_secs(5));
}

Tracing limitations

emit's tracing model is intended to be simple, covering most key use-cases, but has some limitations compared to the OpenTelemetry model:

  • No distinction between sampling and reporting; if a span exists, it's sampled.
  • No span links.
  • No span events.

Metrics

Metrics are an effective approach to monitoring applications at scale. They can be cheap to collect, making them suitable for performance sensitive operations. They can also be compact to report, making them suitable for high-volume scenarios. emit doesn't provide much infrastructure for collecting or sampling metrics. What it does provide is a standard way to report metric samples as events.

A standard kind of metric is a monotonic counter, which can be represented as an atomic integer. In this example, our counter is for the number of bytes written to a file, which we'll call bytes_written. We can report a sample of this counter as an event using some well-known properties:

#![allow(unused)]
fn main() {
extern crate emit;
fn sample_bytes_written() -> usize { 4643 }

let sample = sample_bytes_written();

emit::emit!(
    "{metric_agg} of {metric_name} is {metric_value}",
    evt_kind: "metric",
    metric_agg: "count",
    metric_name: "bytes_written",
    metric_value: sample,
);
}
Event {
    mdl: "my_app",
    tpl: "{metric_agg} of {metric_name} is {metric_value}",
    extent: Some(
        "2024-04-29T10:08:24.780230000Z",
    ),
    props: {
        "evt_kind": metric,
        "metric_name": "bytes_written",
        "metric_agg": "count",
        "metric_value": 4643,
    },
}

an example metric in Prometheus

A metric produced by this example application in Prometheus.

Metrics data model

The data model of metrics is an extension of emit's events. Metric events are points or buckets in a time-series. They don't model the underlying instruments collecting metrics like counters or gauges. They instead model the aggregation of readings from those instruments over their lifetime. Metric events include the following well-known properties:

  • evt_kind: with a value of "metric" to indicate that the event is a metric sample.
  • metric_agg: the aggregation over the underlying data stream that produced the sample.
    • "count": A monotonic sum of 1's for defined values, and 0's for undefined values.
    • "sum": A potentially non-monotonic sum of defined values.
    • "min": The lowest ordered value.
    • "max": The largest ordered value.
    • "last": The most recent value.
  • metric_name: the name of the underlying data stream.
  • metric_value: the sample itself. These values are expected to be numeric.

Attaching properties to metrics

Metric events can carry other properties in addition to their metadata:

#![allow(unused)]
fn main() {
extern crate emit;
emit::emit!(
    "{metric_agg} of {metric_name} is {metric_value}",
    // Metadata
    evt_kind: "metric",
    metric_agg: "count",
    metric_name: "bytes_written",
    metric_value: 591,
    // Additional properties
    file: "./log.1.txt",
    version: "1.2.3-dev",
);
}
Event {
    mdl: "my_app",
    tpl: "{metric_agg} of {metric_name} is {metric_value}",
    extent: Some(
        "2024-04-30T06:53:41.069203000Z",
    ),
    props: {
        "evt_kind": metric,
        "metric_name": "bytes_written",
        "metric_agg": "count",
        "metric_value": 591,
        "file": "./log.1.txt",
        "version": "1.2.3-dev",
    },
}

The Metric type accepts additional properties as an argument to its constructor:

#![allow(unused)]
fn main() {
extern crate emit;
emit::emit!(
    evt: emit::Metric::new(
        // Metadata
        emit::mdl!(),
        "bytes_written",
        "count",
        emit::clock().now(),
        591,
        // Additional properties
        emit::props! {
            file: "./log.1.txt",
            version: "1.2.3-dev",
        },
    ),
);
}

Cumulative metrics

Metric events where their extent is a point are cumulative. Their metric_value is the result of applying their metric_agg over the entire underlying stream up to that point.

The following metric reports the current number of bytes written as 591:

#![allow(unused)]
fn main() {
extern crate emit;
emit::emit!(
    "{metric_agg} of {metric_name} is {metric_value}",
    evt_kind: "metric",
    metric_agg: "count",
    metric_name: "bytes_written",
    metric_value: 591,
);
}
Event {
    mdl: "my_app",
    tpl: "{metric_agg} of {metric_name} is {metric_value}",
    extent: Some(
        "2024-04-30T06:53:41.069203000Z",
    ),
    props: {
        "evt_kind": metric,
        "metric_name": "bytes_written",
        "metric_agg": "count",
        "metric_value": 591,
    },
}

Delta metrics

Metric events where their extent is a time range are deltas. Their metric_value is the result of applying their metric_agg over the underlying stream within the extent.

The following metric reports that the number of bytes written changed by 17 over the last 30 seconds:

#![allow(unused)]
fn main() {
extern crate emit;
let now = emit::clock().now();
let last_sample = now.map(|now| now - std::time::Duration::from_secs(30));

emit::emit!(
    extent: last_sample..now,
    "{metric_agg} of {metric_name} is {metric_value}",
    evt_kind: "metric",
    metric_agg: "count",
    metric_name: "bytes_written",
    metric_value: 17,
);
}
Event {
    mdl: "my_app",
    tpl: "{metric_agg} of {metric_name} is {metric_value}",
    extent: Some(
        "2024-04-30T06:55:59.839770000Z".."2024-04-30T06:56:29.839770000Z",
    ),
    props: {
        "evt_kind": metric,
        "metric_name": "bytes_written",
        "metric_agg": "count",
        "metric_value": 17,
    },
}

Time-series metrics

Metric events where their extent is a time range and the metric_value is an array are a complete time-series. Each element in the array is a bucket in the time-series. The width of each bucket is the length of the extent divided by the number of buckets.

The following metric is for a time-series with 15 buckets, where each bucket covers 1 second:

#![allow(unused)]
fn main() {
extern crate emit;
let now = emit::clock().now();
let last_sample = now.map(|now| now - std::time::Duration::from_secs(15));

emit::emit!(
    extent: last_sample..now,
    "{metric_agg} of {metric_name} is {metric_value}",
    evt_kind: "metric",
    metric_agg: "count",
    metric_name: "bytes_written",
    #[emit::as_value]
    metric_value: [
        0,
        5,
        56,
        0,
        0,
        221,
        7,
        0,
        0,
        5,
        876,
        0,
        194,
        0,
        18,
    ],
);
}
Event {
    mdl: "my_app",
    tpl: "{metric_agg} of {metric_name} is {metric_value}",
    extent: Some(
        "2024-04-30T07:03:07.828185000Z".."2024-04-30T07:03:22.828185000Z",
    ),
    props: {
        "evt_kind": metric,
        "metric_name": "bytes_written",
        "metric_agg": "count",
        "metric_value": [
            0,
            5,
            56,
            0,
            0,
            221,
            7,
            0,
            0,
            5,
            876,
            0,
            194,
            0,
            18,
        ],
    },
}

Reporting metric sources

The Source trait represents some underlying data source that can be sampled to provide Metrics. You can sample sources directly, or combine them into a Reporter to sample all the sources of metrics in your application together:

#![allow(unused)]
fn main() {
extern crate emit;
use emit::metric::{Source as _, Sampler as _};

// Create some metric sources
let source_1 = emit::metric::source::from_fn(|sampler| {
    sampler.metric(emit::Metric::new(
        emit::path!("source_1"),
        "bytes_written",
        emit::well_known::METRIC_AGG_COUNT,
        emit::Empty,
        1,
        emit::Empty,
    ));
});

let source_2 = emit::metric::source::from_fn(|sampler| {
    sampler.metric(emit::Metric::new(
        emit::path!("source_2"),
        "bytes_written",
        emit::well_known::METRIC_AGG_COUNT,
        emit::Empty,
        2,
        emit::Empty,
    ));
});

// Collect them into a reporter
let mut reporter = emit::metric::Reporter::new();

reporter.add_source(source_1);
reporter.add_source(source_2);

// You'll probably want to run this task in your async runtime
// so it observes cancellation etc, but works for this illustration.
std::thread::spawn(move || {
    loop {
        // You could also use `sample_metrics` here and tweak the extents of metrics
        // to ensure they're all aligned together
        reporter.emit_metrics(emit::runtime::shared());

        std::thread::sleep(std::time::Duration::from_secs(30));
    }
});
}

Metrics limitations

emit's metric model is intended to be simple, covering most key use-cases, but has some limitations compared to the OpenTelemetry model:

  • No percentile histograms.
  • Only one metric per event.

Filtering events

emit supports client-side filtering using a Filter.

Setup

Filters are configured through the setup function at the start of your application by calling emit_when:

extern crate emit;
extern crate emit_term;
fn main() {
    let rt = emit::setup()
        // This filter accepts any event with a level over warn
        .emit_when(emit::level::min_filter(emit::Level::Warn))
        .emit_to(emit_term::stdout())
        .init();

    // Your app code goes here

    rt.blocking_flush(std::time::Duration::from_secs(5));
}

Filters can be combined with and_when and or_when:

extern crate emit;
use emit::Filter;

extern crate emit_term;
fn main() {
    let rt = emit::setup()
        // This filter accepts any event with a level over warn or where the module path is `my_module`
        .emit_when(emit::level::min_filter(emit::Level::Warn)
            .or_when(emit::filter::from_fn(|evt| evt.mdl() == "my_module"))
        )
        .emit_to(emit_term::stdout())
        .init();

    // Your app code goes here

    rt.blocking_flush(std::time::Duration::from_secs(5));
}

Wrapping emitters in filters

You can also wrap an emitter in emit_to in a filter:

extern crate emit;
extern crate emit_term;
use emit::Emitter;

fn main() {
    let rt = emit::setup()
        // Wrap the emitter in a filter instead of setting it independently
        .emit_to(emit_term::stdout()
            .wrap_emitter(emit::emitter::wrapping::from_filter(
                emit::level::min_filter(emit::Level::Warn))
            )
        )
        .init();

    // Your app code goes here

    rt.blocking_flush(std::time::Duration::from_secs(5));
}

Wrapping an emitter in a filter is not the same as providing an emitter and filter independently.

When you use emit_when, the filter may be by-passed using the when control parameter on the emit! or #[span] macros to emit an event even if the filter wouldn't match it. However, the filter specified by emit_when doesn't allow you to filter differently if you specify multiple emitters.

When you wrap an emitter in a filter, the filter cannot by by-passed, but each emitter can use its own filter.

Also see Wrapping emitters for more details on wrappings.

Filtering by level

You can use the level::min_filter function to create a filter that matches events based on their level:

extern crate emit;
extern crate emit_term;
fn main() {
    let rt = emit::setup()
        // This filter accepts any event with a level over warn
        .emit_when(emit::level::min_filter(emit::Level::Warn))
        .emit_to(emit_term::stdout())
        .init();

    // Your app code goes here

    rt.blocking_flush(std::time::Duration::from_secs(5));
}

See the crate docs for more details.

Filtering by module

You can use the level::min_by_path_filter function to create a filter that matches events based on their module path and level:

extern crate emit;
extern crate emit_term;
fn main() {
    let rt = emit::setup()
        // This filter accepts any event with a level over warn
        .emit_when(emit::level::min_by_path_filter([
            (emit::path!("noisy_module"), emit::Level::Warn),
            (emit::path!("noisy_module::important_sub_module"), emit::Level::Info),
            (emit::path!("important_module"), emit::Level::Debug),
        ]))
        .emit_to(emit_term::stdout())
        .init();

    // Your app code goes here

    rt.blocking_flush(std::time::Duration::from_secs(5));
}

See the crate docs for more details.

Filtering spans

When you use the #[span] macro, emit will apply the filter to determine whether the span should be created. If the span doesn't match the filter then no trace context will be generated for it. This isn't the same as trace sampling. emit doesn't have the concept of a trace that is not recorded. See Sampling and filtering traces for more details.

Working with events

The result of producing an event is an instance of the Event type. When filtering or emitting events, you may need to inspect or manipulate its fields and properties.

Timestamp

The time-oriented value of an event is called its extent. It can store either a single point-in-time timestamp or a time range. Use the extent() method to get the extent:

extern crate emit;
fn get(evt: emit::Event<impl emit::Props>) {
if let Some(extent) = evt.extent() {
    // The event has an extent, which can be a single timestamp or a time range
}
}
fn main() {}

The returned Extent can then be inspected to get its timestamp as a Timestamp or time range as a Range<Timestamp>.

Extents can always be treated as a point-in-time timestamp:

extern crate emit;
fn get(extent: emit::Extent) {
// If the extent is a point-in-time, this will be the value
// If the extent is a time range, this will be the end bound
let as_timestamp = extent.as_point();
}
fn main() {}

An extent may also be a time range:

extern crate emit;
fn get(extent: emit::Extent) {
if let Some(range) = extent.as_range() {
    // The extent is a time range
} else {
    // The extent is a point-in-time
}
}
fn main() {}

Properties

Event properties are kept in a generic Props collection, which can be accessed through the props() method on the event.

Any data captured on an event, as well as any ambient context at the point it was produced, will be available on its properties. This collection is also where well-known properties for extensions to emit's data model will live.

Finding properties

To find a property value by key, you can call get() on the event properties. If present, the returned Value can be used to further format, serialize, or cast the matching value:

extern crate emit;
fn get(evt: emit::Event<impl emit::Props>) {
use emit::Props;

if let Some(value) = evt.props().get("my_property") {
    // The value is a type-erased object implementing Display/Serialize
}
}
fn main() {}

Casting properties

To find a property and cast it to a concrete type, like a string or i32, you can call pull() on the event properties:

extern crate emit;
fn get(evt: emit::Event<impl emit::Props>) {
use emit::Props;
if let Some::<emit::Str>(value) = evt.props().pull("my_property") {
    // The value is a string
}
}
fn main() {}

Any type implementing the FromValue trait can be pulled as a concrete value from the event properties.

You can also use the cast() method on a value to try cast it to a given type:

extern crate emit;
fn get(evt: emit::Event<impl emit::Props>) {
use emit::Props;
if let Some(value) = evt.props().get("my_property") {
    if let Some(value) = value.cast::<bool>() {
        // The value is a boolean
    }

    // The value is something else
}
}
fn main() {}

Casting to a string

When pulling string values, prefer emit::Str, Cow<str>, or String over &str. Any of the former will successfully cast even if the value needs buffering internally. The latter will only successfully cast if the original value was a borrowed string.

Casting to an error

Property values can contain standard Error values. To try cast a value to an implementation of the Error trait, you can call to_borrowed_error():

extern crate emit;
fn get(evt: emit::Event<impl emit::Props>) {
use emit::Props;
if let Some(err) = evt.props().get("err") {
    if let Some(err) = err.to_borrowed_error() {
        // The value is an error
    }

    // The value is something else
}
}
fn main() {}

You can also pull or cast the value to &(dyn std::error::Error + 'static).

Parsing properties

You can use the parse() method on a value to try parse a concrete type implementing FromStr from it:

extern crate emit;
fn get(evt: emit::Event<impl emit::Props>) {
use emit::Props;
if let Some(value) = evt.props().get("ip") {
    if let Some::<std::net::IpAddr>(ip) = value.parse() {
        // The value is an IP address
    }

    // The value is something else
}
}
fn main() {}

Iterating properties

Use the for_each() method on the event properties to iterate over them. In this example, we iterate over all properties and build a list of their string representations from them:

extern crate emit;
use std::collections::BTreeMap;
use emit::Props;
use std::ops::ControlFlow;
fn get(evt: emit::Event<impl emit::Props>) {
let mut buffered = BTreeMap::<String, String>::new();

evt.props().for_each(|k, v| {
    if !buffered.contains_key(k.get()) {
        buffered.insert(k.into(), v.to_string());
    }

    ControlFlow::Continue(())
});
}
fn main() {}

The for_each method accepts a closure where the inputs are the property key as a Str and value as a Value. The closure returns a ControlFlow to tell the property collection whether it should keep iterating or stop.

Deduplication

Property collections may contain duplicate values, which will likely be yielded when iterating via for_each. Properties are expected to be deduplicated by retaining the first seen for a given key. You can use the dedup() method when working with properties to deduplicate them before yielding, but this may require internal allocation:

extern crate emit;
use std::collections::BTreeMap;
use emit::Props;
use std::ops::ControlFlow;
fn get(evt: emit::Event<impl emit::Props>) {
// This is the same example as before, but we know properties are unique
// thanks to `dedup`, so don't need a unique collection for them
let mut buffered = Vec::<(String, String)>::new();

evt.props().dedup().for_each(|k, v| {
    buffered.push((k.into(), v.to_string()));

    ControlFlow::Continue(())
});
}
fn main() {}

Formatting properties

The Value type always implements Debug and Display with a useful representation, regardless of the kind of value it holds internally.

Serializing properties

When the serde Cargo feature is enabled, the Value type always implements serde::Serialize trait in the most structure-preserving way, regardless of the kind of value it holds internally. The same is true of the sval Cargo feature and sval::Value.

Data model

See Event data model for more details on the shape of emit's events.

Emitting events

Diagnostic events produced by emit are sent to an Emitter. emit provides a few implementations in external libraries you can use in your applications:

Setup

Emitters are configured through the setup function at the start of your application by calling emit_to:

extern crate emit;
extern crate emit_term;
fn main() {
    let rt = emit::setup()
        // Set the emitter
        .emit_to(emit_term::stdout())
        .init();

    // Your app code goes here

    rt.blocking_flush(std::time::Duration::from_secs(5));
}

Once initialized, any subsequent calls to init will panic.

emit_to will replace any previously set emitter during the same setup. You can set multiple emitters by calling and_emit_to:

extern crate emit;
extern crate emit_term;
extern crate emit_file;
fn main() {
    let rt = emit::setup()
        // Set multiple emitters
        .emit_to(emit_term::stdout())
        .and_emit_to(emit_file::set("./target/logs/my_app.txt").spawn())
        .init();

    // Your app code goes here

    rt.blocking_flush(std::time::Duration::from_secs(5));
}

You can map an emitter to a new value by calling map_emitter:

extern crate emit;
extern crate emit_file;
use emit::Emitter;

fn main() {
    let rt = emit::setup()
        // Set the emitter
        .emit_to(emit_file::set("./target/logs/my_app.txt").spawn())
        // Map the emitter, wrapping it with a transformation that
        // sets the module to "new_path". This could be done in the call
        // to `emit_to`, but may be easier to follow when split across two calls
        .map_emitter(|emitter| emitter
            .wrap_emitter(emit::emitter::wrapping::from_fn(|emitter, evt| {
                let evt = evt.with_mdl(emit::path!("new_path"));

                emitter.emit(evt)
            }))
        )
        .init();

    // Your app code goes here

    rt.blocking_flush(std::time::Duration::from_secs(5));
}

Wrapping emitters

Emitters can be treated like middleware using a Wrapping by calling Emitter::wrap_emitter. Wrappings are functions over an Emitter and Event that may transform the event before emitting it, or discard it altogether.

Transforming events with a wrapping

Wrappings can freely modify an event before forwarding it through the wrapped emitter:

#![allow(unused)]
fn main() {
extern crate emit;
use emit::Emitter;

let emitter = emit::emitter::from_fn(|evt| println!("{evt:?}"))
    .wrap_emitter(emit::emitter::wrapping::from_fn(|emitter, evt| {
        // Wrappings can transform the event in any way before emitting it
        // In this example we clear any extent on the event
        let evt = evt.with_extent(emit::Empty);

        // Wrappings need to call the given emitter in order for the event
        // to be emitted
        emitter.emit(evt)
    }));
}

Filtering events with a wrapping

If a wrapping doesn't forward an event then it will be discarded:

#![allow(unused)]
fn main() {
extern crate emit;
use emit::{Emitter, Props};

let emitter = emit::emitter::from_fn(|evt| println!("{evt:?}"))
    .wrap_emitter(emit::emitter::wrapping::from_fn(|emitter, evt| {
        // If a wrapping doesn't call the given emitter then the event
        // will be discarded. In this example, we only emit events
        // carrying a property called "sampled" with the value `true`
        if evt.props().pull::<bool, _>("sampled").unwrap_or_default() {
            emitter.emit(evt)
        }
    }));
}

You can also treat a Filter as a wrapping directly:

#![allow(unused)]
fn main() {
extern crate emit;
use emit::Emitter;

let emitter = emit::emitter::from_fn(|evt| println!("{evt:?}"))
    .wrap_emitter(emit::emitter::wrapping::from_filter(
        emit::level::min_filter(emit::Level::Warn)
    ));
}

Also see Filtering events for more details on filtering in emit.

Flushing

Events may be processed asynchronously, so to ensure they're fulling flushed before your main returns, you can call blocking_flush at the end of your main function:

extern crate emit;
extern crate emit_term;
fn main() {
    let rt = emit::setup()
        .emit_to(emit_term::stdout())
        .init();

    // Your app code goes here

    // Flush at the end of `main`
    rt.blocking_flush(std::time::Duration::from_secs(5));
}

It's a good idea to flush even if your emitter isn't asynchronous. In this case it'll be a no-op, but will ensure flushing does happen if you ever introduce an asynchronous emitter in the future.

Instead of blocking_flush, you can call flush_on_drop:

extern crate emit;
extern crate emit_term;
fn main() {
    let _rt = emit::setup()
        .emit_to(emit_term::stdout())
        .init()
        .flush_on_drop(std::time::Duration::from_secs(5));

    // Your app code goes here
}

Once the returned guard goes out of scope it'll call blocking_flush for you, even if a panic unwinds through your main function. Make sure you give the guard an identifer like _rt and not _, otherwise it will be dropped immediately and not at the end of your main function.

Emitting to the console

You can use emit_term to write diagnostic events to the console in a human-readable format:

[dependencies.emit_term]
version = "0.11.0-alpha.21"
extern crate emit;
extern crate emit_term;
fn main() {
    let rt = emit::setup().emit_to(emit_term::stdout()).init();

    // Your app code goes here

    rt.blocking_flush(std::time::Duration::from_secs(5));
}

See the crate docs for more details.

Format

Events are written with a header containing the timestamp, level, and emitting package name, followed by the rendered message template:

#![allow(unused)]
fn main() {
extern crate emit;
emit::info!("Hello, {user}", user: "Rust");
}

emit_term output for the above program

If the event contains an error (the well-known err property), then it will be formatted as a cause chain after the message:

#![allow(unused)]
fn main() {
extern crate emit;
let err = "";
emit::warn!("writing to {path} failed", path: "./file.txt", err);
}

emit_term output for the above program

If the event is part of a trace, the trace and span ids will be written in the header with corresponding colored boxes derived from their values:

extern crate emit;
fn main() -> Result<(), Box<dyn std::error::Error>> {
#[emit::info_span(err_lvl: "warn", "write to {path}")]
fn write_to_file(path: &str, data: &[u8]) -> std::io::Result<()> {
/*
    ..
*/

    emit::debug!("wrote {bytes} bytes to the file", bytes: data.len());

    Ok(())
}

write_to_file("./file.txt", b"Hello")?;
Ok(())
}

emit_term output for the above program

Writing your own console emitter

The emit_term source code is written to be hackable. You can take and adapt its source to your needs, or write your own emitter that formats events the way you'd like. See Writing an emitter for details.

Emitting to rolling files

You can use emit_file to write diagnostic events to local rolling files:

[dependencies.emit_file]
version = "0.11.0-alpha.21"
extern crate emit;
extern crate emit_file;
fn main() {
    let rt = emit::setup()
        .emit_to(emit_file::set("./target/logs/my_app.txt").spawn())
        .init();

    // Your app code goes here

    rt.blocking_flush(std::time::Duration::from_secs(5));
}

Events will be written in newline-delimited JSON by default:

{"ts_start":"2024-05-29T03:35:13.922768000Z","ts":"2024-05-29T03:35:13.943506000Z","module":"my_app","msg":"in_ctxt failed with `a` is odd","tpl":"in_ctxt failed with `err`","a":1,"err":"`a` is odd","lvl":"warn","span_id":"0a3686d1b788b277","span_parent":"1a50b58f2ef93f3b","trace_id":"8dd5d1f11af6ba1db4124072024933cb"}

emit_file is a robust, asynchronous file writer that can recover from IO errors and manage the size of your retained logs on-disk.

See the crate docs for more details.

Emitting via OTLP

You can use emit_otlp to emit diagnostic events to remote OpenTelemetry-compatible services.

OpenTelemtry defines a wire protocol for exchanging diagnostic data called OTLP. If you're using a modern telemetry backend then chances are it supports OTLP either directly or through OpenTelemetry's Collector.

emit_otlp is an independent implementation of OTLP that maps emit's events onto the OpenTelemetry data model. emit_otlp doesn't rely on the OpenTelemtry SDK or any gRPC or protobuf tooling, so can be added to any Rust application without requiring changes to your build process.

[dependencies.emit_otlp]
version = "0.11.0-alpha.21"
extern crate emit;
extern crate emit_otlp;
fn main() {
    let rt = emit::setup()
        .emit_to(emit_otlp::new()
            // Add required resource properties for OTLP
            .resource(emit::props! {
                #[emit::key("service.name")]
                service_name: "my_app",
            })
            // Configure endpoints for logs/traces/metrics using gRPC + protobuf
            .logs(emit_otlp::logs_grpc_proto("http://localhost:4319"))
            .traces(emit_otlp::traces_grpc_proto("http://localhost:4319"))
            .metrics(emit_otlp::metrics_grpc_proto("http://localhost:4319"))
            .spawn())
        .init();

    // Your app code goes here

    rt.blocking_flush(std::time::Duration::from_secs(30));
}

See the crate docs for more details.

Logs

Any event can be treated as a log event. You need to configure a logs endpoint in your emit_otlp setup for this to happen. See the crate docs for details.

Traces

Events in emit's tracing data model can be treated as a trace span. You need to configure a traces endpoint in your emit_otlp setup for this to happen, otherwise they'll be treated as logs. See the crate docs for details.

Metrics

Events in emit's metrics data model can be treated as a metric. You need to configure a metrics endpoint in your emit_otlp setup for this to happen, otherwise they'll be treated as logs. See the crate docs for details.

Supported protocols

emit_otlp supports sending OTLP using gRPC, HTTP+protobuf, and HTTP+JSON.

TLS

emit_otlp supports TLS using the default Cargo features when your endpoint uses the https scheme. See the crate docs for details.

Compression

emit_otlp will compress payloads using gzip using the default Cargo features. See the crate docs for details.

HTTP headers

emit_otlp supports custom HTTP headers per endpoint. See the crate docs for details.

Advanced apps

This section covers advanced integration scenarios for bigger or more demanding applications.

Integrating with OpenTelemetry

Larger applications may find themselves integrating components using multiple diagnostic frameworks, like log or tracing. In these cases, you can use the OpenTelemetry SDK as your central pipeline, with others integrating with it instead of eachother.

You can configure emit to send its diagnostics to the OpenTelemetry SDK using emit_opentelemetry:

[dependencies.emit]
version = "0.11.0-alpha.21"

[dependencies.emit_opentelemetry]
version = "0.11.0-alpha.21"
extern crate emit;
extern crate emit_opentelemetry;
extern crate opentelemetry;
extern crate opentelemetry_sdk;
fn main() {
    // Configure the OpenTelemetry SDK
    // See the OpenTelemetry SDK docs for details on configuration
    let logger_provider = opentelemetry_sdk::logs::LoggerProvider::builder().build();
    let tracer_provider = opentelemetry_sdk::trace::TracerProvider::builder().build();

    // Configure `emit` to point to the OpenTelemetry SDK
    let rt = emit_opentelemetry::setup(logger_provider, tracer_provider).init();

    // Your app code goes here

    rt.blocking_flush(std::time::Duration::from_secs(30));

    // Shutdown the OpenTelemetry SDK
}

See the crate docs for more details.

Setup outside of main

emit is typically configured in your main function, but that might not be feasible for some applications. In these cases, you can run emit's setup in a function and flush it deliberately at some later point:

#![allow(unused)]
fn main() {
extern crate emit;
extern crate emit_term;
fn diagnostics_init() {
    let _ = emit::setup()
        .emit_to(emit_term::stdout())
        .try_init();
}

fn diagnostics_flush() {
    emit::blocking_flush(std::time::Duration::from_secs(5));
}
}

Calling try_init() ensures you don't panic even if setup is called multiple times.

emit doesn't automatically flush or de-initialize its runtime when Init goes out of scope so it's safe to let it drop before your application exits.

For developers

This section descibes how to implement emit traits to write your own components.

Writing an emitter

You can write a simple emitter using emitter::from_fn, but advanced cases need to implement the Emitter trait.

For a complete implementation, see the source for emit_file.

Dependencies

If you're writing a library with an emitter, you can depend on emit without default features:

[dependencies.emit]
version = "0.11.0-alpha.21"
# Always disable default features
default-features = false
# Add any features you need
features = ["implicit_internal_rt"]

Internal diagnostics

If your emitter is complex enough to need its own diagnostics, you can add the implicit_internal_rt feature of emit and use it when calling emit! or #[span]:

#![allow(unused)]
fn main() {
extern crate emit;
let err = "";
emit::warn!(rt: emit::runtime::internal(), "failed to emit an event: {err}");
}

Your emitter must not write diagnostics to the default runtime. If you disabled default features when adding emit to your Cargo.toml then this will be verified for you at compile-time.

Metrics

A standard pattern for emitters is to expose a function called metric_source that exposes a Source with any metrics for your emitter. See this example from emit_file.

Background processing

Emitters should minimize their impact in calling code but offloading expensive processing to a background thread. You can use emit_batcher to implement a batching, retrying, asynchronous emitter.

Troubleshooting

Emitters write their own diagnostics to an alternative emit runtime, which you can configure via init_internal to debug them:

extern crate emit;
extern crate emit_term;
extern crate emit_file;
fn main() {
    // Configure the internal runtime before your regular setup
    let internal_rt = emit::setup()
        .emit_to(emit_term::stdout())
        .init_internal();

    // Run your regular `emit` setup
    let rt = emit::setup()
        .emit_to(emit_file::set("./target/logs/my_app.txt").spawn())
        .init();

    // Your app code goes here

    rt.blocking_flush(std::time::Duration::from_secs(5));

    // Flush the internal runtime after your regular setup
    internal_rt.blocking_flush(std::time::Duration::from_secs(5));
}

Reference

This section is an index of overarching and cross-cutting concepts in emit.

Architecture

This section describes emit's key components and how they fit together.

Crate organization

emit is split into a few subcrates:

classDiagram
    direction RL
    
    emit_core <.. emit_macros
    emit_core <.. emit
    emit_macros <.. emit

    class emit_macros {
        emit_core = "0.17.0-alpha.17"
        proc-macro2 = "1"
        quote = "1"
        syn = "2"
    }

    emit <.. emit_term
    emit <.. emit_file
    emit <.. emit_otlp
    emit <.. emit_custom

    emit <.. app : required

    class emit {
        emit_core = "0.17.0-alpha.17"
        emit_macros = "0.17.0-alpha.17"
    }

    emit_term .. app : optional
    emit_file .. app : optional
    emit_otlp .. app : optional
    emit_custom .. app : optional

    class emit_term {
        emit = "0.17.0-alpha.17"
    }

    class emit_file {
        emit = "0.17.0-alpha.17"
    }

    class emit_otlp {
        emit = "0.17.0-alpha.17"
    }

    class emit_custom["other emitter"] {
        emit = "0.17.0-alpha.17"
    }

    class app["your app"] {
        emit = "0.17.0-alpha.17"
        emit_term = "0.17.0-alpha.17"*
        emit_file = "0.17.0-alpha.17"*
        emit_otlp = "0.17.0-alpha.17"*
    }

    click emit_core href "https://docs.rs/emit_core/0.11.0-alpha.21/emit_core/index.html"
    click emit_macros href "https://docs.rs/emit_macros/0.11.0-alpha.21/emit_macros/index.html"
    click emit href "https://docs.rs/emit/0.11.0-alpha.21/emit/index.html"
    click emit_term href "https://docs.rs/emit_term/0.11.0-alpha.21/emit_term/index.html"
    click emit_file href "https://docs.rs/emit_file/0.11.0-alpha.21/emit_file/index.html"
    click emit_otlp href "https://docs.rs/emit_otlp/0.11.0-alpha.21/emit_otlp/index.html"
  • emit: The main library that re-exports emit_core and emit_macros. This is the one your applications depend on.
  • emit_core: Just the fundamental APIs. It includes the shared() and internal() runtimes. The goal of this library is to remain stable, even if macro syntax evolves over time.
  • emit_macros: emit!, #[span], and other procedural macros.

The emit library doesn't implement anywhere for you to send your diagnostics itself, but there are other libraries that do:

You can also write your own emitters by implementing the Emitter trait. See Writing an Emitter for details.

Events

Events are the central data type in emit that all others hang off. They look like this:

classDiagram
    direction RL
    Timestamp <.. Extent

    Str <.. Props
    Value <.. Props

    class Props {
        for_each(Fn(Str, Value))*
    }

    <<Trait>> Props

    Path <.. Event
    Props <.. Event
    Template <.. Event

    class Extent {
        as_point() Timestamp
        as_range() Option~Range~Timestamp~~
    }

    Extent <.. Event

    class Event {
        mdl() Path
        tpl() Template
        extent() Extent
        props() Props
    }

    click Event href "https://docs.rs/emit/0.11.0-alpha.21/emit/struct.Event.html"
    click Timestamp href "https://docs.rs/emit/0.11.0-alpha.21/emit/struct.Timestamp.html"
    click Extent href "https://docs.rs/emit/0.11.0-alpha.21/emit/struct.Extent.html"
    click Str href "https://docs.rs/emit/0.11.0-alpha.21/emit/struct.Str.html"
    click Value href "https://docs.rs/emit/0.11.0-alpha.21/emit/struct.Value.html"
    click Props href "https://docs.rs/emit/0.11.0-alpha.21/emit/trait.Props.html"
    click Template href "https://docs.rs/emit/0.11.0-alpha.21/emit/struct.Template.html"
    click Path href "https://docs.rs/emit/0.11.0-alpha.21/emit/struct.Path.html"

Events include:

  • A Path for the component that generated them.
  • A Template for their human-readable description. Templates can also make good low-cardinality identifiers for a specific shape of event.
  • An Extent for the time the event is relevant. The extent itself may be a single Timestamp for a point in time, or a pair of timestamps representing an active time range.
  • Props for structured key-value pairs attached to the event. These can be lazily interpolated into the template.

See Event data model for more details.

Runtimes

In emit, a diagnostic pipeline is an instance of a Runtime. Each runtime is an isolated set of components that help construct and emit diagnostic events in your applications. It looks like this:

classDiagram
    direction RL

    class AmbientSlot {
        get() Runtime
    }

    Runtime <.. AmbientSlot

    class Runtime {
        emitter() Emitter
        filter() Filter
        ctxt() Ctxt
        clock() Clock
        rng() Rng
    }

    Emitter <.. Runtime
    Filter <.. Runtime
    Ctxt <.. Runtime
    Clock <.. Runtime
    Rng <.. Runtime

    class Emitter {
        emit(Event)*
    }

    <<Trait>> Emitter

    class Filter {
        matches() bool*
    }

    <<Trait>> Filter

    class Ctxt {
        open(Props)*
        with_current(FnOnce~Props~)*
    }

    <<Trait>> Ctxt

    class Clock {
        now() Timestamp*
    }

    <<Trait>> Clock

    class Rng {
        fill([u8])*
    }

    <<Trait>> Rng

    click Emitter href "https://docs.rs/emit/0.11.0-alpha.21/emit/trait.Emitter.html"
    click Filter href "https://docs.rs/emit/0.11.0-alpha.21/emit/trait.Filter.html"
    click Ctxt href "https://docs.rs/emit/0.11.0-alpha.21/emit/trait.Ctxt.html"
    click Clock href "https://docs.rs/emit/0.11.0-alpha.21/emit/trait.Clock.html"
    click Rng href "https://docs.rs/emit/0.11.0-alpha.21/emit/trait.Rng.html"
    click Runtime href "https://docs.rs/emit/0.11.0-alpha.21/emit/runtime/struct.Runtime.html"
    click AmbientSlot href "https://docs.rs/emit/0.11.0-alpha.21/emit/runtime/struct.AmbientSlot.html"

A Runtime includes:

  • Emitter: Responsible for sending events to some outside observer.
  • Filter: Responsible for determining whether an event should be emitted or not.
  • Ctxt: Responsible for storing ambient context that's appended to events as they're constructed.
  • Clock: Responsible for assigning timestamps to events and running timers.
  • Rng: Responsible for generating unique identifiers like trace and span ids.

An AmbientSlot is a container for a Runtime that manages global initialization. emit includes two built-in ambient slots:

  • shared(): The runtime used by default when not otherwise specified.
  • internal(): The runtime used by other runtimes for self diagnostics.

You can also define your own AmbientSlots or use Runtimes directly.

Event construction and emission

When the emit! macro is called, an event is constructed using features of the runtime before being emitted through it. This is how it works:

flowchart
    start((start)) --> macro["`<code>emit!('a {x}', y)</code>`"]

    macro --> tpl["`<code>Template('a {x}')</code>`"]
    macro --> macro_props["`<code>Props { x, y }</code>`"]
    
    ctxt{{"`<code>Ctxt::Current</code>`"}} --> ctxt_props["`<code>Props { z }</code>`"]
    
    props["`<code>Props { x, y, z }</code>`"]
    macro_props --> props
    ctxt_props --> props

    clock{{"`<code>Clock::now</code>`"}} --> ts["`<code>Timestamp</code>`"] --> extent["`<code>Extent::point</code>`"]

    mdl_path["`<code>module_path!()</code>`"] --> mdl["`<code>Path('a::b::c')</code>`"]

    event["`<code>Event</code>`"]
    props -- props --> event
    extent -- extent --> event
    tpl -- tpl --> event
    mdl -- mdl --> event

    filter{"`<code>Filter::matches</code>`"}

    event --> filter
    filter -- false --> filter_no(((discard)))

    emitter{{"`<code>Emitter::emit</code>`"}}

    filter -- true --> emitter

    emitter --> END(((end)))

    click macro href "https://docs.rs/emit/0.11.0-alpha.21/emit/macro.emit.html"

    click tpl href "https://docs.rs/emit/0.11.0-alpha.21/emit/struct.Template.html"

    click macro_props href "https://docs.rs/emit/0.11.0-alpha.21/emit/trait.Props.html"
    click ctxt_props href "https://docs.rs/emit/0.11.0-alpha.21/emit/trait.Props.html"
    click props href "https://docs.rs/emit/0.11.0-alpha.21/emit/trait.Props.html"

    click mdl href "https://docs.rs/emit/0.11.0-alpha.21/emit/struct.Path.html"

    click ts href "https://docs.rs/emit/0.11.0-alpha.21/emit/struct.Timestamp.html"
    click extent href "https://docs.rs/emit/0.11.0-alpha.21/emit/struct.Extent.html"

    click event href "https://docs.rs/emit/0.11.0-alpha.21/emit/struct.Event.html"

    click emitter href "https://docs.rs/emit/0.11.0-alpha.21/emit/trait.Emitter.html"
    click filter href "https://docs.rs/emit/0.11.0-alpha.21/emit/trait.Filter.html"
    click ctxt href "https://docs.rs/emit/0.11.0-alpha.21/emit/trait.Ctxt.html"
    click clock href "https://docs.rs/emit/0.11.0-alpha.21/emit/trait.Clock.html"

When constructing an event, the runtime provides the current timestamp and any ambient context. When emitting an event, the runtime filters out events to discard and emits the ones that remain.

Once an event is constructed, it no longer distinguishes properties attached directly from properties added by the ambient context.

You don't need to use macros to construct events. You can also do it manually to get more control over the data they contain.

Event data model

All diagnostics in emit are represented as an Event.

Each event is the combination of:

  • mdl (Path): The path of the component that generated the event.
  • tpl (Template): A lazily-rendered, user-facing description of the event.
  • extent (Extent): The point in time that the event occurred at, or the span of time for which it was active.
  • props (Props): A set of key-value pairs associated with the event.

Here's an example of an event created using the emit! macro:

#![allow(unused)]
fn main() {
extern crate emit;
let user = "user-123";
let item = "product-456";

emit::emit!("{user} added {item} to their cart");
}

This event will have:

  • mdl: The path of the module that called emit!, like shop::orders::add_to_cart.
  • tpl: The raw template. In this case it's "{user} added {item} to their cart". When rendered, the template will produce user-123 added product-456 to their cart.
  • extent: The time when emit! was called, like 2024-01-02T03:04:05.678Z. Extents may also be a range. See Extents and timestamps for details.
  • props: Any properties referenced in or after the template. In this case it's user and item, so the properties are { user: "user-123", item: "product-456" }. Property values aren't restricted to strings, they can be any primitive or complex type. See Value data model for details.

Extensions

The core event data model doesn't encode any specific diagnostic paradigm. It doesn't even include log levels. emit uses well-known properties to support extensions to its data model. A well-known property is a reserved name and set of allowed values that consumers of diagnostic data can use to treat an event as something more specific. See the well_known module for a complete list of well-known properties.

The two main extensions to the event data model are tracing, and metrics. You can also define your own extensions. These extensions are both based on the evt_kind well-known property. Consumers that aren't specially aware of it will treat unknown extended events as regular ones.

Value data model

The Value type is emit's representation of an anonymous structured value based on the value_bag library. Value is a concrete type rather than a trait to make working with them in Props easier. Internally, a value holds a direct reference or embedded primitive value for:

  • Integers: i8-i128, u8-u128.
  • Binary floating points: f32-f64.
  • Booleans: bool.
  • Strings: &'v str.

Values can also store more complex types by embedding references implementing a trait from a serialization framework:

  • Standard formatting: std::fmt::Debug, std::fmt::Display.
  • Serde: serde::Serialize.
  • Sval: sval::Value.

A value can always be formatted or serialized using any of the above frameworks, regardless of whatever might be embedded in it, in the most structure-preserving way. That means if you embed an enum using serde::Serialize you can still serialize it as an enum using the sval::Value implementation on Value.

Extents and timestamps

The time-oriented part of an event is its Extent. Internally, an extent stores Timestamps. An extent can either store one or a pair of timestamps.

An extent that stores a single timestamp is called a point. These are used by log events and other events that represent a point-in-time observation.

An extent that stores a pair of timestamps is called a range. These are used by trace spans and other events that represent something happening over time.

Constructing events without macros

Events don't have to be constructed using macros. You can use the Event::new constructor manually:

#![allow(unused)]
fn main() {
extern crate emit;
let parts = [
    emit::template::Part::hole("user"),
    emit::template::Part::text(" added "),
    emit::template::Part::hole("item"),
    emit::template::Part::text(" to their cart"),
];

let evt = emit::Event::new(
    // mdl
    emit::path!("shop::orders::add_to_cart"),
    // tpl
    emit::Template::new_ref(&parts),
    // extent
    emit::Timestamp::try_from_str("2024-01-02T03:04:05.678Z").unwrap(),
    // props
    [
        ("user", "user-123"),
        ("item", "product-456"),
    ]
);
}

Template syntax and rendering

Producing templates

emit templates are string literals with holes for properties between braces. This is an example of a template:

#![allow(unused)]
fn main() {
extern crate emit;
let user = "Rust";

emit::emit!("Hello, {user}");
}

The emit! and #[span] macros use the same syntax.

Properties within templates

Properties in templates appear within braces:

#![allow(unused)]
fn main() {
extern crate emit;
let user = "Rust";
emit::emit!("Hello, {user}");
}

Braces may be escaped by doubling them:

#![allow(unused)]
fn main() {
extern crate emit;
emit::emit!("Hello, {{user}}");
}

Properties use Rust's field value syntax, like you'd write when initializing struct fields. Usually they're a standalone identifer that will capture a property in scope with that name. Properties can also be given a value inline as an expression:

#![allow(unused)]
fn main() {
extern crate emit;
emit::emit!("Hello, {user: \"Rust\"}");
}

Properties may have attributes applied to them:

#![allow(unused)]
fn main() {
extern crate emit;
let user = "Rust";
emit::emit!("Hello, {#[cfg(enabled)] user}")
}

See Property attributes for details on attributes you can apply. Also see Property capturing for details on what types of properties can be captured.

Properties after templates

Complex property expressions are distracting within templates. Attributes and values for properties declared in the template can be written after it using the same field-value syntax:

#![allow(unused)]
fn main() {
extern crate emit;
emit::emit!(
    "Hello, {user}",
    #[cfg(enabled)]
    user: "Rust",
);
}

Properties outside of the template don't need a corresponding hole to be captured:

#![allow(unused)]
fn main() {
extern crate emit;
let user = "Rust";
emit::emit!(
    "Hello, {user}",
    lang: "en",
);
}

Properties before templates

Properties declared before the template aren't captured. They're called control parameters and are used to change the way events are constructed or emitted:

#![allow(unused)]
fn main() {
extern crate emit;
let user = "Rust";
emit::emit!(
    mdl: emit::path!("a::b::c"),
    "Hello, {user}",
)
}

The names and values of control parameters are different between emit! and #[span]. See Control parameters for details.

Rendering templates

Templates are tokenized into sequences of text and holes for property interpolation:

Hello, {user}

When tokenized, this template will look like:

#![allow(unused)]
fn main() {
extern crate emit;
use emit::template::Part;
let tokens = [
    Part::text("Hello, "),
    Part::hole("user"),
];
}

The template can then be fed a value for user and rendered:

#![allow(unused)]
fn main() {
extern crate emit;
use emit::{Template, template::Part};
let tokens = [Part::text("Hello, "), Part::hole("user")];
let template = Template::new_ref(&tokens);

let rendered = template.render(("user", "Rust")).to_string();
assert_eq!("Hello, Rust", rendered);
}

which will produce:

Hello, Rust

Any holes in the template that are rendered without a matching property will reproduce the hole:

#![allow(unused)]
fn main() {
extern crate emit;
use emit::{Template, template::Part};
let tokens = [Part::text("Hello, "), Part::hole("user")];
let template = Template::new_ref(&tokens);

let rendered = template.render(emit::Empty).to_string();
assert_eq!("Hello, {user}", rendered);
}
Hello, {user}

You can control how properties are rendered within templates by implementing the template::Write trait. emit_term uses this for example to render different property types in different colors.

Property capturing

emit supports fully structured properties through the Value type. Those properties don't have to implement any traits defined by emit itself. It instead leans on other popular serialization frameworks. See Value data model for more details.

When a property value is captured in a call to emit! or #[span] by default, it needs to satisfy Display + 'static. If the type of the property value is a primitive like an i32, bool, or str, then it will be stored directly as that type. Copy primitives are stored by-value. All other values are stored by-ref.

You can change the default Display + 'static bound using attributes prefixed with as_ on them.

Property attributes

This section calls out a few attributes you can use to change the way properties are captured. See the crate docs for a complete list of attributes defined by emit.

#[cfg]

You can add the standard #[cfg] attribute to properties in templates. If the #[cfg] evaluates to false then the entire hole will be omitted from the template.

#![allow(unused)]
fn main() {
extern crate emit;
emit::emit!("Hello, {#[cfg(disabled)] user}");
}
Event {
    mdl: "my_app",
    tpl: "Hello, ",
    extent: Some(
        "2024-10-02T22:01:01.431485400Z",
    ),
    props: {},
}

#[key]

The #[key attribute can be used to set the name of a captured property. This can be used to give a property a name that isn't a valid Rust identifier:

#![allow(unused)]
fn main() {
extern crate emit;
let user = "Rust";
emit::emit!("Hello, {user}", #[emit::key("user.name")] user);
}
Event {
    mdl: "my_app",
    tpl: "Hello, {user.name}",
    extent: Some(
        "2024-10-02T22:01:24.321035400Z",
    ),
    props: {
        "user.name": "Rust",
    },
}

#[fmt]

The #[fmt] attribute applies a formatter to a property value when rendering it in the template. The accepted syntax is the same as Rust's std::fmt:

#![allow(unused)]
fn main() {
extern crate emit;
emit::emit!("pi is {pi}", #[emit::fmt(".3")] pi: 3.1415927);
}
Event {
    mdl: "my_app",
    tpl: "pi is {pi}",
    extent: Some(
        "2024-10-02T22:01:58.842629700Z",
    ),
    props: {
        "pi": 3.1415927,
    },
}

When rendered, the template will produce:

pi is 3.142

#[as_debug]

The #[as_debug] attribute captures a property value using its Debug implementation, instead of the default Display + 'static:

#![allow(unused)]
fn main() {
extern crate emit;
#[derive(Debug)]
struct User<'a> {
    name: &'a str,
}

emit::emit!(
    "Hello, {user}",
    #[emit::as_debug]
    user: User {
        name: "Rust",
    }
);
}
Event {
    mdl: "my_app",
    tpl: "Hello, {user}",
    extent: Some(
        "2024-10-02T22:03:23.588049400Z",
    ),
    props: {
        "user": User {
            name: "Rust",
        },
    },
}

Note that the structure of the captured value is lost. It'll be treated as a string like "User { name: \"Rust\" }" when serialized:

{
    "mdl": "my_app",
    "tpl": "Hello, {user}",
    "ts": "2024-10-02T22:03:23.588049400Z",
    "user": "User { name: \"Rust\" }"
}

See Property capturing for more details.

#[as_serde]

The #[as_serde] attribute captures a property value using its Serialize implementation, instead of the default Display + 'static:

#![allow(unused)]
fn main() {
extern crate emit;
#[macro_use] extern crate serde;
#[derive(Serialize)]
struct User<'a> {
    name: &'a str,
}

emit::emit!(
    "Hello, {user}",
    #[emit::as_serde]
    user: User {
        name: "Rust",
    }
);
}
Event {
    mdl: "my_app",
    tpl: "Hello, {user}",
    extent: Some(
        "2024-10-02T22:05:05.258099900Z",
    ),
    props: {
        "user": User {
            name: "Rust",
        },
    },
}

The structure of properties captured this way is fully preserved:

{
    "mdl": "my_app",
    "tpl": "Hello, {user}",
    "ts": "2024-10-02T22:05:05.258099900Z",
    "user": {
        "name": "Rust"
    }
}

See Property capturing for more details.

Control parameters

Field values that appear before the template literal in emit! or #[span] aren't captured as properties. They're used to control the behavior of the code generated by the macro. The set of valid control parameters and their types is different for each macro.

emit!

See the crate docs for control parameters on emit!.

#[span]

See the crate docs for control parameters on #[span].