OpenTelemetry for MoonBit.
fn traced_request(rand : (Int32) -> Bytes, at : Int64) -> @trace.Span raise {
let tracer = @trace.Tracer::new(
rand~,
sink=@trace.Recorder::new(),
sampler=@trace.parent_based(root=@trace.always),
)
let span = tracer.start("GET /orders/{id}", kind=@trace.Server, at~)
span.set_attribute("http.response.status_code", @trace.Int(200L))
span.finish(at=at + 1_000_000L)
span
}moon add moonbitstack/moontel#tel.span("orders.place", kind=server, attr="item")
fn place(item : String) -> Int raise Sold {
...
}let receipt = place_traced(tracer, "tea", now=clock)
let nested = place_traced(tracer, "tea", now=clock, parent=span.context())rule(name: "span", command: "moonx moonbitstack/moontel/cmd/span -C \"$mod_dir\" \"$input\" -o \"$output\"")dev_build(rule: "span", input: "orders.mbt", output: "orders.g.mbt")
formatter(ignore: [ "orders.g.mbt" ])struct Printer {}
impl @trace.Sink for Printer with fn accept(self, span) {
ignore(self)
println("\{span.name} took \{span.duration()} ns")
}
fn traced_with_printer(rand : (Int32) -> Bytes, at : Int64) -> Unit raise {
let tracer = @trace.Tracer::new(rand~, sink=Printer::{}, sampler=@trace.always)
let span = tracer.start("GET /orders/{id}", at~)
span.finish(at=at + 1L)
}| Sampler | What it decides |
|---|---|
| always | Everything is sampled. |
| never | Nothing is sampled. |
| ratio(ratio=0.1) | A tenth of traces, chosen by the trace id. |
| parent_based(root=...) | The parent's decision, or root when there is no parent. The other four rows, a remote or local parent sampled or not, are arguments too. |
struct Everything {}
impl @trace.Sampler for Everything with fn should_sample(_, _) {
@trace.RecordAndSample
}
impl @trace.Sampler for Everything with fn description(_) {
"Everything"
}fn trace_on_request(headers : Array[(String, String)]) -> @trace.TraceContext? {
@propagate.extract(headers).context
}
fn inbound_headers() -> Array[(String, String)] {
[
("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"),
("tracestate", "vendor=abc"),
("baggage", "userId=alice"),
]
}fn forward(
headers : Array[(String, String)],
span : @trace.Span,
baggage : @propagate.Baggage,
) -> Array[(String, String)] {
@propagate.inject(headers, { context: Some(span.context()), baggage })
}fn layer(tracer : @trace.Tracer, now : () -> Int64) -> @asgi.Tracing {
@asgi.Tracing::new(tracer~, now~)
}fn traced(now : () -> Int64, rand : (Int32) -> Bytes) -> @trace.Tracer {
let transport = @http.HttpTransport::new()
let exporter = @otlp.TraceExporter::new(
transport~,
endpoint="http://localhost:4318",
resource=@otlp.Resource::of_service("orders"),
now~,
rand~,
)
@trace.Tracer::new(rand~, sink=exporter, sampler=@trace.parent_based(root=@trace.always))
}fn traced_binary(now : () -> Int64, rand : (Int32) -> Bytes) -> @trace.Tracer {
let transport = @http.HttpTransport::new()
let exporter = @otlp.TraceExporter::new(
transport~,
endpoint="http://localhost:4318",
encoding=@binary.protobuf,
now~,
rand~,
)
@trace.Tracer::new(rand~, sink=exporter, sampler=@trace.parent_based(root=@trace.always))
}async fn over_grpc(now : () -> Int64, rand : (Int32) -> Bytes) -> @otlp.TraceExporter {
let channel = @grpc.GrpcTransport::connect(host="localhost")
@otlp.TraceExporter::new(
transport=channel,
endpoint="http://localhost:4317",
encoding=@binary.protobuf,
now~,
rand~,
)
}fn failing(logs : &@log.Sink, span : @trace.Span, now : () -> Int64) -> Unit {
let logger = @log.Logger::new(sink=logs, now~, scope="orders/payments").inside(span)
logger.info("order placed", attributes=[("order.id", @trace.Int(42L))])
logger.warn("the card was retried once")
}fn record_latency(
provider : @meter.MeterProvider,
seconds : Double,
attributes : @meter.Attributes,
) -> Unit raise {
let meter = provider.meter(name="checkout")
let duration = meter.histogram(
"gen_ai.client.operation.duration",
description="GenAI operation duration.",
unit="s",
)
duration.record(seconds, attributes~, at=0L)
}
fn collect(provider : @meter.MeterProvider) -> Array[@otlp.Metric] {
provider.collect(at=1_000_000L)
}fn queue_depth(provider : @meter.MeterProvider) -> Unit raise {
let meter = provider.meter(name="checkout")
let queue = meter.observable_gauge(
"checkout.queue.depth",
description="Requests waiting.",
unit="{request}",
observe=fn(observer) { observer.observe(7.0) },
)
ignore(queue)
}fn exponential_latency(provider : @meter.MeterProvider) -> Unit raise {
let meter = provider.meter(name="checkout")
let latency = meter.exponential_histogram(
"checkout.latency",
description="Time a request took.",
unit="s",
)
latency.record(0.004, at=0L)
latency.record(0.052, at=0L)
latency.record(1.7, at=0L)
}fn viewed_provider() -> @meter.MeterProvider {
@meter.MeterProvider::new(
sink=@otlp.MetricRecorder::new(),
now=() => 0L,
views=[
@meter.View::new(
instrument="checkout.token.usage",
name="checkout.tokens",
attribute_keys=["gen_ai.provider.name"],
),
@meter.View::new(instrument="checkout.debug.tick", aggregation=@meter.Drop),
],
)
}fn with_current_span(span : @trace.TraceContext) -> @meter.MeterProvider {
@meter.MeterProvider::new(
sink=@otlp.MetricRecorder::new(),
now=() => 0L,
current=() => Some(span),
)
}fn chat_attributes() -> @meter.Attributes {
@meter.gen_ai_client_attributes(
provider="openai",
operation="chat",
request_model="gpt-5",
)
}fn export(
metrics : @otlp.MetricsExporter,
now : () -> Int64,
elapsed : Double,
) -> Unit raise {
let registry = @metric.Registry::new()
let latency = registry.histogram("order_latency", "Latency.", unit="seconds")
latency.observe(elapsed)
for metric in @p8s.bridge(registry.collect(at=0.0), at=now()) {
metrics.record(metric)
}
}| Item | Signature |
|---|---|
| Tracer::new | (rand~ : (Int32) -> Bytes, sink~ : &Sink, sampler~ : &Sampler, limits? : Limits) -> Tracer |
| Tracer::start | (name, parent? : TraceContext, kind? : SpanKind, at~ : Int64, attributes?, links?) -> Span raise Refused |
| Span::finish | (at~ : Int64) -> Unit |
| Span::set_attribute | (key : String, value : Value) -> Unit |
| Span::set_attributes | (Array[(String, Value)]) -> Unit |
| Span::add_event | (name, attributes?, at~ : Int64) -> Unit |
| Span::record_exception | (message~, at~, type_?, stacktrace?, attributes?) -> Unit |
| Span::set_status | (Status) -> Unit |
| Span::set_name | (String) -> Unit |
| Span::add_link | (Link) -> Unit |
| Span::context | () -> TraceContext |
| Span::is_recording, is_ended | () -> Bool |
| SpanData | context, parent, name, kind, start, end, attributes, events, links, status, dropped |
| SpanData::sampled, duration | () -> Bool, () -> Int64 |
| TraceContext | trace_id : Bytes, span_id : Bytes, flags : Byte, state : TraceState |
| TraceContext::to_traceparent | () -> String |
| TraceState::parse | (StringView) -> TraceState raise Refused |
| TraceState::to_header | () -> String |
| TraceState::get, insert, delete | read a member, add one closest to this hop, drop one |
| Sampler | should_sample(SampleRequest) -> Decision, description() -> String |
| Sink | accept(SpanData) -> Unit |
| Limits | attributes, events, links, attributes_per_event, attribute_value_length |
| Status | Unset, Ok, Error(String) |
| SpanKind | Internal, Server, Client, Producer, Consumer |
| Value | Bool, Int, Double, Str, and the four typed arrays |
| Item | Signature |
|---|---|
| extract | (Array[(String, String)]) -> Extracted |
| inject | (Array[(String, String)], Extracted) -> Array[(String, String)] |
| parse | (traceparent : StringView, tracestate? : StringView) -> TraceContext raise Refused |
| parse_traceparent | (StringView) -> TraceContext raise Refused |
| parse_tracestate | (StringView) -> TraceState raise Refused |
| Baggage | members : Array[Member], with parse, to_header, get, len, add |
| Item | Signature |
|---|---|
| Tracing::new | (tracer~, now~ : () -> Int64, name?, baggage? : Bool, ignore? : Array[String]) -> Tracing |
| Tracing::wrap | (AsgiApp) -> AsgiApp, and Layer for stacking with other middleware |
| Item | Signature |
|---|---|
| Logger::new | (sink~ : &Sink, now~ : () -> Int64, scope?, attributes?, floor? : Severity, trace_based? : Bool, limits? : Limits) -> Logger |
| Logger::log | (severity : Severity, body : Value, attributes?, at? : Int64, observed? : Int64, event_name? : String, error? : Error) -> Unit |
| Logger::write, trace, debug, info, warn, error, fatal | a text body at one severity |
| Logger::inside | (Span) -> Logger — every record it writes names that span |
| Logger::with_context | (TraceContext) -> Logger |
| Logger::under, with_attribute, enabled | name a nested scope, carry a field, ask before building a body |
| LogRecord | time, observed, severity, severity_text, body, attributes, trace_id, span_id, flags, event_name, scope, dropped |
| LogRecord::with_context, with_attribute, with_event_name, timestamp, context | build one, and read what it names |
| Severity | Trace, Debug, Info, Warn, Error, Fatal, numbered 1, 5, 9, 13, 17, 21 |
| Sink | accept(LogRecord) -> Unit |
| Recorder, Discard, Fanout, SpanEvents | in memory, thrown away, several sinks, and a bridge onto the span |
| Item | Signature |
|---|---|
| MeterProvider::new | (sink~ : &MetricSink, now~ : () -> Int64, temporality? : Temporality, limits? : MeterLimits, exemplar? : ExemplarFilter, current? : () -> TraceContext?, views? : Array[View], start? : Int64) -> MeterProvider |
| MeterProvider::meter | (name~ : String, version? : String, attributes?) -> Meter |
| MeterProvider::collect | (at~ : Int64) -> Array[Metric] — run the callbacks, then the points as they stand, without writing them |
| MeterProvider::emit | (at~ : Int64) -> Array[Metric] — collect, write every metric to the sink, and clear a delta reader |
| Meter::counter, up_down_counter, gauge | (name, description?, unit?) -> … raise Refused |
| Meter::histogram | (name, description?, unit?, bounds? : Array[Double]) -> Histogram raise Refused |
| Meter::exponential_histogram | (name, description?, unit?, scale? : Int32, max_buckets? : Int32) -> ExponentialHistogram raise Refused |
| Meter::observable_counter, observable_up_down_counter, observable_gauge | (name, description?, unit?, observe~ : (Observer) -> Unit) -> … raise Refused |
| Observer::observe | (value : Double, attributes?, at? : Int64, context? : TraceContext) — one series' value at this collection |
| Counter::add, inc | (value : Double, attributes?, at? : Int64, context? : TraceContext) — inc adds one |
| UpDownCounter::add | (value : Double, attributes?, at?, context?) — either sign |
| Gauge::set | (value : Double, attributes?, at?, context?) — the last value wins |
| Histogram::record, ExponentialHistogram::record | (value : Double, attributes?, at?, context?) — into the bucket that contains it |
| View::new | (instrument~ : String, kind? : InstrumentKind, name?, description?, attribute_keys? : Array[String], aggregation? : Aggregation) -> View |
| Aggregation | Sum, LastValue, ExplicitBuckets(bounds), ExponentialBuckets(scale, max_buckets), Drop |
| ExemplarFilter | TraceBased (the default), AlwaysOn, AlwaysOff |
| Attributes | new, with_attribute, get, to_array, len, identity |
| gen_ai_client_attributes | (provider~, operation~, request_model~, response_model?, agent_name?, conversation_id?) -> Attributes |
| MeterLimits | attributes 128, series 2000, exemplars 1, attribute_value_length |
| default_buckets, max_exponential_buckets | the metrics SDK's fifteen boundaries and its 160-bucket default |
| exponential_index, max_exponential_scale, min_exponential_scale | the bucket rule, and the scale range the SDK defines |
| InstrumentKind, Refused | what a name is registered as, and why a name or a bucket list was refused |
| Item | Signature |
|---|---|
| TraceExporter::new | (transport~ : &Transport, endpoint? : String, headers?, resource? : Resource, scope? : Scope, policy? : BatchPolicy, backoff? : Backoff, attempts? : Int32, now~ : () -> Int64, rand~ : (Int32) -> Bytes) -> TraceExporter |
| LogsExporter::new, MetricsExporter::new | the same shape for the other two signals |
| flush, tick, run | send everything, send what is due, keep sending |
| Report, Stats | delivered, rejected, dropped, attempts per flush, and since the exporter was made |
| BatchPolicy | queue_limit 2048, batch 512, delay 5000, timeout 30000 |
| BodyLimits | request 64 MiB, response 4 MiB — the protocol's recommended sizes |
| Encoding | format, traces, logs, metrics, rejected — how a body is written |
| json | the JSON encoding, which is what an exporter writes unless told otherwise |
| BodyFormat | Json, Protobuf — what an encoding reports, and the media type that follows |
| Backoff | initial 5000, multiplier 1.5, maximum 30000, jitter 0.2 |
| Transport | async fn send(Self, Request) -> Reply raise Failed |
| Resource, Scope | what produced the telemetry, and which library in it did |
| Metric, MetricKind, DataPoint, Point, Temporality, Exemplar | the OTLP metric shapes, for a producer that has its own numbers |
| MetricSink | accept(Metric) -> Unit — where a producer writes finished metrics |
| MetricRecorder | new, metrics, len, take — the metrics kept in memory |
| Item | Signature |
|---|---|
| HttpTransport::new | (timeout? : Int32, compression? : Compression) -> HttpTransport |
| otlp/grpc.GrpcTransport::connect | (host~ : String, port? : Int32, encoding?) -> GrpcTransport — one unary call per request |
| otlp/binary.protobuf | &Encoding — the binary body, and the messages it is made of |
| otlp/binary.encode_traces, encode_logs, encode_metrics | (records, resource~, scope~) -> Bytes — a body without an exporter |
| Compression | Plain, Gzip — the second sets Content-Encoding on the request |
| bridge, bridge_with_report | (families : Array[MetricFamily], at~ : Int64, start? : Int64) -> Array[Metric] — and the same with what was dropped |
| gen_ai_request_model, gen_ai_client_inference_duration, … | 85 attribute keys, 21 metric names, 13 span types and 11 event names |
moon run src/examples/00-hello # a root span, a child, attributes and an event
moon run src/examples/01-propagate # reading a request's context, writing the next one's
moon run src/examples/02-asgi # the middleware over an application
moon run src/examples/03-annotated # the annotation, and the wrapper it generates
moon run src/examples/04-otlp # three signals, to a collector
moon run src/examples/05-grpc # the same three signals, over gRPC
moon run src/examples/06-meter # observed instruments, views and an exponential histogramlet baggage_byte_limit : Intlet baggage_member_limit : Intfn gen_ai_client_attributes(provider~ : String, operation~ : String, request_model~ : String, response_model? : String, agent_name? : String, conversation_id? : String) -> Attributeslet instrument_name_limit : Intfn is_valid_name(name : String) -> Boolfn method_and_path(http_method : String, path : String) -> Stringversion(2)-trace-id(32)-parent-id(16)-trace-flags(2)let random_flag : Bytelet sampled_flag : Bytelet traceparent_header : Stringlet tracestate_header : Stringlet tracestate_limit : IntInstall
Download zipOpenTelemetry for MoonBit.