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 ofemit
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 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 Frame
s. 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.
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
: thespan_id
of the operation that invoked this one.trace_id
: an identifier shared by all events in a distributed trace. Atrace_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,
},
}
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 of1
's for defined values, and0
'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 Metric
s. 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:
emit_term
for emitting to the console.emit_file
for emitting to rolling files.emit_otlp
for emitting via OTLP.
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"); }
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); }
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(()) }
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-exportsemit_core
andemit_macros
. This is the one your applications depend on.emit_core
: Just the fundamental APIs. It includes theshared()
andinternal()
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:
emit_term
: Writes to the console. See Emitting to the console for details.emit_file
: Writes to rolling files. See Emitting to rolling files for details.emit_otlp
: Writes OpenTelemetry's wire protocol. See Emitting via OTLP for details.
You can also write your own emitters by implementing the Emitter
trait. See Writing an Emitter for details.
Events
Event
s 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 singleTimestamp
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 AmbientSlot
s or use Runtime
s 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 calledemit!
, likeshop::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 whenemit!
was called, like2024-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'suser
anditem
, 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 Timestamp
s. 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]
.