238 lines
7.1 KiB
Rust
238 lines
7.1 KiB
Rust
|
|
use anyhow::{Context, Result};
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
|
||
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
|
|
pub struct PodMetrics {
|
||
|
|
pub name: String,
|
||
|
|
pub namespace: String,
|
||
|
|
pub containers: Vec<ContainerMetrics>,
|
||
|
|
pub cpu: String, // e.g., "100m"
|
||
|
|
pub memory: String, // e.g., "256Mi"
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
|
|
pub struct ContainerMetrics {
|
||
|
|
pub name: String,
|
||
|
|
pub cpu: String,
|
||
|
|
pub memory: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
|
|
pub struct NodeMetrics {
|
||
|
|
pub name: String,
|
||
|
|
pub cpu: String,
|
||
|
|
pub memory: String,
|
||
|
|
pub cpu_percent: f64,
|
||
|
|
pub memory_percent: f64,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Parse kubectl top pods output (JSON format)
|
||
|
|
pub fn parse_pod_metrics(json_output: &str) -> Result<Vec<PodMetrics>> {
|
||
|
|
let value: serde_json::Value =
|
||
|
|
serde_json::from_str(json_output).context("Failed to parse kubectl top pods JSON")?;
|
||
|
|
|
||
|
|
let items = value
|
||
|
|
.get("items")
|
||
|
|
.and_then(|v| v.as_array())
|
||
|
|
.context("Missing items array")?;
|
||
|
|
|
||
|
|
let mut metrics = Vec::new();
|
||
|
|
|
||
|
|
for item in items {
|
||
|
|
let name = item
|
||
|
|
.get("metadata")
|
||
|
|
.and_then(|m| m.get("name"))
|
||
|
|
.and_then(|n| n.as_str())
|
||
|
|
.unwrap_or("")
|
||
|
|
.to_string();
|
||
|
|
|
||
|
|
let namespace = item
|
||
|
|
.get("metadata")
|
||
|
|
.and_then(|m| m.get("namespace"))
|
||
|
|
.and_then(|n| n.as_str())
|
||
|
|
.unwrap_or("default")
|
||
|
|
.to_string();
|
||
|
|
|
||
|
|
let containers_data = item.get("containers").and_then(|c| c.as_array());
|
||
|
|
|
||
|
|
let mut containers = Vec::new();
|
||
|
|
let mut total_cpu_nano = 0u64;
|
||
|
|
let mut total_memory_kb = 0u64;
|
||
|
|
|
||
|
|
if let Some(containers_data) = containers_data {
|
||
|
|
for container in containers_data {
|
||
|
|
let container_name = container
|
||
|
|
.get("name")
|
||
|
|
.and_then(|n| n.as_str())
|
||
|
|
.unwrap_or("")
|
||
|
|
.to_string();
|
||
|
|
|
||
|
|
let cpu_usage = container
|
||
|
|
.get("usage")
|
||
|
|
.and_then(|u| u.get("cpu"))
|
||
|
|
.and_then(|c| c.as_str())
|
||
|
|
.unwrap_or("0")
|
||
|
|
.to_string();
|
||
|
|
|
||
|
|
let memory_usage = container
|
||
|
|
.get("usage")
|
||
|
|
.and_then(|u| u.get("memory"))
|
||
|
|
.and_then(|m| m.as_str())
|
||
|
|
.unwrap_or("0")
|
||
|
|
.to_string();
|
||
|
|
|
||
|
|
// Parse for totals
|
||
|
|
total_cpu_nano += parse_cpu_to_nanocores(&cpu_usage);
|
||
|
|
total_memory_kb += parse_memory_to_kb(&memory_usage);
|
||
|
|
|
||
|
|
containers.push(ContainerMetrics {
|
||
|
|
name: container_name,
|
||
|
|
cpu: cpu_usage,
|
||
|
|
memory: memory_usage,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
metrics.push(PodMetrics {
|
||
|
|
name,
|
||
|
|
namespace,
|
||
|
|
containers,
|
||
|
|
cpu: format_cpu_from_nanocores(total_cpu_nano),
|
||
|
|
memory: format_memory_from_kb(total_memory_kb),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(metrics)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Parse kubectl top nodes output (JSON format)
|
||
|
|
pub fn parse_node_metrics(json_output: &str) -> Result<Vec<NodeMetrics>> {
|
||
|
|
let value: serde_json::Value =
|
||
|
|
serde_json::from_str(json_output).context("Failed to parse kubectl top nodes JSON")?;
|
||
|
|
|
||
|
|
let items = value
|
||
|
|
.get("items")
|
||
|
|
.and_then(|v| v.as_array())
|
||
|
|
.context("Missing items array")?;
|
||
|
|
|
||
|
|
let mut metrics = Vec::new();
|
||
|
|
|
||
|
|
for item in items {
|
||
|
|
let name = item
|
||
|
|
.get("metadata")
|
||
|
|
.and_then(|m| m.get("name"))
|
||
|
|
.and_then(|n| n.as_str())
|
||
|
|
.unwrap_or("")
|
||
|
|
.to_string();
|
||
|
|
|
||
|
|
let cpu = item
|
||
|
|
.get("usage")
|
||
|
|
.and_then(|u| u.get("cpu"))
|
||
|
|
.and_then(|c| c.as_str())
|
||
|
|
.unwrap_or("0")
|
||
|
|
.to_string();
|
||
|
|
|
||
|
|
let memory = item
|
||
|
|
.get("usage")
|
||
|
|
.and_then(|u| u.get("memory"))
|
||
|
|
.and_then(|m| m.as_str())
|
||
|
|
.unwrap_or("0")
|
||
|
|
.to_string();
|
||
|
|
|
||
|
|
// Calculate percentages (simplified - would need capacity from kubectl get nodes)
|
||
|
|
let cpu_percent = 0.0; // TODO: Calculate from capacity
|
||
|
|
let memory_percent = 0.0; // TODO: Calculate from capacity
|
||
|
|
|
||
|
|
metrics.push(NodeMetrics {
|
||
|
|
name,
|
||
|
|
cpu,
|
||
|
|
memory,
|
||
|
|
cpu_percent,
|
||
|
|
memory_percent,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(metrics)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Parse CPU string to nanocores (e.g., "100m" -> 100000000, "2" -> 2000000000)
|
||
|
|
fn parse_cpu_to_nanocores(cpu: &str) -> u64 {
|
||
|
|
if cpu.ends_with('n') {
|
||
|
|
cpu.trim_end_matches('n').parse::<u64>().unwrap_or(0)
|
||
|
|
} else if cpu.ends_with('u') {
|
||
|
|
cpu.trim_end_matches('u').parse::<u64>().unwrap_or(0) * 1000
|
||
|
|
} else if cpu.ends_with('m') {
|
||
|
|
cpu.trim_end_matches('m').parse::<u64>().unwrap_or(0) * 1_000_000
|
||
|
|
} else {
|
||
|
|
cpu.parse::<u64>().unwrap_or(0) * 1_000_000_000
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Parse memory string to kilobytes (e.g., "256Mi" -> 262144, "1Gi" -> 1048576)
|
||
|
|
fn parse_memory_to_kb(memory: &str) -> u64 {
|
||
|
|
if memory.ends_with("Ki") {
|
||
|
|
memory.trim_end_matches("Ki").parse::<u64>().unwrap_or(0)
|
||
|
|
} else if memory.ends_with("Mi") {
|
||
|
|
memory.trim_end_matches("Mi").parse::<u64>().unwrap_or(0) * 1024
|
||
|
|
} else if memory.ends_with("Gi") {
|
||
|
|
memory.trim_end_matches("Gi").parse::<u64>().unwrap_or(0) * 1024 * 1024
|
||
|
|
} else if memory.ends_with("Ti") {
|
||
|
|
memory.trim_end_matches("Ti").parse::<u64>().unwrap_or(0) * 1024 * 1024 * 1024
|
||
|
|
} else {
|
||
|
|
memory.parse::<u64>().unwrap_or(0) / 1024 // Assume bytes
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Format nanocores back to human-readable (e.g., 100000000 -> "100m")
|
||
|
|
fn format_cpu_from_nanocores(nanocores: u64) -> String {
|
||
|
|
if nanocores >= 1_000_000_000 {
|
||
|
|
format!("{:.1}", nanocores as f64 / 1_000_000_000.0)
|
||
|
|
} else {
|
||
|
|
format!("{}m", nanocores / 1_000_000)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Format kilobytes back to human-readable (e.g., 262144 -> "256Mi")
|
||
|
|
fn format_memory_from_kb(kb: u64) -> String {
|
||
|
|
if kb >= 1024 * 1024 * 1024 {
|
||
|
|
format!("{:.1}Ti", kb as f64 / (1024.0 * 1024.0 * 1024.0))
|
||
|
|
} else if kb >= 1024 * 1024 {
|
||
|
|
format!("{:.0}Gi", kb as f64 / (1024.0 * 1024.0))
|
||
|
|
} else if kb >= 1024 {
|
||
|
|
format!("{:.0}Mi", kb as f64 / 1024.0)
|
||
|
|
} else {
|
||
|
|
format!("{}Ki", kb)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_parse_cpu() {
|
||
|
|
assert_eq!(parse_cpu_to_nanocores("100m"), 100_000_000);
|
||
|
|
assert_eq!(parse_cpu_to_nanocores("2"), 2_000_000_000);
|
||
|
|
assert_eq!(parse_cpu_to_nanocores("500u"), 500_000);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_parse_memory() {
|
||
|
|
assert_eq!(parse_memory_to_kb("256Mi"), 262_144);
|
||
|
|
assert_eq!(parse_memory_to_kb("1Gi"), 1_048_576);
|
||
|
|
assert_eq!(parse_memory_to_kb("512Ki"), 512);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_format_cpu() {
|
||
|
|
assert_eq!(format_cpu_from_nanocores(100_000_000), "100m");
|
||
|
|
assert_eq!(format_cpu_from_nanocores(2_000_000_000), "2.0");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_format_memory() {
|
||
|
|
assert_eq!(format_memory_from_kb(262_144), "256Mi");
|
||
|
|
assert_eq!(format_memory_from_kb(1_048_576), "1Gi");
|
||
|
|
}
|
||
|
|
}
|