// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Encode finished spans as an OTLP/JSON `ExportTraceServiceRequest`.
///
/// The body is what `POST /v1/traces` takes: one resource, one scope, and the
/// spans. Field names are the lowerCamelCase of the protobuf schema, ids are
/// hexadecimal and timestamps are decimal strings, which is the mapping the OTLP
/// specification fixes for JSON.
pub fn encode_traces(
spans : Array[@trace.SpanData],
resource~ : Resource = unknown_service,
scope~ : Scope = moonbit_scope,
) -> String {
let w = JsonWriter::new()
w.open_object()
w.name("resourceSpans")
w.open_array()
w.open_object()
w.name("resource")
write_resource(w, resource)
w.name("scopeSpans")
w.open_array()
w.open_object()
w.name("scope")
write_scope(w, scope)
w.name("spans")
w.open_array()
for span in spans {
write_span(w, span)
}
w.close_array()
w.close_object()
w.close_array()
w.close_object()
w.close_array()
w.close_object()
w.text()
}
///|
fn write_span(w : JsonWriter, span : @trace.SpanData) -> Unit {
w.open_object()
w.name("traceId")
w.string(@base16.encode(span.context.trace_id[:]))
w.name("spanId")
w.string(@base16.encode(span.context.span_id[:]))
match span.parent {
Some(parent) => {
w.name("parentSpanId")
w.string(@base16.encode(parent[:]))
}
None => ()
}
w.name("name")
w.string(span.name)
w.name("kind")
w.raw(kind_code(span.kind).to_string())
w.name("startTimeUnixNano")
w.int64(span.start)
w.name("endTimeUnixNano")
w.int64(span.end)
if !span.attributes.is_empty() {
w.name("attributes")
write_attributes(w, span.attributes)
}
if !span.events.is_empty() {
w.name("events")
w.open_array()
for event in span.events {
w.open_object()
w.name("timeUnixNano")
w.int64(event.at)
w.name("name")
w.string(event.name)
if !event.attributes.is_empty() {
w.name("attributes")
write_attributes(w, event.attributes)
}
w.close_object()
}
w.close_array()
}
if !span.links.is_empty() {
w.name("links")
w.open_array()
for link in span.links {
w.open_object()
w.name("traceId")
w.string(@base16.encode(link.context.trace_id[:]))
w.name("spanId")
w.string(@base16.encode(link.context.span_id[:]))
if !link.attributes.is_empty() {
w.name("attributes")
write_attributes(w, link.attributes)
}
w.close_object()
}
w.close_array()
}
match span.status {
@trace.Unset => ()
@trace.Ok => {
w.name("status")
w.open_object()
w.name("code")
w.raw("1")
w.close_object()
}
@trace.Error(message) => {
w.name("status")
w.open_object()
w.name("code")
w.raw("2")
if message != "" {
w.name("message")
w.string(message)
}
w.close_object()
}
}
let dropped = span.dropped
if dropped.attributes > 0 {
w.name("droppedAttributesCount")
w.raw(dropped.attributes.to_string())
}
if dropped.events > 0 {
w.name("droppedEventsCount")
w.raw(dropped.events.to_string())
}
if dropped.links > 0 {
w.name("droppedLinksCount")
w.raw(dropped.links.to_string())
}
if span.context.flags != b'\x00' {
w.name("flags")
w.raw(span.context.flags.to_int().to_string())
}
w.close_object()
}
///|
/// The number OTLP gives a span kind: `SPAN_KIND_INTERNAL` is 1, and the four kinds
/// that describe a position in a distributed call follow it.
pub fn kind_code(kind : @trace.SpanKind) -> Int32 {
match kind {
@trace.Internal => 1
@trace.Server => 2
@trace.Client => 3
@trace.Producer => 4
@trace.Consumer => 5
}
}