use core_types::{HealthStatus, Timestamp};
use serde_json::{Value, json};
use crate::aggregator::{HealthReport, MetricsAggregator};
pub fn export_prometheus_text(aggregator: &MetricsAggregator) -> String {
let mut lines = Vec::new();
for metric in aggregator.flat_metrics() {
let metric_name = sanitize_prometheus_metric_name(&format!("robotrt_{}", metric.name));
lines.push(format!(
"{}{{unit=\"{}\"}} {}",
metric_name,
escape_label_value(metric.unit),
metric.value
));
}
let report = aggregator.health_report();
lines.extend(export_health_prometheus_lines(&report));
lines.join("\n")
}
pub fn export_otel_json_value(aggregator: &MetricsAggregator) -> Value {
let observed_at_unix_nanos = Timestamp::now().0;
let metrics = aggregator
.collect_all()
.into_iter()
.flat_map(|(component, snapshots)| {
snapshots.into_iter().map(move |metric| {
json!({
"name": metric.name,
"description": "",
"unit": metric.unit,
"type": "gauge",
"data_points": [
{
"as_double": metric.value,
"time_unix_nano": observed_at_unix_nanos,
"attributes": {
"component": component,
}
}
]
})
})
})
.collect::<Vec<_>>();
let report = aggregator.health_report();
json!({
"schema": "robotrt.obs.otel_metrics.v1",
"resource": {
"service.name": "robotrt",
"service.namespace": "robotrt.middleware",
},
"observed_at_unix_nanos": observed_at_unix_nanos,
"metrics": metrics,
"health": report.to_json_value(),
})
}
pub fn export_otel_json(aggregator: &MetricsAggregator) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(&export_otel_json_value(aggregator))
}
fn export_health_prometheus_lines(report: &HealthReport) -> Vec<String> {
let mut lines = Vec::new();
lines.push(format!(
"robotrt_health_overall {}",
health_status_to_prometheus_value(&report.overall)
));
for component in &report.components {
lines.push(format!(
"robotrt_health_component{{component=\"{}\"}} {}",
escape_label_value(&component.name),
health_status_to_prometheus_value(&component.status)
));
}
lines
}
fn health_status_to_prometheus_value(status: &HealthStatus) -> u8 {
match status {
HealthStatus::Healthy => 0,
HealthStatus::Degraded { .. } => 1,
HealthStatus::Unhealthy { .. } => 2,
}
}
fn sanitize_prometheus_metric_name(raw: &str) -> String {
raw.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() || ch == '_' {
ch
} else {
'_'
}
})
.collect()
}
fn escape_label_value(raw: &str) -> String {
raw.replace('\\', "\\\\").replace('"', "\\\"")
}