Structured tracing for MoonBit with spans, structured fields, pluggable subscribers.
Dependencies
moon add brickfrog/moontracefn 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")
})
}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@moontrace.trace("verbose detail")
@moontrace.debug("debugging info")
@moontrace.info("normal operation")
@moontrace.warn("something unexpected")
@moontrace.error("something broke")@moontrace.info("request handled",
fields=[
@moontrace.field("method", "GET"),
@moontrace.field("path", "/api/users"),
@moontrace.field("status", 200),
])@moontrace.set_global_field("service", "my-app")
@moontrace.set_global_field("version", "1.2.0")
@moontrace.info("started") // automatically includes service and version fields@moontrace.remove_global_field("version") // remove a single field
@moontrace.clear_global_fields() // remove all@moontrace.set_module_filter("my/db/package", @moontrace.Debug)
@moontrace.set_module_filter("my/http/package", @moontrace.Warn)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)
}match @moontrace.initialize_from_env() {
Ok(_) => ()
Err(err) => println(err)
}@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")
})
})
})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@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
}
})// 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")
})
})let s = @moontrace.span_with_trace(
"handle_request",
"4bf92f3577b34da6a3ce929d0e0e4736",
)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)
}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(_) => ()
}@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
})
}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()
})
})
}@moontrace.set_subscriber(fn(event) {
println(event.format())
})@console.initialize()
// or with options:
@moontrace.set_subscriber(@console.subscriber(
min_level=@moontrace.Info,
color=true,
))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@json.initialize()
// or with options:
@moontrace.set_subscriber(@json.subscriber(min_level=@moontrace.Warn))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 recordspub 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()
}@moontrace.set_subscriber(@moontrace.compose([
@console.subscriber(min_level=@moontrace.Info),
@json.subscriber(min_level=@moontrace.Debug),
]))@moontrace.set_subscriber(
@moontrace.with_filter(@json.subscriber(), @moontrace.Warn)
)// 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()) },
))let buf = @moontrace.buffer(@json.subscriber(), capacity=100)
@moontrace.set_subscriber(buf.subscriber())
// ... events are batched ...
buf.flush() // send all buffered eventslet 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())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()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()
})
}@moontrace.set_min_level(@moontrace.Info)
// trace() and debug() calls now short-circuit before allocating Eventlet event_json = event.to_json().stringify()
let span_json = span.to_json().stringify()let plain = event.format() // no color
let colored = event.format(color=true) // ANSI coloredprintln(event) // human-readable output
let s = "\{span}" // string interpolation works@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 subscribergit clone https://github.com/brickfrog/moontrace
cd moontrace
git config core.hooksPath .githooks
moon test --target nativepub(all) struct RateLimiter {
inner : (Event) -> Unit
limit : Int
window_ns : UInt64
clock : () -> UInt64
window_start : UInt64
count : Int
kept : Int
dropped : Int
}pub(all) struct RatioSampler {
inner : (Event) -> Unit
ratio : Double
rand : () -> Double
kept : Int
dropped : Int
}impl Show for RedactionModepub(all) struct RedactionPolicy {
deny : Array[String]
allow : Array[String]?
mode : RedactionMode
placeholder : String
} derive(Debug)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)pub struct SpanContext {
trace_id : String
span_id : String
flags : TraceFlags
trace_state : TraceState
is_remote : Bool
} derive(Debug)pub(all) struct SpanLink {
trace_id : String
span_id : String
kind : SpanLinkKind
fields : Array[Field]
trace_state : String
} derive(Debug)impl Show for SpanLinkKindimpl Show for SpanStatusfn TraceState::from_entries(entries : Array[TraceStateEntry]) -> Result[TraceState, TraceContextError]fn TraceState::set(self : TraceState, key : String, value : String) -> Result[TraceState, TraceContextError]#deprecated("Use `format_timestamp_utc` instead")
fn format_timestamp_hms(timestamp : UInt64) -> Stringfn format_timestamp_utc(timestamp : UInt64) -> Stringfn parse_span_context(traceparent : String, tracestate? : String) -> Result[SpanContext, TraceContextError]fn rate_limiter(inner : (Event) -> Unit, limit : Int, window_ns : UInt64, clock? : () -> UInt64) -> RateLimiterfn redaction_policy(deny? : Array[String], allow? : Array[String]?, mode? : RedactionMode, placeholder? : String) -> RedactionPolicyfn span_context(trace_id : TraceId, span_id : SpanId, flags? : TraceFlags, trace_state? : TraceState, is_remote? : Bool) -> SpanContextfn span_context_from_ids(trace_id : String, span_id : String, flags? : TraceFlags, trace_state? : TraceState, is_remote? : Bool) -> Result[SpanContext, TraceContextError]fn span_from_remote_context(ctx : SpanContext, name : String, fields? : Array[Field], kind? : SpanKind) -> Spanfn span_link_from_context(ctx : SpanContext, kind? : SpanLinkKind, fields? : Array[Field]) -> SpanLinkfn[T] with_trace_state(f : () -> T raise?) -> T raise?Structured tracing for MoonBit with spans, structured fields, pluggable subscribers.
Dependencies