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.11.5"
        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.11.5"
        emit_macros = "0.11.5"
    }

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

    class emit_term {
        emit = "0.11.5"
    }

    class emit_file {
        emit = "0.11.5"
    }

    class emit_otlp {
        emit = "0.11.5"
    }

    class emit_custom["other emitter"] {
        emit = "0.11.5"
    }

    class app["your app"] {
        emit = "0.11.5"
        emit_term = "0.11.5"*
        emit_file = "0.11.5"*
        emit_otlp = "0.11.5"*
    }

    click emit_core href "https://docs.rs/emit_core/0.11.5/emit_core/index.html"
    click emit_macros href "https://docs.rs/emit_macros/0.11.5/emit_macros/index.html"
    click emit href "https://docs.rs/emit/0.11.5/emit/index.html"
    click emit_term href "https://docs.rs/emit_term/0.11.5/emit_term/index.html"
    click emit_file href "https://docs.rs/emit_file/0.11.5/emit_file/index.html"
    click emit_otlp href "https://docs.rs/emit_otlp/0.11.5/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.5/emit/struct.Event.html"
    click Timestamp href "https://docs.rs/emit/0.11.5/emit/struct.Timestamp.html"
    click Extent href "https://docs.rs/emit/0.11.5/emit/struct.Extent.html"
    click Str href "https://docs.rs/emit/0.11.5/emit/struct.Str.html"
    click Value href "https://docs.rs/emit/0.11.5/emit/struct.Value.html"
    click Props href "https://docs.rs/emit/0.11.5/emit/trait.Props.html"
    click Template href "https://docs.rs/emit/0.11.5/emit/struct.Template.html"
    click Path href "https://docs.rs/emit/0.11.5/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(Event) 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.5/emit/trait.Emitter.html"
    click Filter href "https://docs.rs/emit/0.11.5/emit/trait.Filter.html"
    click Ctxt href "https://docs.rs/emit/0.11.5/emit/trait.Ctxt.html"
    click Clock href "https://docs.rs/emit/0.11.5/emit/trait.Clock.html"
    click Rng href "https://docs.rs/emit/0.11.5/emit/trait.Rng.html"
    click Runtime href "https://docs.rs/emit/0.11.5/emit/runtime/struct.Runtime.html"
    click AmbientSlot href "https://docs.rs/emit/0.11.5/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>mdl!()</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.5/emit/macro.emit.html"

    click tpl href "https://docs.rs/emit/0.11.5/emit/struct.Template.html"

    click macro_props href "https://docs.rs/emit/0.11.5/emit/trait.Props.html"
    click ctxt_props href "https://docs.rs/emit/0.11.5/emit/trait.Props.html"
    click props href "https://docs.rs/emit/0.11.5/emit/trait.Props.html"

    click mdl href "https://docs.rs/emit/0.11.5/emit/struct.Path.html"

    click ts href "https://docs.rs/emit/0.11.5/emit/struct.Timestamp.html"
    click extent href "https://docs.rs/emit/0.11.5/emit/struct.Extent.html"

    click event href "https://docs.rs/emit/0.11.5/emit/struct.Event.html"

    click emitter href "https://docs.rs/emit/0.11.5/emit/trait.Emitter.html"
    click filter href "https://docs.rs/emit/0.11.5/emit/trait.Filter.html"
    click ctxt href "https://docs.rs/emit/0.11.5/emit/trait.Ctxt.html"
    click clock href "https://docs.rs/emit/0.11.5/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.

Span construction and emission

When the #[span] macro is called, the annotated function is instrumented using features of the runtime before a span representing its execution is emitted through it. This is how it works:

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

    macro --> tpl["`<code>Template('a {x}')</code>`"]
    macro --> macro_props["`<code>Props { x, y }</code>`"]

    ctxt{{"`<code>Ctxt</code>`"}}

    span_ctxt["`<code>SpanCtxt</code>`"]

    rng{{"`<code>Rng</code>`"}} -- new_child --> span_ctxt
    ctxt -- current --> span_ctxt

    clock{{"`<code>Clock</code>`"}} --> timer["`<code>Timer::start</code>`"]

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

    completion["`<code>completion::Default</code>`"]
    tpl --> completion
    emitter --> completion
    ctxt_2{{"`<code>Ctxt</code>`"}} --> completion

    frame["`<code>Frame</code>`"]
    ctxt --> frame
    macro_props -- push --> frame
    span_ctxt -- push --> frame

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

    timer --> active_span

    active_span["`<code>SpanGuard</code>`"]

    active_span --> filter
    frame --> filter

    filter -- false --> filter_no(((disabled)))

    filter -- true --> active_span_2
    filter -- true --> frame_2

    frame_2["<code>Frame::call</code>"]

    active_span_2["`<code>SpanGuard::complete</code>`"] -- produces --> span["`<code>Span</code>`"] --> completion
    
    completion --> END(((end)))

    click macro href "https://docs.rs/emit/0.11.5/emit/attr.span.html"

    click tpl href "https://docs.rs/emit/0.11.5/emit/struct.Template.html"

    click macro_props href "https://docs.rs/emit/0.11.5/emit/trait.Props.html"

    click span_ctxt href "https://docs.rs/emit/0.11.5/emit/span/struct.SpanCtxt.html"
    click span href "https://docs.rs/emit/0.11.5/emit/span/struct.Span.html"
    click timer href "https://docs.rs/emit/0.11.5/emit/timer/struct.Timer.html"

    click frame href "https://docs.rs/emit/0.11.5/emit/frame/struct.Frame.html"
    click frame_2 href "https://docs.rs/emit/0.11.5/emit/frame/struct.Frame.html"

    click active_span href "https://docs.rs/emit/0.11.5/emit/span/struct.SpanGuard.html"
    click active_span_2 href "https://docs.rs/emit/0.11.5/emit/span/struct.SpanGuard.html"

    click completion href "https://docs.rs/emit/0.11.5/emit/span/completion/struct.Default.html"

    click emitter href "https://docs.rs/emit/0.11.5/emit/trait.Emitter.html"
    click filter href "https://docs.rs/emit/0.11.5/emit/trait.Filter.html"
    click ctxt href "https://docs.rs/emit/0.11.5/emit/trait.Ctxt.html"
    click ctxt_2 href "https://docs.rs/emit/0.11.5/emit/trait.Ctxt.html"
    click clock href "https://docs.rs/emit/0.11.5/emit/trait.Clock.html"
    click rng href "https://docs.rs/emit/0.11.5/emit/trait.Rng.html"

When constructing a span, the runtime generates random trace and span ids from the current ambient context and starts a timer using the clock.

If the runtime filter doesn't match an event representing the start of the span then it's disabled and won't be completed. If it does, an active span guard managing the completion of the span, and a frame containing its ambient trace and span ids are constructed.

At the end of the annotated function, the active span guard is completed, constructing a span event and passing it to its configured completion. From there, the event can be emitted back through the runtime.