moonbitstack/moontel/trace does not have a README file

    Sampler

    pub(open) trait Sampler {
    fn should_sample(Self, SampleRequest) -> Decision
    fn description(Self) -> String
    }

    Whether a span is wanted, and how much of it.

    A caller can plug in its own: the four here are the ones the specification names, and anything with a decision — a rate limit read from configuration, a list of paths that must never be dropped — is another implementation of this.

    Sink

    pub(open) trait Sink {
    fn accept(Self, SpanData) -> Unit
    }

    Where finished spans go.

    This is the one seam this library leaves open: exporting to OTLP, to a file, to a ring buffer that a background task drains, or to nothing at all is the caller's business, and the shapes that do it belong to whoever owns the transport. A sink is called on the task that finished the span, so an implementation that writes to a socket should hand the span to a queue rather than block the request.

    A sink receives every recorded span, including the ones the sampler dropped from the trace: SpanData::sampled is the bit that separates "keep for counting" from "send to the backend".

    Refused

    pub(all) suberror Refused {
    Field(at~ : Int, want~ : String)
    NotHex(at~ : Int)
    Zero(at~ : Int)
    TooMany(limit~ : Int, got~ : Int)
    TooLong(limit~ : Int, got~ : Int)
    Ids(want~ : Int, got~ : Int)
    } derive(Eq,
    Debug
    )

    Why a header, an id or a span id source was refused.

    One error type for the whole module: a caller parsing an inbound traceparent and a caller handing the tracer a broken source of randomness both have one thing to catch.

    Refused::equal

    fn Refused::equal(Refused, Refused) -> Bool

    Refused::not_equal

    fn Refused::not_equal(x : Refused, y : Refused) -> Bool

    Refused::to_repr

    Always

    pub(all) struct Always {
    }

    Samples everything, whatever the parent said. The starting point for a service whose traffic is small enough to keep whole.
    impl Sampler for Always

    Always::description

    fn Always::description(_ : Always) -> String

    Always::should_sample

    fn Always::should_sample(_ : Always, _ : SampleRequest) -> Decision

    Decision

    pub(all) enum Decision {
    Drop
    RecordOnly
    RecordAndSample
    } derive(Eq,
    Debug
    )

    The three answers a sampler can give.

    Decision::equal

    fn Decision::equal(Decision, Decision) -> Bool

    Decision::not_equal

    fn Decision::not_equal(x : Decision, y : Decision) -> Bool

    Decision::recording

    fn Decision::recording(self : Decision) -> Bool

    Whether the span reaches the sink with data in it.

    Decision::sampled

    fn Decision::sampled(self : Decision) -> Bool

    Whether the sampled bit is set and the trace continues.

    Decision::to_repr

    Discard

    pub(all) struct Discard {
    }

    Throws spans away. The default for a tracer whose owner has not decided where they go yet, and the sink a replay uses when it only cares that nothing crashes.
    impl Sink for Discard

    Discard::accept

    fn Discard::accept(_ : Discard, _ : SpanData) -> Unit

    Dropped

    pub(all) struct Dropped {
    attributes : Int
    events : Int
    links : Int
    } derive(Eq,
    Debug
    )

    How much a span gave up at its limits. OpenTelemetry asks for the counts rather than for the dropped values, so a reader can tell "this span had no attributes" from "this span had three hundred and we kept 128".

    Dropped::equal

    fn Dropped::equal(Dropped, Dropped) -> Bool

    Dropped::not_equal

    fn Dropped::not_equal(x : Dropped, y : Dropped) -> Bool

    Dropped::to_repr

    Event

    pub(all) struct Event {
    name : String
    at : Int64
    attributes : Array[(String, Value)]
    } derive(Eq,
    Debug
    )

    Something that happened during a span: a name, when it happened, and what it carried. Events keep the order they were added in, which for timestamps handed in by the caller is not necessarily the order of those timestamps.

    Event::equal

    fn Event::equal(Event, Event) -> Bool

    Event::not_equal

    fn Event::not_equal(x : Event, y : Event) -> Bool

    Event::to_repr

    Limits

    pub(all) struct Limits {
    attributes : Int
    events : Int
    links : Int
    attributes_per_event : Int
    attribute_value_length : Int?
    } derive(Eq,
    Debug
    )

    How much one span collects.

    The defaults are OpenTelemetry's: 128 attributes, 128 events, 128 links, 128 attributes on an event, and no limit on the length of a value. They exist to keep instrumentation that loops over user data from taking the process down with it, so the counts are per span and fixed when the span starts.

    Limits::equal

    fn Limits::equal(Limits, Limits) -> Bool

    Limits::new

    fn Limits::new(attributes? : Int, events? : Int, links? : Int, attributes_per_event? : Int, attribute_value_length? : Int?) -> Limits

    The same defaults, with any of them changed.

    Limits::not_equal

    fn Limits::not_equal(x : Limits, y : Limits) -> Bool

    Limits::to_repr

    pub(all) struct Link {
    context : TraceContext
    attributes : Array[(String, Value)]
    } derive(Eq,
    Debug
    )

    A causal relationship to another span that is not this one's parent — a batch item, a message consumed later, a fan-in. The linked context travels with the link so a reader can join the two traces.

    Link::equal

    fn Link::equal(Link, Link) -> Bool

    Link::not_equal

    fn Link::not_equal(x : Link, y : Link) -> Bool

    Link::to_repr

    Never

    pub(all) struct Never {
    }

    Samples nothing. Useful behind parent_based, where it is the answer for a parent that was not sampled.
    impl Sampler for Never

    Never::description

    fn Never::description(_ : Never) -> String

    Never::should_sample

    fn Never::should_sample(_ : Never, _ : SampleRequest) -> Decision

    ParentBased

    pub(all) struct ParentBased {
    root : &Sampler
    remote_sampled : &Sampler
    remote_unsampled : &Sampler
    local_sampled : &Sampler
    local_unsampled : &Sampler
    }

    Follows the parent's decision and asks root only when there is no parent.

    This is the sampler to give a service, because it is what keeps a distributed trace whole: the hop that made the decision decided for the whole trace, and every later hop obeys it instead of rolling its own dice.

    The specification's five rows are the five fields: root without a parent, then a parent from another process or this one, sampled or not. The default for a sampled parent is always and for an unsampled one never, so a program that names only root follows the parent.

    ParentBased::description

    fn ParentBased::description(self : ParentBased) -> String

    ParentBased::should_sample

    fn ParentBased::should_sample(self : ParentBased, request : SampleRequest) -> Decision

    Ratio

    pub(all) struct Ratio {
    ratio : Double
    } derive(
    Debug
    )

    Samples a fixed fraction of traces, decided by the trace id rather than by a coin: the same id gets the same answer on every hop, in every process, and from every service, so a trace that one service keeps is not dropped by the next.

    The fraction is read off the low 64 bits of the trace id, which the specification asks for so that two rates nest: raising the rate keeps every trace the lower rate kept.
    impl Sampler for Ratio

    Ratio::description

    fn Ratio::description(self : Ratio) -> String

    Ratio::should_sample

    fn Ratio::should_sample(self : Ratio, request : SampleRequest) -> Decision

    Ratio::to_repr

    Recorder

    pub struct Recorder {
    spans :
    Ref
    [Array[SpanData]]
    }

    Keeps spans in memory, in the order they finished.

    Meant for replays, examples and the debugging of a single request; a service wants a sink that ships spans off the process, because this one grows without bound.
    impl Sink for Recorder

    Recorder::accept

    fn Recorder::accept(self : Recorder, span : SpanData) -> Unit

    Recorder::len

    fn Recorder::len(self : Recorder) -> Int

    How many spans are kept.

    Recorder::new

    fn Recorder::new() -> Recorder

    An empty recorder.

    Recorder::spans

    fn Recorder::spans(self : Recorder) -> Array[SpanData]

    The spans kept so far, oldest first.

    Recorder::take

    fn Recorder::take(self : Recorder) -> Array[SpanData]

    The spans kept so far, and an empty recorder afterwards. Use this when a replay drives several requests through one tracer and reads one request's spans at a time.

    SampleRequest

    pub(all) struct SampleRequest {
    parent : TraceContext?
    trace_id : Bytes
    name : String
    kind : SpanKind
    }

    What a sampler is told about the span it is deciding on.

    The four things a sampler may look at are all known before the span exists: the parent, the trace id the span will carry, its name and its kind. Later changes — a better name, an attribute discovered mid-request — cannot move the decision, which is why the decision is made here and not on the finished span.

    Span

    pub struct Span {
    context : TraceContext
    parent : Bytes?
    kind : SpanKind
    start : Int64
    recording : Bool
    sink : &Sink
    limits : Limits
    span_name :
    Ref
    [String]
    attribute_list :
    Ref
    [Array[(String, Value)]]
    event_list :
    Ref
    [Array[Event]]
    link_list :
    Ref
    [Array[Link]]
    span_status :
    Ref
    [Status]
    end_time :
    Ref
    [Int64?]
    dropped :
    Ref
    [Dropped]
    }

    The span a caller holds: an identity fixed at creation, and fields that fill in until it is finished.

    Everything that changes after creation sits behind a Ref, so one span may be filled in from the task that opened it and read from another. A non-recording span — one the sampler dropped — accepts the same calls and forgets them, which is what lets instrumented code stay the same whatever the sampler decided.

    Span::add_event

    fn Span::add_event(self : Span, name : String, attributes? : Array[(String, Value)], at~ : Int64) -> Unit

    Add an event. at is the moment the caller says it happened, so a span recorded after the fact keeps its real timeline.
    fn Span::add_link(self : Span, link : Link) -> Unit

    Add a link to another span after creation, past the link limit being counted rather than applied.

    Span::attributes

    fn Span::attributes(self : Span) -> Array[(String, Value)]

    A copy of the attributes set so far, in the order their keys first appeared.

    Span::context

    fn Span::context(self : Span) -> TraceContext

    The identity of the span. It does not change for the span's whole life and stays usable after it ends, because a later span may still name this one as its parent.

    Span::events

    fn Span::events(self : Span) -> Array[Event]

    A copy of the events added so far, in the order they were added.

    Span::finish

    fn Span::finish(self : Span, at~ : Int64) -> Unit

    Finish the span at at, handing it to the sink once.

    Finishing twice does nothing the second time, and finishing a span the sampler dropped hands nothing over. Child spans are untouched: they may still be running, and they keep naming this one as their parent.

    Span::is_ended

    fn Span::is_ended(self : Span) -> Bool

    Whether the span has been finished. A finished span takes no more writes and is not handed to the sink a second time.

    Span::is_recording

    fn Span::is_recording(self : Span) -> Bool

    Whether the span is collecting anything: false after it ends, and false for a span the sampler dropped. Instrumented code can use it to skip building expensive attributes.
    fn Span::links(self : Span) -> Array[Link]

    A copy of the links added so far.

    Span::name

    fn Span::name(self : Span) -> String

    The name the span carries now, which set_name may have changed since it started.

    Span::record_exception

    fn Span::record_exception(self : Span, message~ : String, at~ : Int64, type_? : String, stacktrace? : String, attributes? : Array[(String, Value)]) -> Unit

    Record an error the way OpenTelemetry's exception conventions ask: an event named exception carrying exception.type, exception.message and exception.stacktrace, with any attributes of the caller's own taking precedence over those three.

    Span::set_attribute

    fn Span::set_attribute(self : Span, key : String, value : Value) -> Unit

    Set one attribute. A key already present is overwritten in place, so the attribute list keeps the order in which keys first appeared rather than the order of their last write.

    Past the attribute limit the write is dropped and counted rather than applied: a span with a runaway loop of attributes must not take the process down with it.

    Span::set_attributes

    fn Span::set_attributes(self : Span, attributes : Array[(String, Value)]) -> Unit

    Set several attributes at once, each following the same rule as set_attribute.

    Span::set_name

    fn Span::set_name(self : Span, name : String) -> Unit

    Rename the span. The sampler has already decided, so a better name found during the request cannot change that decision — it changes what the backend draws.

    Span::set_status

    fn Span::set_status(self : Span, status : Status) -> Unit

    Set the status, judged against the order Ok above Error above Unset: Unset changes nothing, Ok is final, and a later Error replaces an earlier one so the message can improve.

    Span::status

    fn Span::status(self : Span) -> Status

    The status as it stands: Unset until a caller sets one.

    SpanData

    pub(all) struct SpanData {
    context : TraceContext
    parent : Bytes?
    name : String
    kind : SpanKind
    start : Int64
    end : Int64
    attributes : Array[(String, Value)]
    events : Array[Event]
    links : Array[Link]
    status : Status
    dropped : Dropped
    } derive(Eq,
    Debug
    )

    A finished span, as a Sink receives it.

    It is a value, not a handle: everything in it is settled, and a sink may keep it, queue it or throw it away. Nothing here is normalised — a span may begin and end at the same nanosecond, and an event may carry a time outside the span.

    SpanData::duration

    fn SpanData::duration(self : SpanData) -> Int64

    How long the span lasted, in nanoseconds. Negative only if the caller handed in an end before the start, which is left as it came.

    SpanData::equal

    fn SpanData::equal(SpanData, SpanData) -> Bool

    SpanData::not_equal

    fn SpanData::not_equal(x : SpanData, y : SpanData) -> Bool

    SpanData::sampled

    fn SpanData::sampled(self : SpanData) -> Bool

    Whether this span carries the sampled bit, which is the one thing a sink needs in order to choose between exporting and merely counting.

    SpanData::to_repr

    SpanKind

    pub(all) enum SpanKind {
    Internal
    Server
    Client
    Producer
    Consumer
    } derive(Eq,
    Debug
    )

    What a span is about, from the point of view of the process that made it. The kind is part of the sampling input and of what a backend draws, so it is settled when the span starts.

    SpanKind::equal

    fn SpanKind::equal(SpanKind, SpanKind) -> Bool

    SpanKind::not_equal

    fn SpanKind::not_equal(x : SpanKind, y : SpanKind) -> Bool

    SpanKind::to_repr

    Status

    pub(all) enum Status {
    Unset
    Ok
    Error(String)
    } derive(Eq,
    Debug
    )

    How the operation ended, as the caller judges it.

    The three values form a total order, Ok above Error above Unset, and a status is judged against that order rather than by "last write wins": Ok is final, Error may replace another Error because the message may improve, and Unset is what a span already has.

    Unset means "the caller has no opinion", which is not a failure: an instrumentation library leaves it alone unless the semantic conventions say otherwise.
    impl Show for Status

    Status::equal

    fn Status::equal(Status, Status) -> Bool

    Status::not_equal

    fn Status::not_equal(x : Status, y : Status) -> Bool

    Status::output

    fn Status::output(self : Status, logger : &Logger) -> Unit

    Status::to_repr

    Status::to_string

    fn Status::to_string(self : Status) -> String

    TraceContext

    pub(all) struct TraceContext {
    trace_id : Bytes
    span_id : Bytes
    flags : Byte
    state : TraceState
    remote : Bool
    } derive(Eq,
    Debug
    )

    The identity of a span: the trace it belongs to, its own id, the flags travelling with it, and the vendor state riding along.

    Ids are octets, not text: sixteen for the trace and eight for the span, as W3C Trace Context lays them out. Hexadecimal is a spelling used at the wire and in logs, and it is produced there rather than stored here.

    remote is the specification's IsRemote: true for a context read off the wire, false for one this process minted. It does not travel in any header.

    TraceContext::equal

    TraceContext::is_valid

    fn TraceContext::is_valid(self : TraceContext) -> Bool

    Whether the trace id is sixteen octets and the span id eight, neither all zero. A context that fails this is not a trace to join.

    TraceContext::new

    fn TraceContext::new(trace_id~ : Bytes, span_id~ : Bytes, flags? : Byte, state? : TraceState, remote? : Bool) -> TraceContext

    Build a context from ids the caller already has. Nothing is validated: this is the shape a tracer mints and a sink reads, and a caller holding a Span already has a valid one. A context read off the wire comes from parse instead, which does validate.

    TraceContext::not_equal

    fn TraceContext::not_equal(x : TraceContext, y : TraceContext) -> Bool

    TraceContext::sampled

    fn TraceContext::sampled(self : TraceContext) -> Bool

    Whether the sampled bit is set: the flag that says this span, and the children that carry it, are wanted by whoever reads the traces.

    TraceContext::to_traceparent

    fn TraceContext::to_traceparent(self : TraceContext) -> String

    The traceparent header value for this context: version 00, the trace id, the span id, and the flags, in lowercase hexadecimal.

    TraceContext::with_sampled

    fn TraceContext::with_sampled(self : TraceContext, sampled~ : Bool) -> TraceContext

    The same context with the sampled bit set or cleared, which is what a sampler's decision turns into. Every other flag travels as it came: a version of the header this library has not met may have put meaning in them.

    TraceState

    pub(all) struct TraceState {
    members : Array[(String, String)]
    } derive(Eq,
    Debug
    )

    Vendor state travelling with the trace, as an ordered list of at most 32 list-members (W3C Trace Context §3.3.1).

    It is a list, not a map: the order is what a hop writes back, the first member belongs to the closest hop, and two members may not share a key. The update operations return a new state rather than editing this one, so that a caller holding the state that arrived cannot change what it arrived as.

    TraceState::delete

    fn TraceState::delete(self : TraceState, key : String) -> TraceState

    The same state without the member key names, and unchanged when there is none.

    TraceState::equal

    fn TraceState::equal(TraceState, TraceState) -> Bool

    TraceState::get

    fn TraceState::get(self : TraceState, key : String) -> String?

    The value key is bound to, or None when the state has no such member.

    TraceState::insert

    fn TraceState::insert(self : TraceState, key~ : String, value~ : String) -> TraceState raise Refused

    The same state with key set to value as the member closest to this hop (W3C Trace Context §3.3.1): an existing member with that key is replaced rather than moved, and a list already at 32 members drops its rightmost.

    Building a header to send means adding your own entry to what came in, which is the one mutation a hop is allowed to make.

    TraceState::is_empty

    fn TraceState::is_empty(self : TraceState) -> Bool

    Whether the hop carried no vendor state at all.

    TraceState::len

    fn TraceState::len(self : TraceState) -> Int

    How many list-members the state carries.

    TraceState::not_equal

    fn TraceState::not_equal(x : TraceState, y : TraceState) -> Bool

    TraceState::parse

    fn TraceState::parse(input : StringView) -> TraceState raise Refused

    Parse a tracestate header value.

    Strict on purpose: a list-member that does not match the grammar or a list longer than 32 members refuses the whole header. A hop that kept the parseable half of a broken tracestate would hand the next hop something the sender never wrote, and W3C Trace Context §3.3.1 asks for the header to be dropped instead.

    The one place the reading is wider than §3.3.1's ABNF is the key: the W3C Trace Context's reference parser is followed there, so foo@ and foo@@bar are list-members like any other. check_key says why, and says what is still out.

    A key that appears twice is the one thing handled member by member: the first member with that key wins and the later one is dropped, which is the choice W3C Trace Context level 2 allows ("pass the duplicate on as-is or drop one of them") and the one W3C Trace Context's reference parser makes.

    Whitespace before a member and after a value is dropped, as is whitespace around the commas; whitespace anywhere else is part of a key or a value that the grammar does not allow.

    TraceState::to_header

    fn TraceState::to_header(self : TraceState) -> String

    The header value: members in order, key=value, comma-separated, with no whitespace (W3C Trace Context §3.3.1.2). Empty when there is no state, which on the wire is the same as no header being sent.

    Tracer

    pub struct Tracer {
    rand : (Int) -> Bytes
    sink : &Sink
    sampler : &Sampler
    limits : Limits
    }

    Makes spans, and the one place ids and the sampling decision are settled.

    A tracer holds three things it cannot invent for itself: where randomness comes from, where finished spans go, and who decides whether a trace is wanted. The clock is the fourth, and it does not even reach the tracer — every call that needs a time is handed one.

    Tracer::new

    fn Tracer::new(rand~ : (Int) -> Bytes, sink~ : &Sink, sampler~ : &Sampler, limits? : Limits) -> Tracer

    A tracer drawing ids from rand, handing finished spans to sink, and asking sampler about every span it starts.

    rand(n) must return exactly n octets and must not return all zeros: eight octets of zeros is not a valid span id, and a generator that produces one is broken rather than unlucky. A call that breaks either rule makes start refuse; see Refused::Ids and Refused::Zero.

    Tracer::start

    fn Tracer::start(self : Tracer, name : String, parent? : TraceContext, kind? : SpanKind, at~ : Int64, attributes? : Array[(String, Value)], links? : Array[Link]) -> Span raise Refused

    Start a span.

    With a parent whose context is valid, the new span joins that trace: it takes the parent's trace id and trace state, and parent_based sampling follows the parent's decision. Without one it starts a trace of its own, with a fresh trace id and no vendor state.

    at is the start of the operation in nanoseconds. attributes and links are set before the span is handed back; a sampler sees them, which is why they are parameters here rather than calls on the span afterwards.

    Value

    pub(all) enum Value {
    Bool(Bool)
    Int(Int64)
    Double(Double)
    Str(String)
    Bools(Array[Bool])
    Ints(Array[Int64])
    Doubles(Array[Double])
    Strs(Array[String])
    } derive(Eq,
    Debug
    )

    One attribute value.

    The arrays are separate variants rather than one array of Value because OpenTelemetry requires an array attribute to be homogeneous, and because that is what an exporter has to write: a mixed array has no single type tag.
    impl Show for Value

    Value::equal

    fn Value::equal(Value, Value) -> Bool

    Value::not_equal

    fn Value::not_equal(x : Value, y : Value) -> Bool

    Value::output

    fn Value::output(self : Value, logger : &Logger) -> Unit

    Value::to_repr

    Value::to_string

    fn Value::to_string(self : Value) -> String

    always

    let always : Always

    discard

    let discard : Discard

    dropped_nothing

    let dropped_nothing : Dropped

    Nothing dropped, the value a span starts with.

    empty_trace_state

    let empty_trace_state : TraceState

    No vendor state: what a new trace starts with, and what a hop that carries nothing forward hands on.

    never

    let never : Never

    parent_based

    fn parent_based(root~ : &Sampler, remote_sampled? : &Sampler, remote_unsampled? : &Sampler, local_sampled? : &Sampler, local_unsampled? : &Sampler) -> ParentBased

    Parent-based sampling over root, which decides for the first span of a trace.

    random_flag

    let random_flag : Byte

    The random bit W3C Trace Context level 2 uses to say a trace id was drawn at random. Carried through a hop unchanged; this library does not read it.

    ratio

    fn ratio(ratio~ : Double) -> Ratio

    A ratio sampler. A ratio outside 0 to 1 is clamped, so configuration read from a file cannot produce a sampler that samples more than everything or less than nothing.

    sampled_flag

    let sampled_flag : Byte

    The sampled bit of traceparent's flags field (W3C Trace Context §3.2.2.5).

    trace_limits

    let trace_limits : Limits

    The OpenTelemetry defaults, from the trace SDK specification's span limits table.

    tracestate_limit

    let tracestate_limit : Int

    The list-members W3C Trace Context allows in one tracestate (§3.3.1).