robotrt-cli 0.1.0-beta.2

RobotRT modular robotics runtime and middleware components.
use introspection_core::StatusSnapshot;
use obs_core::{MetricsAggregator, export_otel_json_value, export_prometheus_text};

use crate::constants::DEFAULT_STATUS_REPORT_PATH;
use crate::helpers::{has_flag, option_value};

const OBS_EXPORT_API_VERSION: &str = "robotrt.obs.export.v1";

type SnapshotSource = super::snapshot_shared::SnapshotSource;

pub fn obs_export(args: &[String]) -> Result<(), String> {
    let format = option_value(args, "--format").unwrap_or_else(|| "prometheus".to_string());
    if format != "prometheus" && format != "otel" {
        return Err(format!(
            "unsupported --format value: {format} (expected prometheus|otel)"
        ));
    }

    let alert_template =
        option_value(args, "--alert-template").unwrap_or_else(|| "none".to_string());
    if alert_template != "none" && alert_template != "basic" {
        return Err(format!(
            "unsupported --alert-template value: {alert_template} (expected none|basic)"
        ));
    }

    let json = has_flag(args, "--json");
    let (snapshot, source) = load_status_snapshot(args)?;
    let aggregator = build_snapshot_aggregator(&snapshot);

    let metrics_payload = if format == "prometheus" {
        serde_json::Value::String(export_prometheus_text(&aggregator))
    } else {
        export_otel_json_value(&aggregator)
    };

    let template_payload = build_alert_template(&alert_template);

    if json {
        let payload = serde_json::json!({
            "api_version": OBS_EXPORT_API_VERSION,
            "kind": "obs_export",
            "captured_at_unix_nanos": snapshot.captured_at_unix_nanos,
            "source": source.json,
            "query": {
                "format": format,
                "alert_template": alert_template,
            },
            "result": {
                "metrics": metrics_payload,
                "alert_template": template_payload,
            }
        });
        println!(
            "{}",
            serde_json::to_string_pretty(&payload)
                .map_err(|err| format!("serialize obs export payload failed: {err}"))?
        );
    } else {
        println!("RobotRT Obs Export");
        println!("source: {}", source.label);
        println!("format: {}", format);
        println!();

        if format == "prometheus" {
            println!(
                "{}",
                metrics_payload
                    .as_str()
                    .ok_or_else(|| String::from("unexpected prometheus payload type"))?
            );
        } else {
            println!(
                "{}",
                serde_json::to_string_pretty(&metrics_payload)
                    .map_err(|err| format!("serialize otel payload failed: {err}"))?
            );
        }

        if !template_payload.is_null() {
            println!();
            println!("alert_template:");
            println!(
                "{}",
                template_payload
                    .as_str()
                    .ok_or_else(|| String::from("unexpected alert template payload type"))?
            );
        }
    }

    Ok(())
}

fn load_status_snapshot(
    args: &[String],
) -> Result<(StatusSnapshot, SnapshotSource), String> {
    super::snapshot_shared::load_status_snapshot(
        args,
        option_value(args, "--endpoint"),
        "--timeout-ms",
        DEFAULT_STATUS_REPORT_PATH,
    )
}

fn build_snapshot_aggregator(snapshot: &StatusSnapshot) -> MetricsAggregator {
    crate::commands::snapshot_shared::build_snapshot_aggregator(snapshot)
}

fn build_alert_template(kind: &str) -> serde_json::Value {
    match kind {
        "none" => serde_json::Value::Null,
        "basic" => serde_json::Value::String(
            "groups:\n  - name: robotrt-basic\n    rules:\n      - alert: RobotRTUnhealthy\n        expr: robotrt_health_overall > 0\n        for: 2m\n        labels:\n          severity: critical\n        annotations:\n          summary: \"RobotRT health degraded or unhealthy\"\n\n      - alert: RobotRTHighQueuePressure\n        expr: robotrt_runtime_topics_pending_total > 100\n        for: 3m\n        labels:\n          severity: warning\n        annotations:\n          summary: \"RobotRT topic queue pressure is high\""
                .to_string(),
        ),
        _ => serde_json::Value::Null,
    }
}