symbi_runtime/metrics/
file.rs1use super::{FileMetricsConfig, MetricsError, MetricsExporter, MetricsSnapshot};
7use async_trait::async_trait;
8use std::path::PathBuf;
9
10pub struct FileExporter {
12 path: PathBuf,
13 pretty_print: bool,
14}
15
16impl FileExporter {
17 pub fn new(config: FileMetricsConfig) -> Result<Self, MetricsError> {
19 if let Some(parent) = config.path.parent() {
20 std::fs::create_dir_all(parent).map_err(|e| {
21 MetricsError::ConfigError(format!(
22 "Failed to create metrics directory {}: {}",
23 parent.display(),
24 e
25 ))
26 })?;
27 }
28 Ok(Self {
29 path: config.path,
30 pretty_print: config.pretty_print,
31 })
32 }
33}
34
35#[async_trait]
36impl MetricsExporter for FileExporter {
37 async fn export(&self, snapshot: &MetricsSnapshot) -> Result<(), MetricsError> {
38 let json = if self.pretty_print {
39 serde_json::to_string_pretty(snapshot)?
40 } else {
41 serde_json::to_string(snapshot)?
42 };
43
44 let path = self.path.clone();
45
46 tokio::task::spawn_blocking(move || -> Result<(), MetricsError> {
48 use std::io::Write;
49
50 let parent = path.parent().unwrap_or_else(|| std::path::Path::new("."));
51 let mut tmp = tempfile::NamedTempFile::new_in(parent)?;
52 tmp.write_all(json.as_bytes())?;
53 tmp.flush()?;
54 tmp.persist(&path).map_err(|e| {
55 MetricsError::ExportFailed(format!(
56 "Failed to persist metrics file {}: {}",
57 path.display(),
58 e
59 ))
60 })?;
61 Ok(())
62 })
63 .await
64 .map_err(|e| MetricsError::ExportFailed(format!("Blocking task panicked: {}", e)))??;
65
66 tracing::debug!("Metrics snapshot written to {}", self.path.display());
67 Ok(())
68 }
69
70 async fn shutdown(&self) -> Result<(), MetricsError> {
71 Ok(())
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use crate::metrics::{
79 LoadBalancerMetrics, SchedulerMetrics, SystemResourceMetrics, TaskManagerMetrics,
80 };
81
82 fn sample_snapshot() -> MetricsSnapshot {
83 MetricsSnapshot {
84 timestamp: 1700000000,
85 scheduler: SchedulerMetrics {
86 total_scheduled: 10,
87 uptime_seconds: 3600,
88 running_agents: 5,
89 queued_agents: 3,
90 suspended_agents: 2,
91 max_capacity: 1000,
92 load_factor: 0.005,
93 },
94 task_manager: TaskManagerMetrics {
95 total_tasks: 5,
96 healthy_tasks: 4,
97 average_uptime_seconds: 1800.0,
98 total_memory_usage: 1024,
99 },
100 load_balancer: LoadBalancerMetrics {
101 total_allocations: 100,
102 active_allocations: 5,
103 memory_utilization: 0.45,
104 cpu_utilization: 0.30,
105 allocation_failures: 2,
106 average_allocation_time_ms: 1.5,
107 },
108 system: SystemResourceMetrics {
109 memory_usage_mb: 512.0,
110 cpu_usage_percent: 30.0,
111 },
112 compaction: None,
113 }
114 }
115
116 #[tokio::test]
117 async fn test_file_exporter_write_and_read() {
118 let dir = tempfile::tempdir().unwrap();
119 let path = dir.path().join("metrics.json");
120
121 let exporter = FileExporter::new(FileMetricsConfig {
122 path: path.clone(),
123 pretty_print: true,
124 })
125 .unwrap();
126
127 let snapshot = sample_snapshot();
128 exporter.export(&snapshot).await.unwrap();
129
130 let content = std::fs::read_to_string(&path).unwrap();
131 let loaded: MetricsSnapshot = serde_json::from_str(&content).unwrap();
132 assert_eq!(loaded.timestamp, 1700000000);
133 assert_eq!(loaded.scheduler.running_agents, 5);
134 }
135
136 #[tokio::test]
137 async fn test_file_exporter_creates_parent_dirs() {
138 let dir = tempfile::tempdir().unwrap();
139 let path = dir.path().join("nested").join("deep").join("metrics.json");
140
141 let exporter = FileExporter::new(FileMetricsConfig {
142 path: path.clone(),
143 pretty_print: false,
144 })
145 .unwrap();
146
147 let snapshot = sample_snapshot();
148 exporter.export(&snapshot).await.unwrap();
149 assert!(path.exists());
150 }
151
152 #[tokio::test]
153 async fn test_file_exporter_compact_json() {
154 let dir = tempfile::tempdir().unwrap();
155 let path = dir.path().join("compact.json");
156
157 let exporter = FileExporter::new(FileMetricsConfig {
158 path: path.clone(),
159 pretty_print: false,
160 })
161 .unwrap();
162
163 let snapshot = sample_snapshot();
164 exporter.export(&snapshot).await.unwrap();
165
166 let content = std::fs::read_to_string(&path).unwrap();
167 assert!(!content.trim().contains('\n'));
169 }
170
171 #[tokio::test]
172 async fn test_file_exporter_shutdown() {
173 let dir = tempfile::tempdir().unwrap();
174 let path = dir.path().join("shutdown.json");
175
176 let exporter = FileExporter::new(FileMetricsConfig {
177 path,
178 pretty_print: true,
179 })
180 .unwrap();
181
182 assert!(exporter.shutdown().await.is_ok());
183 }
184
185 #[tokio::test]
186 async fn test_file_exporter_overwrite() {
187 let dir = tempfile::tempdir().unwrap();
188 let path = dir.path().join("overwrite.json");
189
190 let exporter = FileExporter::new(FileMetricsConfig {
191 path: path.clone(),
192 pretty_print: false,
193 })
194 .unwrap();
195
196 let mut snapshot = sample_snapshot();
197 exporter.export(&snapshot).await.unwrap();
198
199 snapshot.timestamp = 1700000001;
201 snapshot.scheduler.running_agents = 42;
202 exporter.export(&snapshot).await.unwrap();
203
204 let content = std::fs::read_to_string(&path).unwrap();
205 let loaded: MetricsSnapshot = serde_json::from_str(&content).unwrap();
206 assert_eq!(loaded.timestamp, 1700000001);
207 assert_eq!(loaded.scheduler.running_agents, 42);
208 }
209}