Skip to content
Logo

Tracer

E.with_span(effect, name) opens a span around an effect: one named, timed unit that nests. A span opened inside another becomes its child and shares its trace id, and it ends with the effect's Exit when the effect settles, on success, typed failure, defect and interruption alike. Spans go through the implicit E.Tracer service, so nothing enters R; timestamps come from E.now() and ids are drawn through E.random(). with_span takes the effect first, like annotate_logs, and is also a method:

@E.
def (: ) -> E.[, ]:
    yield from E.(user_id=)  # attributes on the open span
    return ()
 
 
tracedArrow
= E.((1), "fetch_user")
= .("handle_request", kind="server", route="/users/1")

E.annotate_current_span(**attributes) adds attributes to the innermost open span and is a no-op outside one; E.current_span() returns that span and fails with E.Tracer.NoCurrentSpan outside one. The default tracer, E.Tracer.Live, opens in-memory spans that nothing collects, so tracing costs nothing until a tracer that exports them is provided: an OpenTelemetry exporter would be another implementation of E.Tracer.Protocol, and the hex ids already match the W3C traceparent header. A forked fiber starts with a fresh environment, so it opens root spans unless the parent is passed along with E.provide_implicit(forked, E.Tracer.ParentSpan(span)), span being the result of E.current_span(); race_first and timeout inherit the current span.

In tests, provide E.Tracer.Test, which records every span it opens in spans, in opening order. Each span carries name, kind, attributes, parent, trace_id, span_id and a status that is Started(start_time) while open and Ended(start_time, end_time, exit) afterwards. A test that requests the test_tracer fixture gets one provided to its effect:

@E.
def (
    : E.Tracer.,
) -> E.[None, ]:
    yield from (1).("handle_request", kind="server")
 
    ,  = .
    assert (., .) == ("handle_request", )
    assert (., E.Tracer.)

More examples: test_tracer.py.