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))
}let channel = @grpc.GrpcTransport::connect(host="localhost")
let traces = @otlp.TraceExporter::new(
transport=channel,
endpoint="http://localhost:4317",
format=@otlp.BodyFormat::Protobuf,
now=millis,
rand=random_bytes,
)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 export(metrics : @otlp.MetricsExporter, now : () -> Int64) -> 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 |
|---|---|
| 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 |
| BodyFormat | Json, Protobuf — Content-Type and encoder in one |
| 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 |
| Item | Signature |
|---|---|
| HttpTransport::new | (timeout? : Int32, compression? : Compression) -> HttpTransport |
| otlp/grpc.GrpcTransport::connect | (host~ : String, port? : Int32, encoding?) -> GrpcTransport — one unary call per request |
| 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/04-otlp # three signals, to a collectorlet baggage_byte_limit : Intlet baggage_member_limit : Intfn 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.