// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
fn with_pair(
labels : Array[(String, String)],
name : String,
value : String,
) -> Array[(String, String)] {
let out : Array[(String, String)] = []
for p in labels {
out.push(p)
}
out.push((name, value))
out
}
///|
fn compare_pair(a : (String, String), b : (String, String)) -> Int32 {
String::lexical_compare(a.0, b.0)
}
///|
fn render_labels(labels : Array[(String, String)]) -> String {
if labels.length() == 0 {
return ""
}
let sorted : Array[(String, String)] = []
for p in labels {
sorted.push(p)
}
sorted.sort_by(fn(a, b) { compare_pair(a, b) })
let sb = StringBuilder()
sb.write_char('{')
for i = 0; i < sorted.length(); i = i + 1 {
if i > 0 {
sb.write_char(',')
}
sb.write_string(sorted[i].0)
sb.write_string("=\"")
sb.write_string(escape_label_value(sorted[i].1))
sb.write_char('"')
}
sb.write_char('}')
sb.to_string()
}
///|
fn exemplar_text(ex : @metric.Exemplar) -> String {
if ex.labels.length() == 0 {
return ""
}
let sb = StringBuilder()
sb.write_string(" # ")
sb.write_string(render_labels(ex.labels))
sb.write_char(' ')
sb.write_string(format_float(ex.value))
match ex.timestamp {
Some(t) => {
sb.write_char(' ')
sb.write_string(format_float(t))
}
None => ()
}
sb.to_string()
}
///|
fn timestamp_text(ts : Double, milliseconds : Bool) -> String {
if milliseconds {
" " + (ts * 1000.0).to_int64().to_string()
} else {
" " + format_float(ts)
}
}
///|
fn write_line(
sb : StringBuilder,
name : String,
labels : Array[(String, String)],
value : String,
timestamp : Double?,
milliseconds : Bool,
exemplar : String,
) -> Unit {
sb.write_string(name)
sb.write_string(render_labels(labels))
sb.write_char(' ')
sb.write_string(value)
match timestamp {
Some(ts) => sb.write_string(timestamp_text(ts, milliseconds))
None => ()
}
sb.write_string(exemplar)
sb.write_char('\n')
}
///|
fn is_counter(kind : @metric.Kind) -> Bool {
match kind {
@metric.Kind::Counter => true
_ => false
}
}
///|
fn is_gauge_histogram(kind : @metric.Kind) -> Bool {
match kind {
@metric.Kind::GaugeHistogram => true
_ => false
}
}
///|
fn number_name(kind : @metric.Kind, base : String) -> String {
match kind {
@metric.Kind::Counter => base + "_total"
@metric.Kind::Info => base + "_info"
_ => base
}
}
///|
fn text_declared(kind : @metric.Kind, base : String) -> (String, String) {
match kind {
@metric.Kind::Counter => (base + "_total", "counter")
@metric.Kind::Info => (base + "_info", "gauge")
@metric.Kind::StateSet => (base, "gauge")
@metric.Kind::GaugeHistogram => (base, "histogram")
@metric.Kind::Unknown => (base, "untyped")
_ => (base, kind.code())
}
}
///|
fn find_exemplar(
exemplars : Array[@metric.Exemplar],
low : Double,
high : Double,
infinity : Bool,
) -> String {
for ex in exemplars {
let hit = if infinity {
ex.value > low
} else {
ex.value > low && ex.value <= high
}
if hit {
return exemplar_text(ex)
}
}
""
}
///|
fn write_created_family(
sb : StringBuilder,
name : String,
help : String,
lines : StringBuilder,
) -> Unit {
sb.write_string("# HELP ")
sb.write_string(name)
sb.write_char(' ')
sb.write_string(escape_help(help, false))
sb.write_char('\n')
sb.write_string("# TYPE ")
sb.write_string(name)
sb.write_string(" gauge\n")
sb.write_string(lines.to_string())
}
///|
fn encode_text_family(
sb : StringBuilder,
family : @metric.MetricFamily,
) -> Unit {
let base = family.name
let (declared, spelling) = text_declared(family.kind, base)
sb.write_string("# HELP ")
sb.write_string(declared)
sb.write_char(' ')
sb.write_string(escape_help(family.help, false))
sb.write_char('\n')
sb.write_string("# TYPE ")
sb.write_string(declared)
sb.write_char(' ')
sb.write_string(spelling)
sb.write_char('\n')
let created = StringBuilder()
let gcount = StringBuilder()
let gsum = StringBuilder()
for point in family.metrics {
match point.value {
@metric.Value::Number(v) => {
write_line(
sb,
number_name(family.kind, base),
point.labels,
format_float(v),
point.timestamp,
true,
"",
)
if is_counter(family.kind) {
match point.created {
Some(c) =>
write_line(
created,
base + "_created",
point.labels,
format_float(c),
point.timestamp,
true,
"",
)
None => ()
}
}
}
@metric.Value::Buckets(buckets~, sum~, count~) => {
for entry in buckets {
write_line(
sb,
base + "_bucket",
with_pair(point.labels, "le", format_float(entry.0)),
format_float(entry.1.to_double()),
point.timestamp,
true,
"",
)
}
write_line(
sb,
base + "_bucket",
with_pair(point.labels, "le", "+Inf"),
format_float(count.to_double()),
point.timestamp,
true,
"",
)
if is_gauge_histogram(family.kind) {
write_line(
gcount,
base + "_gcount",
point.labels,
format_float(count.to_double()),
point.timestamp,
true,
"",
)
write_line(
gsum,
base + "_gsum",
point.labels,
format_float(sum),
point.timestamp,
true,
"",
)
} else {
write_line(
sb,
base + "_count",
point.labels,
format_float(count.to_double()),
point.timestamp,
true,
"",
)
write_line(
sb,
base + "_sum",
point.labels,
format_float(sum),
point.timestamp,
true,
"",
)
}
match point.created {
Some(c) =>
write_line(
created,
base + "_created",
point.labels,
format_float(c),
point.timestamp,
true,
"",
)
None => ()
}
}
@metric.Value::Quantiles(quantiles~, sum~, count~) => {
for q in quantiles {
write_line(
sb,
base,
with_pair(point.labels, "quantile", format_float(q.0)),
format_float(q.1),
point.timestamp,
true,
"",
)
}
write_line(
sb,
base + "_count",
point.labels,
format_float(count.to_double()),
point.timestamp,
true,
"",
)
write_line(
sb,
base + "_sum",
point.labels,
format_float(sum),
point.timestamp,
true,
"",
)
match point.created {
Some(c) =>
write_line(
created,
base + "_created",
point.labels,
format_float(c),
point.timestamp,
true,
"",
)
None => ()
}
}
@metric.Value::State(states~) =>
for state in states {
write_line(
sb,
base,
with_pair(point.labels, base, state.0),
if state.1 {
"1.0"
} else {
"0.0"
},
point.timestamp,
true,
"",
)
}
}
}
if created.to_string() != "" {
write_created_family(sb, base + "_created", family.help, created)
}
if gcount.to_string() != "" {
write_created_family(sb, base + "_gcount", family.help, gcount)
}
if gsum.to_string() != "" {
write_created_family(sb, base + "_gsum", family.help, gsum)
}
}
///|
fn encode_om_family(sb : StringBuilder, family : @metric.MetricFamily) -> Unit {
let base = family.name
sb.write_string("# HELP ")
sb.write_string(base)
sb.write_char(' ')
sb.write_string(escape_help(family.help, true))
sb.write_char('\n')
sb.write_string("# TYPE ")
sb.write_string(base)
sb.write_char(' ')
sb.write_string(family.kind.code())
sb.write_char('\n')
match family.unit {
Some(u) => {
sb.write_string("# UNIT ")
sb.write_string(base)
sb.write_char(' ')
sb.write_string(u)
sb.write_char('\n')
}
None => ()
}
let neg_inf = 0.0 - 1.0 / 0.0
for point in family.metrics {
match point.value {
@metric.Value::Number(v) => {
let ex = if point.exemplars.length() > 0 {
if is_counter(family.kind) {
exemplar_text(point.exemplars[0])
} else {
""
}
} else {
""
}
write_line(
sb,
number_name(family.kind, base),
point.labels,
format_float(v),
point.timestamp,
false,
ex,
)
if is_counter(family.kind) {
match point.created {
Some(c) =>
write_line(
sb,
base + "_created",
point.labels,
format_float(c),
point.timestamp,
false,
"",
)
None => ()
}
}
}
@metric.Value::Buckets(buckets~, sum~, count~) => {
let mut low = neg_inf
for entry in buckets {
write_line(
sb,
base + "_bucket",
with_pair(point.labels, "le", format_float(entry.0)),
format_float(entry.1.to_double()),
point.timestamp,
false,
find_exemplar(point.exemplars, low, entry.0, false),
)
low = entry.0
}
let last = if buckets.length() == 0 {
0.0
} else {
buckets[buckets.length() - 1].0
}
write_line(
sb,
base + "_bucket",
with_pair(point.labels, "le", "+Inf"),
format_float(count.to_double()),
point.timestamp,
false,
find_exemplar(point.exemplars, last, 0.0, true),
)
if is_gauge_histogram(family.kind) {
write_line(
sb,
base + "_gcount",
point.labels,
format_float(count.to_double()),
point.timestamp,
false,
"",
)
write_line(
sb,
base + "_gsum",
point.labels,
format_float(sum),
point.timestamp,
false,
"",
)
} else {
write_line(
sb,
base + "_count",
point.labels,
format_float(count.to_double()),
point.timestamp,
false,
"",
)
write_line(
sb,
base + "_sum",
point.labels,
format_float(sum),
point.timestamp,
false,
"",
)
}
match point.created {
Some(c) =>
write_line(
sb,
base + "_created",
point.labels,
format_float(c),
point.timestamp,
false,
"",
)
None => ()
}
}
@metric.Value::Quantiles(quantiles~, sum~, count~) => {
for q in quantiles {
write_line(
sb,
base,
with_pair(point.labels, "quantile", format_float(q.0)),
format_float(q.1),
point.timestamp,
false,
"",
)
}
write_line(
sb,
base + "_count",
point.labels,
format_float(count.to_double()),
point.timestamp,
false,
"",
)
write_line(
sb,
base + "_sum",
point.labels,
format_float(sum),
point.timestamp,
false,
"",
)
match point.created {
Some(c) =>
write_line(
sb,
base + "_created",
point.labels,
format_float(c),
point.timestamp,
false,
"",
)
None => ()
}
}
@metric.Value::State(states~) =>
for state in states {
write_line(
sb,
base,
with_pair(point.labels, base, state.0),
if state.1 {
"1.0"
} else {
"0.0"
},
point.timestamp,
false,
"",
)
}
}
}
}
///|
/// Write families in the Prometheus text exposition format, version 0.0.4.
/// Counters gain a `_total` suffix, info becomes a gauge ending in `_info`,
/// state sets become gauges, and `_created`, `_gcount`, and `_gsum` series are
/// emitted as separate gauge families, as the compatibility format requires.
pub fn text(families : Array[@metric.MetricFamily]) -> String {
let sb = StringBuilder()
for family in families {
encode_text_family(sb, family)
}
sb.to_string()
}
///|
/// Write families in the OpenMetrics text format, version 1.0.0, ending with the
/// `# EOF` marker the format requires.
pub fn openmetrics(families : Array[@metric.MetricFamily]) -> String {
let sb = StringBuilder()
for family in families {
encode_om_family(sb, family)
}
sb.write_string("# EOF\n")
sb.to_string()
}