Sign in

    moontrace

    Structured tracing for MoonBit with spans, structured fields, pluggable subscribers.

    tracing
    logging
    spans
    structured-logging
    observability
    Download zip
    Author
    Version
    0.14.0
    License
    Apache-2.0
    Last updated
    last month
    Downloads
    218

    #brickfrog/moontrace

    Structured tracing for MoonBit. Spans, structured fields, pluggable subscribers.

    Inspired by Rust's tracing crate and loguru's developer experience.

    #Install

    moon add brickfrog/moontrace

    #Quick Start

    fn main {
    // One-liner setup with colored console output
    @console.initialize()

    // Structured events
    @moontrace.info("server started", fields=[@moontrace.field("port", 8080)])
    @moontrace.warn("slow query", fields=[@moontrace.field("ms", 250)])

    // Spans with duration tracking
    @moontrace.with_span("handle_request", fn() {
    @moontrace.info("processing")
    })
    }

    Output:

    14:31:43.903 | INFO | myapp — server started port=8080 14:31:43.904 | WARN | myapp — slow query ms=250 14:31:43.904 | TRACE | moontrace — span.enter handle_request 14:31:43.904 | INFO | myapp — processing 14:31:43.904 | TRACE | moontrace — span.exit handle_request duration_ns=12345

    #Events

    Fire-and-forget structured log entries at five levels:

    @moontrace.trace("verbose detail")
    @moontrace.debug("debugging info")
    @moontrace.info("normal operation")
    @moontrace.warn("something unexpected")
    @moontrace.error("something broke")

    Attach structured fields to any event:

    @moontrace.info("request handled",
    fields=[
    @moontrace.field("method", "GET"),
    @moontrace.field("path", "/api/users"),
    @moontrace.field("status", 200),
    ])

    The field() function uses MoonBit's ToJson trait dispatch — any type implementing ToJson works. For raw Json values, use fields() instead.

    Every event automatically captures its source package via SourceLoc, available as event.source.

    #Global Context

    Set fields once, automatically included in every event:

    @moontrace.set_global_field("service", "my-app")
    @moontrace.set_global_field("version", "1.2.0")

    @moontrace.info("started") // automatically includes service and version fields

    Explicit fields override global context on key collision.

    @moontrace.remove_global_field("version") // remove a single field
    @moontrace.clear_global_fields() // remove all

    #Per-Module Filtering

    Filter log levels by package:

    @moontrace.set_module_filter("my/db/package", @moontrace.Debug)
    @moontrace.set_module_filter("my/http/package", @moontrace.Warn)

    Module names are extracted automatically from SourceLoc at each call site. No manual tagging needed. A package filter overrides the global minimum level for matching packages, so it can be more verbose or stricter than the default.

    EnvFilter-style directives configure the same state from a string:

    match @moontrace.set_filter_from_directives(
    "info,my/db/package=debug,my/http/package=warn",
    ) {
    Ok(_) => ()
    Err(err) => println(err)
    }

    match @moontrace.parse_env_filter("warn,my/queue/package=trace") {
    Ok(filter) => filter.apply()
    Err(err) => println(err)
    }

    initialize_from_env() reads MOONTRACE_LOG by default. Use it during application startup:

    match @moontrace.initialize_from_env() {
    Ok(_) => ()
    Err(err) => println(err)
    }

    #Scoped State Guards

    For tests or temporary overrides, wrap work in a synchronous scope:

    @moontrace.with_subscriber(collecting_subscriber, fn() {
    @moontrace.with_min_level(@moontrace.Debug, fn() {
    @moontrace.with_global_field("request_id", "abc", fn() {
    @moontrace.debug("captured only inside this scope")
    })
    })
    })

    with_trace_state(fn() { ... }) snapshots the subscriber, global minimum level, global context fields, and module filters, then restores them when the closure returns or raises. The convenience helpers with_subscriber, with_min_level, with_global_field, with_global_fields, and with_module_filter use the same guard, so state changed inside nested scopes restores in LIFO order and does not leak into later tests.

    These guards are synchronous call-stack scopes only. They are not task-local storage and do not propagate tracing state across async tasks.

    #Spans

    Spans track operations with enter/exit lifecycle, duration, and distributed tracing IDs:

    let s = @moontrace.span("db_query")
    .with_field("table", "users")
    .with_field("limit", 100)
    s.enter()
    // ... do work ...
    s.record("rows", 42) // add fields after creation
    s.exit()
    // s.duration() returns elapsed nanoseconds

    Spans are single-use lifecycles: exit() closes a span, and later enter()/exit() calls on the same span are ignored. Create a new child span for repeated or nested work.

    Every span gets auto-generated trace_id (32 hex chars) and span_id (16 hex chars).

    #Error Handling on Spans

    @moontrace.with_span_ctx("db_query", fn(s) {
    match run_query() {
    Ok(result) => s.set_status(@moontrace.Ok)
    Err(e) => s.record_error(e.to_string()) // sets SpanError + records error field
    }
    })

    record_error sets the span status to SpanError, records the error message as a field, and includes both in the span's exit event and JSON output.

    #Convenience Wrappers

    // Auto enter/exit
    @moontrace.with_span("operation", fn() {
    @moontrace.info("inside span")
    })

    // Access the span inside the closure
    @moontrace.with_span_ctx("operation", fn(span) {
    span.record("result", "ok")
    })

    // Nested spans with parent linking
    @moontrace.with_span_ctx("parent_op", fn(parent) {
    @moontrace.with_child_span(parent, "child_op", fn(_child) {
    @moontrace.info("in child span")
    })
    })

    with_child_span propagates the parent's trace_id and sets parent_span_id, linking spans in a trace.

    #Cross-Process Trace Correlation

    For simple propagation, inject an existing trace ID:

    let s = @moontrace.span_with_trace(
    "handle_request",
    "4bf92f3577b34da6a3ce929d0e0e4736",
    )

    For HTTP or RPC boundaries, parse and format W3C trace-context headers:

    let incoming = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" match @moontrace.parse_traceparent(incoming) { Ok(remote) => { let server = @moontrace.span_from_remote_context( remote, "handle_request", kind=@moontrace.Server, ) match @moontrace.span_context_from_span(server) { Ok(local) => { let _outgoing = @moontrace.format_traceparent(local) () } Err(_) => () } } Err(err) => println(err) } match @moontrace.parse_tracestate("rojo=00f067aa0ba902b7,congo=t61rcWkgMzE") { Ok(state) => { let _header = @moontrace.format_tracestate(state) () } Err(err) => println(err) }

    SpanContext keeps the remote trace ID, span ID, sampled flag, tracestate, and remote/local marker. span_from_remote_context creates a local child span while preserving the incoming sampled flag for downstream export.

    Incoming traceparent and tracestate values are limited to 512 characters. Serialized tracestate values may contain at most 32 non-empty members and must not repeat a key. parse_span_context preserves a valid traceparent while dropping malformed, oversized, or duplicate-key tracestate, matching its partial-recovery behavior.

    Parent/child spans model ownership. Links model causal edges that should not change the parent relationship, such as retries, queued work, or fan-in.

    let enqueue = @moontrace.span("enqueue")
    let retry = @moontrace.span("retry")

    retry.link(enqueue, fields=[@moontrace.field("edge", "same trace")])
    retry.follows_from(enqueue, fields=[@moontrace.field("reason", "retry")])

    match @moontrace.parse_traceparent(
    "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
    ) {
    Ok(remote) => retry.link_context(remote, fields=[
    @moontrace.field("edge", "remote"),
    ])
    Err(_) => ()
    }

    Use link_context and follows_from_context when the related operation came from a remote SpanContext. OTLP export includes these span links separately from parent_span_id.

    #Async Span Propagation

    MoonBit has no task-local storage, so spans must be passed explicitly across async boundaries:

    @moontrace.with_span_ctx("request", fn(parent) {
    async_work(parent)
    })

    pub async fn async_work(parent : @moontrace.Span) -> Unit {
    @moontrace.with_child_span(parent, "async_step", fn(_s) {
    // child inherits parent's trace_id
    })
    }

    For async closures that should keep a span active across @async.sleep, @async.pause, or task-group cancellation, use the optional brickfrog/moontrace/span_async package. It intentionally avoids the package name async so applications can still import moonbitlang/async as @async.

    pub async fn handle_request() -> Unit {
    @span_async.with_span_async("request", parent => {
    @async.sleep(1)
    @span_async.with_child_span_async(parent, "async_step", child => {
    child.record("phase", "load")
    @async.pause()
    })
    })
    }

    with_span_async and with_child_span_async enter before the async closure runs and exit when it returns or raises. Errors, including structured cancellation delivered through MoonBit async error/catch paths, record SpanError before exit and are re-raised. This is best-effort structured cancellation instrumentation, not Rust Drop-guard semantics; out-of-band abort/kill is out of scope.

    #Subscribers

    Subscribers receive events. Set one globally:

    @moontrace.set_subscriber(fn(event) {
    println(event.format())
    })

    #Console Subscriber

    Human-readable colored output with timestamps, source, and pipe separators:

    @console.initialize()
    // or with options:
    @moontrace.set_subscriber(@console.subscriber(
    min_level=@moontrace.Info,
    color=true,
    ))

    Output:

    14:31:43.903 | INFO | server — UDS server listening path=".choir/server.sock" 14:31:44.012 | WARN | poller — retry attempt=3 max=5 14:31:44.500 | ERROR | handler — delivery failed target="leaf-1" exit_code=2

    Human-readable formatting keeps each event on one physical record. C0/C1 control codes in event messages, field keys, and trace-context diagnostics are rendered visibly (\\n, \\r, \\t, or \\u{001b}-style escapes). Printable Unicode is preserved. The JSON subscriber remains governed by JSON string escaping and is unchanged.

    #JSON Subscriber

    Machine-readable JSON output:

    @json.initialize()
    // or with options:
    @moontrace.set_subscriber(@json.subscriber(min_level=@moontrace.Warn))

    #OTLP Export

    Convert events and completed spans to OpenTelemetry-compatible JSON:

    let resource = @otlp.resource(
    service_name="checkout-api",
    service_version="1.2.3",
    )
    let scope = @otlp.instrumentation_scope(
    name="checkout-worker",
    version="0.12.0",
    )
    let exp = @otlp.exporter(
    log_output=fn(_json) { () },
    span_output=fn(_json) { () },
    capacity=100,
    resource~,
    scope~,
    )
    @moontrace.set_subscriber(exp.subscriber())
    @moontrace.set_span_observer(exp.span_observer())

    let s = @moontrace.span("operation")
    s.enter()
    // ... work ...
    s.exit()
    exp.shutdown() // stop accepting new events, then flush buffered records

    span_observer() captures completed spans when they close, without parsing span.enter / span.exit trace messages or calling add_span manually. Manual add_span(@otlp.span_to_otlp(s)) remains available for spans collected outside the global observer path. The OTLP package handles format conversion, resource attributes, instrumentation scope metadata, span flags, and links.

    For OTLP/HTTP JSON export, use brickfrog/moontrace/otlp/transport. The transport package is async, provides a default HTTP client, accepts injected OtlpHttpClient implementations, and handles retry classification and backoff:

    pub async fn export_batch(
    spans : Array[@otlp.OtlpSpan],
    resource : @otlp.Resource,
    scope : @otlp.InstrumentationScope,
    ) -> Unit {
    let client = @transport.async_http_client(timeout_ms=5000)
    let tx = @transport.transport(client, "http://collector:4318")
    match tx.export_spans(spans, resource~, scope~) {
    Ok(outcome) =>
    if !outcome.success {
    println("OTLP export failed after \{outcome.attempts} attempts")
    }
    Err(err) => println(err)
    }
    tx.shutdown()
    client.shutdown()
    }

    The default HTTP client streams response bodies and retains at most 64 KiB. Larger responses return Request("HTTP response body exceeds 65536 bytes") and are not retried. Successful and error response bodies within the limit remain available through HttpResponse.body; injected clients keep the same public interface.

    #Subscriber Composition

    Route events to multiple subscribers:

    @moontrace.set_subscriber(@moontrace.compose([
    @console.subscriber(min_level=@moontrace.Info),
    @json.subscriber(min_level=@moontrace.Debug),
    ]))

    Filter events for any subscriber:

    @moontrace.set_subscriber(
    @moontrace.with_filter(@json.subscriber(), @moontrace.Warn)
    )

    #Utility Subscribers

    // No-op (for benchmarks/testing)
    @moontrace.set_subscriber(@moontrace.noop())

    // Intercept (run a side-effect before the main subscriber)
    @moontrace.set_subscriber(@moontrace.intercept(
    main_subscriber,
    fn(e) { metrics.increment(e.level.to_string()) },
    ))

    #Buffered Subscriber

    Batch events and flush on demand or at capacity:

    let buf = @moontrace.buffer(@json.subscriber(), capacity=100)
    @moontrace.set_subscriber(buf.subscriber())
    // ... events are batched ...
    buf.flush() // send all buffered events

    #Sampling and Redaction

    Sampling and redaction are subscriber wrappers. They compose with console, JSON, OTLP, buffers, and fan-out:

    let policy = @moontrace.redaction_policy(
    deny=["password", "authorization", "token"],
    placeholder="[redacted]",
    )
    let redacted = @moontrace.redact(@json.subscriber(), policy~)
    let ratio = @moontrace.ratio_sampler(redacted, 0.10)
    let limited = @moontrace.rate_limiter(
    ratio.subscriber(),
    100,
    1_000_000_000UL,
    )
    let traced = @moontrace.trace_sampler(limited.subscriber(), 0.25)

    @moontrace.set_subscriber(traced.subscriber())

    ratio_sampler, rate_limiter, and trace_sampler each expose subscriber(), kept(), and dropped(). should_sample_trace(trace_id, ratio) is available when you need the deterministic trace sampling decision without installing a subscriber.

    redact(inner, policy?) builds a redacted copy of the event for that subscriber. The default policy redacts fields whose names contain password, token, or secret; redaction_policy(...) can switch between replacing values and dropping fields.

    See docs/sampling.md for the sampling model.

    #Flame Export

    brickfrog/moontrace/flame observes completed spans and writes folded-stack output for flamegraph tools. It reconstructs stacks from span IDs and sums identical collapsed stacks.

    let flame = @flame.flame_exporter(output=fn(folded) { println(folded) })
    @moontrace.set_span_observer(flame.span_observer())

    @moontrace.with_span("request", fn() {
    @moontrace.with_span("db query", fn() { () })
    })

    flame.flush()
    @moontrace.clear_span_observer()

    See docs/flame.md for rendering with inferno-flamegraph or flamegraph.pl.

    #File Subscriber

    brickfrog/moontrace/file provides a native-only buffered JSONL file subscriber. The synchronous subscriber enqueues without blocking the logging call site; an async worker drains, rotates, optionally gzips rotated files, and tracks written/dropped counts.

    On Unix, files newly created by the subscriber request owner-only 0600 permissions. Existing file modes are left unchanged.

    pub async fn install_file_subscriber() -> Unit {
    let files = @file.file_subscriber(
    "logs/moontrace.jsonl",
    max_size_bytes=10 * 1024 * 1024,
    max_files=5,
    gzip=true,
    )

    @moontrace.set_subscriber(files.subscriber())
    @async.with_task_group(group => {
    group.spawn_bg(() => files.run())
    @moontrace.info("service started")
    files.flush()
    files.shutdown()
    })
    }

    See docs/file.md for worker ownership, rotation, retention, and native-target behavior.

    #Performance

    Set a global minimum level to skip event allocation entirely:

    @moontrace.set_min_level(@moontrace.Info)
    // trace() and debug() calls now short-circuit before allocating Event

    Global context fields use a fast path — zero allocation overhead when no context is set.

    #Serialization

    Events, spans, and fields all support JSON serialization:

    let event_json = event.to_json().stringify()
    let span_json = span.to_json().stringify()

    Span JSON includes trace_id, span_id, parent_span_id, status, duration, and links.

    Human-readable formatting uses pipe-separated columns:

    let plain = event.format() // no color
    let colored = event.format(color=true) // ANSI colored

    All types implement Show for println and string interpolation:

    println(event) // human-readable output
    let s = "\{span}" // string interpolation works

    #Architecture

    @moontrace # core — what libraries depend on @moontrace/json # JSON subscriber (structured output) @moontrace/console # console subscriber (colored, human-readable) @moontrace/otlp # OpenTelemetry JSON format conversion @moontrace/otlp/transport # async OTLP HTTP JSON transport @moontrace/test # test subscriber and assertion helpers @moontrace/span_async # async span wrappers @moontrace/flame # folded-stack flamegraph export @moontrace/file # native buffered JSONL file subscriber

    Libraries instrument with @moontrace. Applications choose subscribers.

    #Contributing

    Contributions are welcome. Please:

    1. moon fmt before committing
    2. moon test --target native must pass
    3. Run moon info && moon fmt if you change public APIs (updates .mbti files)
    4. Add tests for new features

    The pre-commit hook runs moon fmt and moon check automatically.

    #Development

    git clone https://github.com/brickfrog/moontrace cd moontrace git config core.hooksPath .githooks moon test --target native

    #License

    Apache-2.0

    EnvFilterError

    pub(all) enum EnvFilterError {
    InvalidLevel(String)
    MalformedDirective(String)
    } derive(Compare, Eq,
    Debug
    )

    EnvFilterError::to_string

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

    Event

    pub(all) struct Event {
    level : Level
    message : String
    fields : Array[Field]
    timestamp : UInt64
    source : String
    } derive(
    Debug
    )

    impl Show for Event

    Event::format

    fn Event::format(self : Event, color? : Bool) -> String

    Event::to_json

    fn Event::to_json(self : Event) -> Json

    EventBuffer

    pub(all) struct EventBuffer {
    events : Array[Event]
    capacity : Int
    inner : (Event) -> Unit
    }

    EventBuffer::flush

    fn EventBuffer::flush(self : EventBuffer) -> Unit

    EventBuffer::len

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

    EventBuffer::subscriber

    fn EventBuffer::subscriber(self : EventBuffer) -> ((Event) -> Unit)

    Field

    pub(all) struct Field {
    key : String
    value : Json
    } derive(
    Debug
    )

    impl Show for Field

    Field::to_json

    fn Field::to_json(self : Field) -> Json

    Level

    pub(all) enum Level {
    Trace
    Debug
    Info
    Warn
    Error_
    } derive(Compare, Eq,
    Debug
    )

    impl Show for Level

    Level::from_string

    fn Level::from_string(s : String) -> Level?

    Level::to_json

    fn Level::to_json(self : Level) -> Json

    Level::to_string

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

    ParsedFilter

    pub(all) struct ParsedFilter {
    default_level : Level
    module_filters : Map[String, Level]
    } derive(
    Debug
    )

    ParsedFilter::apply

    fn ParsedFilter::apply(self : ParsedFilter) -> Unit

    RateLimiter

    pub(all) struct RateLimiter {
    inner : (Event) -> Unit
    limit : Int
    window_ns : UInt64
    clock : () -> UInt64
    window_start : UInt64
    count : Int
    kept : Int
    dropped : Int
    }

    RateLimiter::dropped

    fn RateLimiter::dropped(self : RateLimiter) -> Int

    RateLimiter::kept

    fn RateLimiter::kept(self : RateLimiter) -> Int

    RateLimiter::subscriber

    fn RateLimiter::subscriber(self : RateLimiter) -> ((Event) -> Unit)

    RatioSampler

    pub(all) struct RatioSampler {
    inner : (Event) -> Unit
    ratio : Double
    rand : () -> Double
    kept : Int
    dropped : Int
    }

    RatioSampler::dropped

    fn RatioSampler::dropped(self : RatioSampler) -> Int

    RatioSampler::kept

    fn RatioSampler::kept(self : RatioSampler) -> Int

    RatioSampler::subscriber

    fn RatioSampler::subscriber(self : RatioSampler) -> ((Event) -> Unit)

    RedactionMode

    pub(all) enum RedactionMode {
    Redact
    Drop
    } derive(Compare, Eq,
    Debug
    )

    RedactionMode::to_string

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

    RedactionPolicy

    pub(all) struct RedactionPolicy {
    deny : Array[String]
    allow : Array[String]?
    mode : RedactionMode
    placeholder : String
    } derive(
    Debug
    )

    Span

    pub(all) struct Span {
    name : String
    fields : Array[Field]
    links : Array[SpanLink]
    trace_id : String
    span_id : String
    parent_span_id : String
    trace_flags : Int
    trace_state : TraceState
    kind : SpanKind
    start_time : UInt64
    end_time : UInt64
    active : Bool
    status : SpanStatus
    status_message : String
    } derive(
    Debug
    )

    impl Show for Span

    Span::debug

    #callsite(autofill(loc))
    fn Span::debug(self : Span, msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    Span::duration

    fn Span::duration(self : Span) -> UInt64

    Span::enter

    fn Span::enter(self : Span) -> Unit

    Span::error

    #callsite(autofill(loc))
    fn Span::error(self : Span, msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    Span::event

    #callsite(autofill(loc))
    fn Span::event(self : Span, level : Level, msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    Span::exit

    fn Span::exit(self : Span) -> Unit

    Span::follows_from

    fn Span::follows_from(self : Span, target : Span, fields? : Array[Field]) -> Unit

    Span::follows_from_context

    fn Span::follows_from_context(self : Span, ctx : SpanContext, fields? : Array[Field]) -> Unit

    Span::info

    #callsite(autofill(loc))
    fn Span::info(self : Span, msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    Span::is_active

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

    fn Span::link(self : Span, target : Span, fields? : Array[Field]) -> Unit

    fn Span::link_context(self : Span, ctx : SpanContext, fields? : Array[Field]) -> Unit

    Span::record

    fn[T : ToJson] Span::record(self : Span, key : String, value : T) -> Unit

    Span::record_error

    fn Span::record_error(self : Span, msg : String) -> Unit

    Span::set_status

    fn Span::set_status(self : Span, status : SpanStatus, message? : String?) -> Unit

    Span::to_json

    fn Span::to_json(self : Span) -> Json

    Span::trace

    #callsite(autofill(loc))
    fn Span::trace(self : Span, msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    Span::warn

    #callsite(autofill(loc))
    fn Span::warn(self : Span, msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    Span::with_field

    fn[T : ToJson] Span::with_field(self : Span, key : String, value : T) -> Span

    Span::with_kind

    fn Span::with_kind(self : Span, kind : SpanKind) -> Span

    SpanContext

    pub struct SpanContext {
    trace_id : String
    span_id : String
    flags : TraceFlags
    trace_state : TraceState
    is_remote : Bool
    } derive(
    Debug
    )

    SpanContext::is_remote

    fn SpanContext::is_remote(self : SpanContext) -> Bool

    SpanContext::is_sampled

    fn SpanContext::is_sampled(self : SpanContext) -> Bool

    SpanContext::trace_flags_int

    fn SpanContext::trace_flags_int(self : SpanContext) -> Int

    SpanContext::traceparent

    fn SpanContext::traceparent(self : SpanContext) -> String

    SpanId

    pub struct SpanId {
    value : String
    } derive(Compare, Eq,
    Debug
    )

    impl Show for SpanId

    SpanId::parse

    fn SpanId::parse(value : String) -> Result[SpanId, TraceContextError]

    SpanId::to_string

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

    SpanKind

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

    impl Show for SpanKind

    SpanKind::to_int

    fn SpanKind::to_int(self : SpanKind) -> Int

    SpanKind::to_json

    fn SpanKind::to_json(self : SpanKind) -> Json

    SpanKind::to_string

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

    SpanLifecycleObserver

    pub(all) struct SpanLifecycleObserver {
    on_created : (Span) -> Unit
    on_entered : (Span) -> Unit
    on_recorded : (Span, Field) -> Unit
    on_exited : (Span) -> Unit
    on_closed : (Span) -> Unit
    on_linked : (Span, SpanLink) -> Unit
    }

    pub(all) struct SpanLink {
    trace_id : String
    span_id : String
    kind : SpanLinkKind
    fields : Array[Field]
    trace_state : String
    } derive(
    Debug
    )

    SpanLink::to_json

    fn SpanLink::to_json(self : SpanLink) -> Json

    SpanLinkKind

    pub(all) enum SpanLinkKind {
    SpanLinked
    SpanFollowsFrom
    SpanChildOf
    } derive(Compare, Eq,
    Debug
    )

    SpanLinkKind::to_json

    fn SpanLinkKind::to_json(self : SpanLinkKind) -> Json

    SpanLinkKind::to_string

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

    SpanStatus

    pub(all) enum SpanStatus {
    Unset
    Ok
    SpanError
    } derive(Compare, Eq,
    Debug
    )

    impl Show for SpanStatus

    SpanStatus::to_int

    fn SpanStatus::to_int(self : SpanStatus) -> Int

    SpanStatus::to_json

    fn SpanStatus::to_json(self : SpanStatus) -> Json

    SpanStatus::to_string

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

    TraceContextError

    pub(all) enum TraceContextError {
    InvalidTraceId(String)
    InvalidSpanId(String)
    InvalidTraceFlags(String)
    InvalidTraceParent(String)
    UnsupportedTraceParentVersion(String)
    InvalidTraceState(String)
    } derive(Compare, Eq,
    Debug
    )

    TraceContextError::to_string

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

    TraceFlags

    pub struct TraceFlags {
    flags : Int
    } derive(Compare, Eq,
    Debug
    )

    impl Show for TraceFlags

    TraceFlags::from_hex

    fn TraceFlags::from_hex(value : String) -> Result[TraceFlags, TraceContextError]

    TraceFlags::from_int

    fn TraceFlags::from_int(flags : Int) -> Result[TraceFlags, TraceContextError]

    TraceFlags::is_sampled

    fn TraceFlags::is_sampled(self : TraceFlags) -> Bool

    TraceFlags::sampled

    fn TraceFlags::sampled() -> TraceFlags

    TraceFlags::to_hex

    fn TraceFlags::to_hex(self : TraceFlags) -> String

    TraceFlags::to_int

    fn TraceFlags::to_int(self : TraceFlags) -> Int

    TraceFlags::unsampled

    fn TraceFlags::unsampled() -> TraceFlags

    TraceFlags::with_sampled

    fn TraceFlags::with_sampled(self : TraceFlags, sampled : Bool) -> TraceFlags

    TraceId

    pub struct TraceId {
    value : String
    } derive(Compare, Eq,
    Debug
    )

    impl Show for TraceId

    TraceId::parse

    fn TraceId::parse(value : String) -> Result[TraceId, TraceContextError]

    TraceId::to_string

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

    TraceSampler

    pub(all) struct TraceSampler {
    inner : (Event) -> Unit
    ratio : Double
    kept : Int
    dropped : Int
    }

    TraceSampler::dropped

    fn TraceSampler::dropped(self : TraceSampler) -> Int

    TraceSampler::kept

    fn TraceSampler::kept(self : TraceSampler) -> Int

    TraceSampler::subscriber

    fn TraceSampler::subscriber(self : TraceSampler) -> ((Event) -> Unit)

    TraceState

    pub struct TraceState {
    // private fields
    } derive(
    Debug
    )

    TraceState::empty

    fn TraceState::empty() -> TraceState

    TraceState::from_entries

    fn TraceState::from_entries(entries : Array[TraceStateEntry]) -> Result[TraceState, TraceContextError]

    TraceState::get

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

    TraceState::is_empty

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

    TraceState::len

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

    TraceState::set

    fn TraceState::set(self : TraceState, key : String, value : String) -> Result[TraceState, TraceContextError]

    TraceState::to_header

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

    TraceStateEntry

    pub struct TraceStateEntry {
    key : String
    value : String
    } derive(Compare, Eq,
    Debug
    )

    apply_env_filter

    fn apply_env_filter(parsed : ParsedFilter) -> Unit

    Apply a parsed filter deterministically by replacing global filter state.

    buffer

    fn buffer(inner : (Event) -> Unit, capacity? : Int) -> EventBuffer

    child_span

    fn child_span(parent : Span, name : String, fields? : Array[Field], kind? : SpanKind) -> Span

    clear_global_fields

    fn clear_global_fields() -> Unit

    clear_module_filters

    fn clear_module_filters() -> Unit

    Clear all module-specific filters

    clear_span_observer

    fn clear_span_observer() -> Unit

    clear_subscriber

    fn clear_subscriber() -> Unit

    completed_span_observer

    fn completed_span_observer(on_completed : (Span) -> Unit) -> SpanLifecycleObserver

    compose

    fn compose(subscribers : Array[(Event) -> Unit]) -> ((Event) -> Unit)

    compose_span_observers

    fn compose_span_observers(observers : Array[SpanLifecycleObserver]) -> SpanLifecycleObserver

    debug

    #callsite(autofill(loc))
    fn debug(msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    debug_in_span

    #callsite(autofill(loc))
    fn debug_in_span(span : Span, msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    default_redaction_policy

    fn default_redaction_policy() -> RedactionPolicy

    error

    #callsite(autofill(loc))
    fn error(msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    error_in_span

    #callsite(autofill(loc))
    fn error_in_span(span : Span, msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    event_in_span

    #callsite(autofill(loc))
    fn event_in_span(span : Span, level : Level, msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    field

    fn[T : ToJson] field(key : String, value : T) -> Field

    fields

    fn fields(pairs : Array[(String, Json)]) -> Array[Field]

    fields_to_json

    fn fields_to_json(fields : Array[Field]) -> Json

    format_event

    fn format_event(event : Event, color : Bool, format_timestamp : (UInt64) -> String) -> String

    format_timestamp_hms

    #deprecated("Use `format_timestamp_utc` instead")
    fn format_timestamp_hms(timestamp : UInt64) -> String

    Deprecated: use format_timestamp_utc instead.

    format_timestamp_utc

    fn format_timestamp_utc(timestamp : UInt64) -> String

    Format a millisecond timestamp as HH:MM:SS.mmm in UTC.

    format_traceparent

    fn format_traceparent(ctx : SpanContext) -> String

    format_tracestate

    fn format_tracestate(state : TraceState) -> String

    get_global_field

    fn get_global_field(key : String) -> Json?

    get_min_level

    fn get_min_level() -> Level

    info

    #callsite(autofill(loc))
    fn info(msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    info_in_span

    #callsite(autofill(loc))
    fn info_in_span(span : Span, msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    initialize_from_env

    fn initialize_from_env(env_var? : String) -> Result[Unit, EnvFilterError]

    Initialize filters from MOONTRACE_LOG by default.

    Unset or ASCII-whitespace-only values are no-ops. Present non-empty values are parsed and applied, with invalid directives returned as structured errors so logging is never widened by ignoring a bad filter.

    intercept

    fn intercept(subscriber : (Event) -> Unit, on_event : (Event) -> Unit) -> ((Event) -> Unit)

    level_color

    fn level_color(level : Level) -> String

    next_span_id

    fn next_span_id() -> String

    next_trace_id

    fn next_trace_id() -> String

    noop

    fn noop() -> ((Event) -> Unit)

    parse_env_filter

    fn parse_env_filter(directive : String) -> Result[ParsedFilter, EnvFilterError]

    Parse a compact filter directive string.

    Supported directives are comma-separated bare levels (info) and exact package filters (brickfrog/moontrace=debug). Empty directives are skipped, duplicate defaults or package filters are accepted, and the last directive wins. If no bare default is present, the parsed default is Info.

    parse_span_context

    fn parse_span_context(traceparent : String, tracestate? : String) -> Result[SpanContext, TraceContextError]

    parse_traceparent

    fn parse_traceparent(header : String) -> Result[SpanContext, TraceContextError]

    parse_tracestate

    fn parse_tracestate(header : String) -> Result[TraceState, TraceContextError]

    rate_limiter

    fn rate_limiter(inner : (Event) -> Unit, limit : Int, window_ns : UInt64, clock? : () -> UInt64) -> RateLimiter

    ratio_sampler

    fn ratio_sampler(inner : (Event) -> Unit, ratio : Double, rand? : () -> Double) -> RatioSampler

    redact

    fn redact(inner : (Event) -> Unit, policy? : RedactionPolicy) -> ((Event) -> Unit)

    redaction_policy

    fn redaction_policy(deny? : Array[String], allow? : Array[String]?, mode? : RedactionMode, placeholder? : String) -> RedactionPolicy

    remove_global_field

    fn remove_global_field(key : String) -> Unit

    sample_trace_decision

    fn sample_trace_decision(parent_flags : TraceFlags?, trace_id : String, ratio : Double) -> Bool

    set_filter_from_directives

    fn set_filter_from_directives(directive : String) -> Result[Unit, EnvFilterError]

    set_global_field

    fn[T : ToJson] set_global_field(key : String, value : T) -> Unit

    set_min_level

    fn set_min_level(level : Level) -> Unit

    set_module_filter

    fn set_module_filter(module_name : String, level : Level) -> Unit

    Set a filter for a specific module to the given minimum level

    Parameters:
    • module_name - The package name (e.g., "my/package")
    • level - The minimum level to log for this module

    set_span_observer

    fn set_span_observer(observer : SpanLifecycleObserver) -> Unit

    set_subscriber

    fn set_subscriber(f : (Event) -> Unit) -> Unit

    should_sample_trace

    fn should_sample_trace(trace_id : String, ratio : Double) -> Bool

    span

    fn span(name : String, fields? : Array[Field], kind? : SpanKind) -> Span

    span_context

    fn span_context(trace_id : TraceId, span_id : SpanId, flags? : TraceFlags, trace_state? : TraceState, is_remote? : Bool) -> SpanContext

    span_context_from_ids

    fn span_context_from_ids(trace_id : String, span_id : String, flags? : TraceFlags, trace_state? : TraceState, is_remote? : Bool) -> Result[SpanContext, TraceContextError]

    span_context_from_span

    fn span_context_from_span(span : Span) -> Result[SpanContext, TraceContextError]

    span_from_remote_context

    fn span_from_remote_context(ctx : SpanContext, name : String, fields? : Array[Field], kind? : SpanKind) -> Span

    span_id

    fn span_id(value : String) -> Result[SpanId, TraceContextError]

    span_lifecycle_observer

    fn span_lifecycle_observer(on_created? : (Span) -> Unit, on_entered? : (Span) -> Unit, on_recorded? : (Span, Field) -> Unit, on_exited? : (Span) -> Unit, on_closed? : (Span) -> Unit, on_linked? : (Span, SpanLink) -> Unit) -> SpanLifecycleObserver

    fn span_link(target : Span, kind? : SpanLinkKind, fields? : Array[Field]) -> SpanLink

    fn span_link_from_context(ctx : SpanContext, kind? : SpanLinkKind, fields? : Array[Field]) -> SpanLink

    span_trace_flags

    fn span_trace_flags(span : Span) -> TraceFlags

    span_trace_state

    fn span_trace_state(span : Span) -> TraceState

    span_with_trace

    fn span_with_trace(name : String, trace_id : String, fields? : Array[Field], kind? : SpanKind) -> Span

    trace

    #callsite(autofill(loc))
    fn trace(msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    trace_flags_sampled

    fn trace_flags_sampled() -> TraceFlags

    trace_flags_unsampled

    fn trace_flags_unsampled() -> TraceFlags

    trace_id

    fn trace_id(value : String) -> Result[TraceId, TraceContextError]

    trace_in_span

    #callsite(autofill(loc))
    fn trace_in_span(span : Span, msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    trace_sampler

    fn trace_sampler(inner : (Event) -> Unit, ratio : Double) -> TraceSampler

    trace_state

    fn trace_state(entries : Array[TraceStateEntry]) -> Result[TraceState, TraceContextError]

    trace_state_entry

    fn trace_state_entry(key : String, value : String) -> Result[TraceStateEntry, TraceContextError]

    traceparent_from_span

    fn traceparent_from_span(span : Span) -> Result[String, TraceContextError]

    warn

    #callsite(autofill(loc))
    fn warn(msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    warn_in_span

    #callsite(autofill(loc))
    fn warn_in_span(span : Span, msg : String, fields? : Array[Field], loc~ : SourceLoc) -> Unit

    with_child_span

    fn[T] with_child_span(parent : Span, name : String, fields? : Array[Field], f : (Span) -> T) -> T

    with_filter

    fn with_filter(subscriber : (Event) -> Unit, min_level : Level) -> ((Event) -> Unit)

    with_global_field

    fn[V : ToJson, T] with_global_field(key : String, value : V, f : () -> T raise?) -> T raise?

    Run f with one scoped global context field, then restore the previous tracing globals. This is a synchronous scope, not task-local async propagation.

    with_global_fields

    fn[T] with_global_fields(fields : Array[Field], f : () -> T raise?) -> T raise?

    Run f with additional scoped global context fields, then restore the previous tracing globals. Later fields with the same key overwrite earlier scoped values. This is a synchronous scope, not task-local async propagation.

    with_min_level

    fn[T] with_min_level(level : Level, f : () -> T raise?) -> T raise?

    Run f with a scoped global minimum level, then restore the previous tracing globals. This is a synchronous scope, not task-local async propagation.

    with_module_filter

    fn[T] with_module_filter(module_name : String, level : Level, f : () -> T raise?) -> T raise?

    Run f with one scoped module filter, then restore the previous tracing globals. This is a synchronous scope, not task-local async propagation.

    with_span

    fn[T] with_span(name : String, fields? : Array[Field], f : () -> T) -> T

    with_span_ctx

    fn[T] with_span_ctx(name : String, fields? : Array[Field], f : (Span) -> T) -> T

    with_subscriber

    fn[T] with_subscriber(subscriber : (Event) -> Unit, f : () -> T raise?) -> T raise?

    Run f with subscriber installed, then restore the previous tracing globals. This is a synchronous scope, not task-local async propagation.

    with_trace_state

    fn[T] with_trace_state(f : () -> T raise?) -> T raise?

    Run f with the current tracing globals guarded by a synchronous scope.

    The subscriber, span observer, global minimum level, global context fields, and module filters are snapshotted before f runs and restored when f returns or raises. This is only a synchronous guard for the current call stack; it is not task-local storage and does not propagate tracing state across async tasks.