// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// What produced the telemetry: the service, the host, the deployment.
///
/// Every signal carries the same resource, because it is what makes a span, a
/// metric point and a log line from one process recognisable as coming from one
/// place. The specification requires `service.name` to be present, so the default
/// here is the one it names for a program that never set one.
#derive(Eq, Debug)
pub(all) struct Resource {
attributes : Array[(String, @trace.Value)]
}
///|
pub extend Resource with Eq::{equal, not_equal}
///|
pub extend Resource with Debug::{to_repr}
///|
/// The resource a program that has not described itself exports under:
/// `service.name` is `unknown_service`, which is what the specification asks for
/// rather than a guess at the binary's name.
pub let unknown_service : Resource = {
attributes: [("service.name", @trace.Str("unknown_service"))],
}
///|
/// A resource with exactly these attributes.
pub fn Resource::new(attributes~ : Array[(String, @trace.Value)]) -> Resource {
{ attributes, }
}
///|
/// A resource with `service.name` set to `name` and nothing else.
pub fn Resource::of_service(name : String) -> Resource {
{ attributes: [("service.name", @trace.Str(name))], }
}
///|
/// The same resource with one more attribute, replacing a value under the same key.
pub fn Resource::with_attribute(
self : Resource,
key : String,
value : @trace.Value,
) -> Resource {
{ attributes: upsert(self.attributes, key, value), }
}
///|
/// The value under `key`, or `None` when the resource does not carry it.
pub fn Resource::get(self : Resource, key : String) -> @trace.Value? {
for attribute in self.attributes {
if attribute.0 == key {
return Some(attribute.1)
}
}
None
}
///|
/// The instrumentation scope: which library in the program produced the telemetry,
/// and at which version. A backend groups by it, which is how a reader can tell
/// the spans this library made from the ones a framework made.
#derive(Eq, Debug)
pub(all) struct Scope {
name : String
version : String?
attributes : Array[(String, @trace.Value)]
}
///|
pub extend Scope with Eq::{equal, not_equal}
///|
pub extend Scope with Debug::{to_repr}
///|
/// The scope this library exports under.
pub let moonbit_scope : Scope = {
name: "moonbitstack/moontel",
version: None,
attributes: [],
}
///|
/// A scope with this name, and a version when the caller knows one.
pub fn Scope::new(
name~ : String,
version? : String,
attributes~ : Array[(String, @trace.Value)] = [],
) -> Scope {
{ name, version, attributes, }
}
///|
fn write_resource(w : JsonWriter, resource : Resource) -> Unit {
w.open_object()
if !resource.attributes.is_empty() {
w.name("attributes")
write_attributes(w, resource.attributes)
}
w.close_object()
}
///|
fn write_scope(w : JsonWriter, scope : Scope) -> Unit {
w.open_object()
w.name("name")
w.string(scope.name)
match scope.version {
Some(version) => {
w.name("version")
w.string(version)
}
None => ()
}
if !scope.attributes.is_empty() {
w.name("attributes")
write_attributes(w, scope.attributes)
}
w.close_object()
}
///|
/// `attributes` with `key` set to `value`, in the position the key first held.
fn upsert(
attributes : Array[(String, @trace.Value)],
key : String,
value : @trace.Value,
) -> Array[(String, @trace.Value)] {
let out : Array[(String, @trace.Value)] = []
let mut replaced = false
for attribute in attributes {
if attribute.0 == key {
out.push((key, value))
replaced = true
} else {
out.push(attribute)
}
}
if !replaced {
out.push((key, value))
}
out
}