robotrt-obs-core 0.1.0-beta.2

RobotRT modular robotics runtime and middleware components.
Documentation
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('"', "\\\"")
}