moontrace

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

tracing
logging
spans
structured-logging
observability
moon add brickfrog/moontrace@0.13.1
Download zip
Author
Version
0.13.1
License
Apache-2.0
Last updated
15 days ago
Downloads
148
README

#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.