use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::metrics::{ExportMetric, ExportValues, MetricsRegistry, parse_label_key};
use super::types::MetricType;
pub fn export_prometheus_format(registry: &MetricsRegistry) -> String {
let mut output = String::new();
let metrics = registry.get_all_for_export();
for metric in metrics {
output.push_str(&format!("# HELP {} {}\n", metric.name, metric.help));
output.push_str(&format!(
"# TYPE {} {}\n",
metric.name,
metric.metric_type.prometheus_type()
));
match metric.values {
ExportValues::Counter(values) | ExportValues::Gauge(values) => {
for (label_key, value) in values {
let labels = format_labels(&metric.label_names, &label_key);
if labels.is_empty() {
output.push_str(&format!("{} {}\n", metric.name, value));
} else {
output.push_str(&format!("{}{{{}}} {}\n", metric.name, labels, value));
}
}
}
ExportValues::Histogram(histograms) => {
for (label_key, hist) in histograms {
let base_labels = format_labels(&metric.label_names, &label_key);
for bucket in &hist.buckets {
let le_label = if base_labels.is_empty() {
format!("le=\"{}\"", bucket.le)
} else {
format!("{},le=\"{}\"", base_labels, bucket.le)
};
output.push_str(&format!(
"{}_bucket{{{}}} {}\n",
metric.name, le_label, bucket.count
));
}
let inf_label = if base_labels.is_empty() {
"le=\"+Inf\"".to_string()
} else {
format!("{},le=\"+Inf\"", base_labels)
};
output.push_str(&format!(
"{}_bucket{{{}}} {}\n",
metric.name, inf_label, hist.count
));
if base_labels.is_empty() {
output.push_str(&format!("{}_sum {}\n", metric.name, hist.sum));
output.push_str(&format!("{}_count {}\n", metric.name, hist.count));
} else {
output.push_str(&format!(
"{}_sum{{{}}} {}\n",
metric.name, base_labels, hist.sum
));
output.push_str(&format!(
"{}_count{{{}}} {}\n",
metric.name, base_labels, hist.count
));
}
}
}
ExportValues::Summary(summaries) => {
for (label_key, summary) in summaries {
let base_labels = format_labels(&metric.label_names, &label_key);
for quantile in &summary.quantiles {
let q_label = if base_labels.is_empty() {
format!("quantile=\"{}\"", quantile.quantile)
} else {
format!("{},quantile=\"{}\"", base_labels, quantile.quantile)
};
output.push_str(&format!(
"{}{{{}}} {}\n",
metric.name, q_label, quantile.value
));
}
if base_labels.is_empty() {
output.push_str(&format!("{}_sum {}\n", metric.name, summary.sum));
output.push_str(&format!("{}_count {}\n", metric.name, summary.count));
} else {
output.push_str(&format!(
"{}_sum{{{}}} {}\n",
metric.name, base_labels, summary.sum
));
output.push_str(&format!(
"{}_count{{{}}} {}\n",
metric.name, base_labels, summary.count
));
}
}
}
}
output.push('\n');
}
output
}
pub fn export_json_format(registry: &MetricsRegistry) -> String {
let metrics = registry.get_all_for_export();
let mut output = String::new();
output.push_str("{\n \"metrics\": [\n");
for (i, metric) in metrics.iter().enumerate() {
output.push_str(" {\n");
output.push_str(&format!(" \"name\": \"{}\",\n", metric.name));
output.push_str(&format!(
" \"help\": \"{}\",\n",
escape_json(&metric.help)
));
output.push_str(&format!(
" \"type\": \"{}\",\n",
metric.metric_type.prometheus_type()
));
output.push_str(" \"labels\": [");
for (j, label) in metric.label_names.iter().enumerate() {
output.push_str(&format!("\"{}\"", label));
if j < metric.label_names.len() - 1 {
output.push_str(", ");
}
}
output.push_str("],\n");
output.push_str(" \"values\": ");
output.push_str(&format_values_json(&metric.values, &metric.label_names));
output.push('\n');
output.push_str(" }");
if i < metrics.len() - 1 {
output.push(',');
}
output.push('\n');
}
output.push_str(" ]\n}\n");
output
}
fn format_labels(label_names: &[String], label_key: &str) -> String {
if label_names.is_empty() || label_key.is_empty() {
return String::new();
}
let values = parse_label_key(label_key);
let pairs: Vec<String> = label_names
.iter()
.zip(values.iter())
.map(|(name, value)| format!("{}=\"{}\"", name, escape_prometheus(value)))
.collect();
pairs.join(",")
}
fn escape_prometheus(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
}
fn escape_json(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
fn format_values_json(values: &ExportValues, label_names: &[String]) -> String {
match values {
ExportValues::Counter(v) | ExportValues::Gauge(v) => {
let mut output = String::from("[\n");
let items: Vec<_> = v.iter().collect();
for (i, (key, value)) in items.iter().enumerate() {
output.push_str(" {\n");
output.push_str(&format_label_json(label_names, key));
output.push_str(&format!(" \"value\": {}\n", value));
output.push_str(" }");
if i < items.len() - 1 {
output.push(',');
}
output.push('\n');
}
output.push_str(" ]");
output
}
ExportValues::Histogram(v) => {
let mut output = String::from("[\n");
let items: Vec<_> = v.iter().collect();
for (i, (key, hist)) in items.iter().enumerate() {
output.push_str(" {\n");
output.push_str(&format_label_json(label_names, key));
output.push_str(&format!(" \"sum\": {},\n", hist.sum));
output.push_str(&format!(" \"count\": {},\n", hist.count));
output.push_str(" \"buckets\": [");
for (j, bucket) in hist.buckets.iter().enumerate() {
output.push_str(&format!(
"{{\"le\": {}, \"count\": {}}}",
bucket.le, bucket.count
));
if j < hist.buckets.len() - 1 {
output.push_str(", ");
}
}
output.push_str("]\n");
output.push_str(" }");
if i < items.len() - 1 {
output.push(',');
}
output.push('\n');
}
output.push_str(" ]");
output
}
ExportValues::Summary(v) => {
let mut output = String::from("[\n");
let items: Vec<_> = v.iter().collect();
for (i, (key, summary)) in items.iter().enumerate() {
output.push_str(" {\n");
output.push_str(&format_label_json(label_names, key));
output.push_str(&format!(" \"sum\": {},\n", summary.sum));
output.push_str(&format!(" \"count\": {},\n", summary.count));
output.push_str(" \"quantiles\": [");
for (j, q) in summary.quantiles.iter().enumerate() {
output.push_str(&format!(
"{{\"quantile\": {}, \"value\": {}}}",
q.quantile, q.value
));
if j < summary.quantiles.len() - 1 {
output.push_str(", ");
}
}
output.push_str("]\n");
output.push_str(" }");
if i < items.len() - 1 {
output.push(',');
}
output.push('\n');
}
output.push_str(" ]");
output
}
}
}
fn format_label_json(label_names: &[String], label_key: &str) -> String {
if label_names.is_empty() || label_key.is_empty() {
return String::new();
}
let values = parse_label_key(label_key);
let mut output = String::from(" \"labels\": {");
let pairs: Vec<String> = label_names
.iter()
.zip(values.iter())
.map(|(name, value)| format!("\"{}\": \"{}\"", name, escape_json(value)))
.collect();
output.push_str(&pairs.join(", "));
output.push_str("},\n");
output
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn test_escape_prometheus() {
assert_eq!(escape_prometheus("hello"), "hello");
assert_eq!(escape_prometheus("hello\"world"), "hello\\\"world");
assert_eq!(escape_prometheus("line\nbreak"), "line\\nbreak");
}
#[test]
fn test_escape_json() {
assert_eq!(escape_json("hello"), "hello");
assert_eq!(escape_json("tab\there"), "tab\\there");
}
#[test]
fn test_format_labels() {
let names = vec!["dataset".to_string(), "operation".to_string()];
let key = "pool1\x00read";
let result = format_labels(&names, key);
assert_eq!(result, "dataset=\"pool1\",operation=\"read\"");
}
#[test]
fn test_export_prometheus() {
let mut registry = MetricsRegistry::new();
registry
.register_counter("test_total", "Test counter", &["label1"])
.unwrap();
let output = export_prometheus_format(®istry);
assert!(output.contains("# HELP test_total Test counter"));
assert!(output.contains("# TYPE test_total counter"));
}
}