robotrt-obs-core 0.1.0-beta.2

RobotRT modular robotics runtime and middleware components.
Documentation
use core_types::{HealthStatus, MetricsProvider, MetricsSnapshot};
use serde_json::{Value, json};

/// Health + metrics bundle for a single named component.
#[derive(Clone, Debug)]
pub struct ComponentHealth {
    /// Component name as registered with the aggregator.
    pub name: String,
    /// Health status at the time of collection.
    pub status: HealthStatus,
    /// All metrics emitted by this component.
    pub metrics: Vec<MetricsSnapshot>,
}

/// Overall system health report produced by [`MetricsAggregator`].
#[derive(Clone, Debug)]
pub struct HealthReport {
    /// Per-component breakdown.
    pub components: Vec<ComponentHealth>,
    /// The worst status across all components (monotone escalation).
    pub overall: HealthStatus,
}

impl HealthReport {
    /// Returns `true` only when every component is `Healthy`.
    pub fn is_fully_healthy(&self) -> bool {
        matches!(self.overall, HealthStatus::Healthy)
    }

    /// Returns components whose health is not `Healthy`.
    pub fn degraded_components(&self) -> impl Iterator<Item = &ComponentHealth> {
        self.components.iter().filter(|c| !c.status.is_healthy())
    }

    pub fn to_json_value(&self) -> Value {
        json!({
            "schema": "robotrt.obs.health_report.v1",
            "overall": health_status_to_json_value(&self.overall),
            "components": self
                .components
                .iter()
                .map(ComponentHealth::to_json_value)
                .collect::<Vec<_>>()
        })
    }

    pub fn to_json(&self) -> String {
        self.to_json_value().to_string()
    }

    pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
        serde_json::to_string_pretty(&self.to_json_value())
    }
}

impl ComponentHealth {
    pub fn to_json_value(&self) -> Value {
        json!({
            "name": self.name,
            "status": health_status_to_json_value(&self.status),
            "metrics": self.metrics.iter().map(metrics_snapshot_to_json_value).collect::<Vec<_>>()
        })
    }
}

fn health_status_to_json_value(status: &HealthStatus) -> Value {
    match status {
        HealthStatus::Healthy => json!({
            "state": "healthy"
        }),
        HealthStatus::Degraded { reason } => json!({
            "state": "degraded",
            "reason": reason
        }),
        HealthStatus::Unhealthy { reason } => json!({
            "state": "unhealthy",
            "reason": reason
        }),
    }
}

fn metrics_snapshot_to_json_value(metric: &MetricsSnapshot) -> Value {
    json!({
        "name": metric.name,
        "value": metric.value,
        "unit": metric.unit
    })
}

// ─── Merge helper ─────────────────────────────────────────────────────────────

fn merge_health(a: HealthStatus, b: &HealthStatus) -> HealthStatus {
    match (&a, b) {
        (_, HealthStatus::Unhealthy { .. }) => b.clone(),
        (HealthStatus::Unhealthy { .. }, _) => a,
        (_, HealthStatus::Degraded { .. }) => b.clone(),
        (HealthStatus::Degraded { .. }, _) => a,
        _ => HealthStatus::Healthy,
    }
}

// ─── MetricsAggregator ────────────────────────────────────────────────────────

struct Entry {
    name: String,
    provider: Box<dyn MetricsProvider + Send>,
}

/// Collects metrics and health from multiple [`MetricsProvider`] implementations.
///
/// # Example
/// ```
/// use obs_core::aggregator::MetricsAggregator;
///
/// let mut agg = MetricsAggregator::new();
/// assert!(agg.is_empty());
/// ```
#[derive(Default)]
pub struct MetricsAggregator {
    entries: Vec<Entry>,
}

impl MetricsAggregator {
    pub fn new() -> Self {
        Self::default()
    }

    /// Register a named provider.
    pub fn register(&mut self, name: impl Into<String>, provider: Box<dyn MetricsProvider + Send>) {
        self.entries.push(Entry {
            name: name.into(),
            provider,
        });
    }

    /// Returns `true` when no providers are registered.
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    /// Number of registered providers.
    pub fn len(&self) -> usize {
        self.entries.len()
    }

    /// Collect all metrics from every registered provider.
    pub fn collect_all(&self) -> Vec<(String, Vec<MetricsSnapshot>)> {
        self.entries
            .iter()
            .map(|e| (e.name.clone(), e.provider.collect()))
            .collect()
    }

    /// Build a complete [`HealthReport`] from all providers.
    pub fn health_report(&self) -> HealthReport {
        let mut overall = HealthStatus::Healthy;
        let components = self
            .entries
            .iter()
            .map(|e| {
                let status = e.provider.health();
                let metrics = e.provider.collect();
                overall = merge_health(overall.clone(), &status);
                ComponentHealth {
                    name: e.name.clone(),
                    status,
                    metrics,
                }
            })
            .collect();

        HealthReport {
            components,
            overall,
        }
    }

    /// Flat iterator over every `MetricsSnapshot` from every provider,
    /// prefixed with the provider name: `"<name>/<metric_name>"`.
    pub fn flat_metrics(&self) -> Vec<MetricsSnapshot> {
        self.entries
            .iter()
            .flat_map(|e| {
                e.provider.collect().into_iter().map(|mut m| {
                    m.name = format!("{}/{}", e.name, m.name);
                    m
                })
            })
            .collect()
    }
}