Complete atomic migration to structured data architecture
All checks were successful
Build and Release / build-and-release (push) Successful in 1m7s

Implements clean structured data collection eliminating all string metric
parsing bugs. Collectors now populate AgentData directly with type-safe
field access.

Key improvements:
- Mount points preserved correctly (/ and /boot instead of root/boot)
- Tmpfs discovery added to memory collector
- Temperature data flows as typed f32 fields
- Zero string parsing overhead
- Complete removal of MetricCollectionManager bridge
- Direct ZMQ transmission of structured JSON

All functionality maintained: service tracking, notifications, status
evaluation, and multi-host monitoring.
This commit is contained in:
2025-11-24 18:53:31 +01:00
parent 11d1c2dc94
commit 2b2cb2da3e
17 changed files with 1952 additions and 3205 deletions

View File

@@ -1,172 +1,100 @@
use async_trait::async_trait;
use cm_dashboard_shared::{Metric, MetricValue, Status, StatusTracker};
use cm_dashboard_shared::AgentData;
use std::fs;
use std::process::Command;
use tracing::debug;
use super::{Collector, CollectorError};
use crate::config::NixOSConfig;
/// NixOS system information collector
/// NixOS system information collector with structured data output
///
/// Collects NixOS-specific system information including:
/// - NixOS version and build information
/// This collector gathers NixOS-specific information like:
/// - System generation/build information
/// - Version information
/// - Agent version from Nix store path
pub struct NixOSCollector {
config: NixOSConfig,
}
impl NixOSCollector {
pub fn new(_config: NixOSConfig) -> Self {
Self {}
pub fn new(config: NixOSConfig) -> Self {
Self { config }
}
/// Collect NixOS system information and populate AgentData
async fn collect_nixos_info(&self, agent_data: &mut AgentData) -> Result<(), CollectorError> {
debug!("Collecting NixOS system information");
/// Get agent hash from binary path
fn get_agent_hash(&self) -> Result<String, Box<dyn std::error::Error>> {
// Get the path of the current executable
let exe_path = std::env::current_exe()?;
let exe_str = exe_path.to_string_lossy();
// Extract Nix store hash from path like /nix/store/fn804fh332mp8gz06qawminpj20xl25h-cm-dashboard-0.1.0/bin/cm-dashboard-agent
if let Some(store_path) = exe_str.strip_prefix("/nix/store/") {
if let Some(dash_pos) = store_path.find('-') {
return Ok(store_path[..dash_pos].to_string());
}
}
// Fallback to "unknown" if not in Nix store
Ok("unknown".to_string())
// Set hostname (this is universal, not NixOS-specific)
agent_data.hostname = self.get_hostname().await.unwrap_or_else(|| "unknown".to_string());
// Set agent version from environment or Nix store path
agent_data.agent_version = self.get_agent_version().await;
// Set current timestamp
agent_data.timestamp = chrono::Utc::now().timestamp() as u64;
Ok(())
}
/// Get configuration hash from deployed nix store system
/// Get git commit hash from rebuild process
fn get_git_commit(&self) -> Result<String, Box<dyn std::error::Error>> {
let commit_file = "/var/lib/cm-dashboard/git-commit";
match std::fs::read_to_string(commit_file) {
Ok(content) => {
let commit_hash = content.trim();
if commit_hash.len() >= 7 {
Ok(commit_hash.to_string())
} else {
Err("Git commit hash too short".into())
}
}
Err(e) => Err(format!("Failed to read git commit file: {}", e).into())
}
}
fn get_config_hash(&self) -> Result<String, Box<dyn std::error::Error>> {
// Read the symlink target of /run/current-system to get nix store path
let output = Command::new("readlink")
.arg("/run/current-system")
.output()?;
if !output.status.success() {
return Err("readlink command failed".into());
}
let binding = String::from_utf8_lossy(&output.stdout);
let store_path = binding.trim();
// Extract hash from nix store path
// Format: /nix/store/HASH-nixos-system-HOSTNAME-VERSION
if let Some(hash_part) = store_path.strip_prefix("/nix/store/") {
if let Some(hash) = hash_part.split('-').next() {
if hash.len() >= 8 {
// Return first 8 characters of nix store hash
return Ok(hash[..8].to_string());
/// Get system hostname
async fn get_hostname(&self) -> Option<String> {
match fs::read_to_string("/etc/hostname") {
Ok(hostname) => Some(hostname.trim().to_string()),
Err(_) => {
// Fallback to hostname command
match Command::new("hostname").output() {
Ok(output) => Some(String::from_utf8_lossy(&output.stdout).trim().to_string()),
Err(_) => None,
}
}
}
Err("Could not extract hash from nix store path".into())
}
/// Get agent version from Nix store path or environment
async fn get_agent_version(&self) -> String {
// Try to extract version from the current executable path (Nix store)
if let Ok(current_exe) = std::env::current_exe() {
if let Some(exe_path) = current_exe.to_str() {
if exe_path.starts_with("/nix/store/") {
// Extract version from Nix store path
// Path format: /nix/store/hash-cm-dashboard-agent-v0.1.138/bin/cm-dashboard-agent
if let Some(store_part) = exe_path.strip_prefix("/nix/store/") {
if let Some(dash_pos) = store_part.find('-') {
let package_part = &store_part[dash_pos + 1..];
if let Some(bin_pos) = package_part.find("/bin/") {
let package_name = &package_part[..bin_pos];
// Extract version from package name
if let Some(version_start) = package_name.rfind("-v") {
return package_name[version_start + 1..].to_string();
}
}
}
}
}
}
}
// Fallback to environment variable or default
std::env::var("CM_DASHBOARD_VERSION").unwrap_or_else(|_| "unknown".to_string())
}
/// Get NixOS system generation (build) information
async fn get_nixos_generation(&self) -> Option<String> {
match Command::new("nixos-version").output() {
Ok(output) => {
let version_str = String::from_utf8_lossy(&output.stdout);
Some(version_str.trim().to_string())
}
Err(_) => None,
}
}
}
#[async_trait]
impl Collector for NixOSCollector {
async fn collect(&self, _status_tracker: &mut StatusTracker) -> Result<Vec<Metric>, CollectorError> {
debug!("Collecting NixOS system information");
let mut metrics = Vec::new();
let timestamp = chrono::Utc::now().timestamp() as u64;
// Collect git commit information (shows what's actually deployed)
match self.get_git_commit() {
Ok(git_commit) => {
metrics.push(Metric {
name: "system_nixos_build".to_string(),
value: MetricValue::String(git_commit),
unit: None,
description: Some("Git commit hash of deployed configuration".to_string()),
status: Status::Ok,
timestamp,
});
}
Err(e) => {
debug!("Failed to get git commit: {}", e);
metrics.push(Metric {
name: "system_nixos_build".to_string(),
value: MetricValue::String("unknown".to_string()),
unit: None,
description: Some("Git commit hash (failed to detect)".to_string()),
status: Status::Unknown,
timestamp,
});
}
}
// Collect config hash
match self.get_config_hash() {
Ok(hash) => {
metrics.push(Metric {
name: "system_config_hash".to_string(),
value: MetricValue::String(hash),
unit: None,
description: Some("NixOS deployed configuration hash".to_string()),
status: Status::Ok,
timestamp,
});
}
Err(e) => {
debug!("Failed to get config hash: {}", e);
metrics.push(Metric {
name: "system_config_hash".to_string(),
value: MetricValue::String("unknown".to_string()),
unit: None,
description: Some("Deployed config hash (failed to detect)".to_string()),
status: Status::Unknown,
timestamp,
});
}
}
// Collect agent hash
match self.get_agent_hash() {
Ok(hash) => {
metrics.push(Metric {
name: "system_agent_hash".to_string(),
value: MetricValue::String(hash),
unit: None,
description: Some("Agent Nix store hash".to_string()),
status: Status::Ok,
timestamp,
});
}
Err(e) => {
debug!("Failed to get agent hash: {}", e);
metrics.push(Metric {
name: "system_agent_hash".to_string(),
value: MetricValue::String("unknown".to_string()),
unit: None,
description: Some("Agent hash (failed to detect)".to_string()),
status: Status::Unknown,
timestamp,
});
}
}
debug!("Collected {} NixOS metrics", metrics.len());
Ok(metrics)
async fn collect_structured(&self, agent_data: &mut AgentData) -> Result<(), CollectorError> {
self.collect_nixos_info(agent_data).await
}
}