1use prometheus::{
17 Error as PrometheusError, GaugeVec, Histogram, HistogramOpts, IntCounter, IntGauge,
18 IntGaugeVec, Opts, Registry,
19};
20
21#[derive(Clone)]
23pub struct RuntimeMetrics {
24 pub registry: Registry,
26
27 pub vm_boot_duration: Histogram,
30 pub vm_count: IntGaugeVec,
32 pub vm_created_total: IntCounter,
34 pub vm_destroyed_total: IntCounter,
36
37 pub vm_cpu_percent: GaugeVec,
40 pub vm_memory_bytes: GaugeVec,
42
43 pub exec_total: IntCounter,
46 pub exec_duration: Histogram,
48 pub exec_errors_total: IntCounter,
50
51 pub image_pull_total: IntCounter,
54 pub image_pull_duration: Histogram,
56 pub image_build_total: IntCounter,
58 pub rootfs_cache_hits: IntCounter,
60 pub rootfs_cache_misses: IntCounter,
62
63 pub warm_pool_size: IntGauge,
66 pub warm_pool_capacity: IntGauge,
68 pub warm_pool_hits: IntCounter,
70 pub warm_pool_misses: IntCounter,
72}
73
74impl RuntimeMetrics {
75 pub fn new() -> Self {
77 Self::try_new().expect("static RuntimeMetrics descriptors should be valid")
78 }
79
80 pub fn try_new() -> Result<Self, PrometheusError> {
82 let registry = Registry::new();
83 Self::try_with_registry(registry)
84 }
85
86 pub fn with_registry(registry: Registry) -> Self {
88 Self::try_with_registry(registry)
89 .expect("static RuntimeMetrics descriptors should not conflict")
90 }
91
92 pub fn try_with_registry(registry: Registry) -> Result<Self, PrometheusError> {
94 let vm_boot_duration = Histogram::with_opts(
96 HistogramOpts::new(
97 "a3s_box_vm_boot_duration_seconds",
98 "VM boot duration in seconds",
99 )
100 .buckets(vec![0.05, 0.1, 0.15, 0.2, 0.3, 0.5, 1.0, 2.0, 5.0, 10.0]),
101 )?;
102
103 let vm_count = IntGaugeVec::new(
104 Opts::new("a3s_box_vm_count", "Number of VMs by state"),
105 &["state"],
106 )?;
107
108 let vm_created_total = IntCounter::new("a3s_box_vm_created_total", "Total VMs created")?;
109
110 let vm_destroyed_total =
111 IntCounter::new("a3s_box_vm_destroyed_total", "Total VMs destroyed")?;
112
113 let vm_cpu_percent = GaugeVec::new(
115 Opts::new("a3s_box_vm_cpu_percent", "VM CPU usage percentage"),
116 &["box_id"],
117 )?;
118
119 let vm_memory_bytes = GaugeVec::new(
120 Opts::new("a3s_box_vm_memory_bytes", "VM memory usage in bytes"),
121 &["box_id"],
122 )?;
123
124 let exec_total = IntCounter::new("a3s_box_exec_total", "Total exec commands executed")?;
126
127 let exec_duration = Histogram::with_opts(
128 HistogramOpts::new(
129 "a3s_box_exec_duration_seconds",
130 "Exec command duration in seconds",
131 )
132 .buckets(vec![0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0, 30.0, 60.0]),
133 )?;
134
135 let exec_errors_total =
136 IntCounter::new("a3s_box_exec_errors_total", "Total failed exec commands")?;
137
138 let image_pull_total = IntCounter::new("a3s_box_image_pull_total", "Total image pulls")?;
140
141 let image_pull_duration = Histogram::with_opts(
142 HistogramOpts::new(
143 "a3s_box_image_pull_duration_seconds",
144 "Image pull duration in seconds",
145 )
146 .buckets(vec![0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0, 120.0]),
147 )?;
148
149 let image_build_total = IntCounter::new("a3s_box_image_build_total", "Total image builds")?;
150
151 let rootfs_cache_hits =
152 IntCounter::new("a3s_box_rootfs_cache_hits_total", "Rootfs cache hits")?;
153
154 let rootfs_cache_misses =
155 IntCounter::new("a3s_box_rootfs_cache_misses_total", "Rootfs cache misses")?;
156
157 let warm_pool_size = IntGauge::new(
159 "a3s_box_warm_pool_size",
160 "Current warm pool size (idle VMs)",
161 )?;
162
163 let warm_pool_capacity =
164 IntGauge::new("a3s_box_warm_pool_capacity", "Warm pool max capacity")?;
165
166 let warm_pool_hits = IntCounter::new(
167 "a3s_box_warm_pool_hits_total",
168 "VMs allocated from warm pool",
169 )?;
170
171 let warm_pool_misses = IntCounter::new(
172 "a3s_box_warm_pool_misses_total",
173 "VMs created fresh (warm pool miss)",
174 )?;
175
176 registry.register(Box::new(vm_boot_duration.clone()))?;
178 registry.register(Box::new(vm_count.clone()))?;
179 registry.register(Box::new(vm_created_total.clone()))?;
180 registry.register(Box::new(vm_destroyed_total.clone()))?;
181 registry.register(Box::new(vm_cpu_percent.clone()))?;
182 registry.register(Box::new(vm_memory_bytes.clone()))?;
183 registry.register(Box::new(exec_total.clone()))?;
184 registry.register(Box::new(exec_duration.clone()))?;
185 registry.register(Box::new(exec_errors_total.clone()))?;
186 registry.register(Box::new(image_pull_total.clone()))?;
187 registry.register(Box::new(image_pull_duration.clone()))?;
188 registry.register(Box::new(image_build_total.clone()))?;
189 registry.register(Box::new(rootfs_cache_hits.clone()))?;
190 registry.register(Box::new(rootfs_cache_misses.clone()))?;
191 registry.register(Box::new(warm_pool_size.clone()))?;
192 registry.register(Box::new(warm_pool_capacity.clone()))?;
193 registry.register(Box::new(warm_pool_hits.clone()))?;
194 registry.register(Box::new(warm_pool_misses.clone()))?;
195
196 Ok(Self {
197 registry,
198 vm_boot_duration,
199 vm_count,
200 vm_created_total,
201 vm_destroyed_total,
202 vm_cpu_percent,
203 vm_memory_bytes,
204 exec_total,
205 exec_duration,
206 exec_errors_total,
207 image_pull_total,
208 image_pull_duration,
209 image_build_total,
210 rootfs_cache_hits,
211 rootfs_cache_misses,
212 warm_pool_size,
213 warm_pool_capacity,
214 warm_pool_hits,
215 warm_pool_misses,
216 })
217 }
218
219 pub fn encode(&self) -> String {
221 use prometheus::Encoder;
222 let encoder = prometheus::TextEncoder::new();
223 let metric_families = self.registry.gather();
224 let mut buffer = Vec::new();
225 encoder
226 .encode(&metric_families, &mut buffer)
227 .expect("encode");
228 String::from_utf8(buffer).expect("utf8")
229 }
230}
231
232impl Default for RuntimeMetrics {
233 fn default() -> Self {
234 Self::new()
235 }
236}
237
238impl a3s_box_core::traits::MetricsCollector for RuntimeMetrics {
239 fn record_vm_boot(&self, duration_secs: f64) {
240 self.vm_boot_duration.observe(duration_secs);
241 }
242
243 fn inc_vm_state(&self, state: &str) {
244 self.vm_count.with_label_values(&[state]).inc();
245 }
246
247 fn dec_vm_state(&self, state: &str) {
248 self.vm_count.with_label_values(&[state]).dec();
249 }
250
251 fn inc_vm_created(&self) {
252 self.vm_created_total.inc();
253 }
254
255 fn inc_vm_destroyed(&self) {
256 self.vm_destroyed_total.inc();
257 }
258
259 fn record_exec(&self, duration_secs: f64, success: bool) {
260 self.exec_total.inc();
261 self.exec_duration.observe(duration_secs);
262 if !success {
263 self.exec_errors_total.inc();
264 }
265 }
266
267 fn inc_cache_hit(&self) {
268 self.rootfs_cache_hits.inc();
269 }
270
271 fn inc_cache_miss(&self) {
272 self.rootfs_cache_misses.inc();
273 }
274}
275
276impl std::fmt::Debug for RuntimeMetrics {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 f.debug_struct("RuntimeMetrics").finish()
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_metrics_creation() {
288 let m = RuntimeMetrics::new();
289 assert_eq!(m.vm_created_total.get(), 0);
290 assert_eq!(m.vm_destroyed_total.get(), 0);
291 assert_eq!(m.exec_total.get(), 0);
292 }
293
294 #[test]
295 fn test_vm_boot_duration_observe() {
296 let m = RuntimeMetrics::new();
297 m.vm_boot_duration.observe(0.195);
298 m.vm_boot_duration.observe(0.210);
299 assert_eq!(m.vm_boot_duration.get_sample_count(), 2);
300 }
301
302 #[test]
303 fn test_vm_count_by_state() {
304 let m = RuntimeMetrics::new();
305 m.vm_count.with_label_values(&["ready"]).set(3);
306 m.vm_count.with_label_values(&["busy"]).set(1);
307 assert_eq!(m.vm_count.with_label_values(&["ready"]).get(), 3);
308 assert_eq!(m.vm_count.with_label_values(&["busy"]).get(), 1);
309 assert_eq!(m.vm_count.with_label_values(&["stopped"]).get(), 0);
310 }
311
312 #[test]
313 fn test_vm_created_destroyed_counters() {
314 let m = RuntimeMetrics::new();
315 m.vm_created_total.inc();
316 m.vm_created_total.inc();
317 m.vm_destroyed_total.inc();
318 assert_eq!(m.vm_created_total.get(), 2);
319 assert_eq!(m.vm_destroyed_total.get(), 1);
320 }
321
322 #[test]
323 fn test_vm_resource_gauges() {
324 let m = RuntimeMetrics::new();
325 m.vm_cpu_percent.with_label_values(&["box-123"]).set(45.5);
326 m.vm_memory_bytes
327 .with_label_values(&["box-123"])
328 .set(256.0 * 1024.0 * 1024.0);
329 assert_eq!(m.vm_cpu_percent.with_label_values(&["box-123"]).get(), 45.5);
330 }
331
332 #[test]
333 fn test_exec_metrics() {
334 let m = RuntimeMetrics::new();
335 m.exec_total.inc();
336 m.exec_duration.observe(0.05);
337 m.exec_errors_total.inc();
338 assert_eq!(m.exec_total.get(), 1);
339 assert_eq!(m.exec_errors_total.get(), 1);
340 assert_eq!(m.exec_duration.get_sample_count(), 1);
341 }
342
343 #[test]
344 fn test_image_metrics() {
345 let m = RuntimeMetrics::new();
346 m.image_pull_total.inc();
347 m.image_pull_duration.observe(3.5);
348 m.image_build_total.inc();
349 m.rootfs_cache_hits.inc();
350 m.rootfs_cache_misses.inc();
351 m.rootfs_cache_misses.inc();
352 assert_eq!(m.image_pull_total.get(), 1);
353 assert_eq!(m.rootfs_cache_hits.get(), 1);
354 assert_eq!(m.rootfs_cache_misses.get(), 2);
355 }
356
357 #[test]
358 fn test_warm_pool_metrics() {
359 let m = RuntimeMetrics::new();
360 m.warm_pool_capacity.set(10);
361 m.warm_pool_size.set(5);
362 m.warm_pool_hits.inc();
363 m.warm_pool_misses.inc();
364 assert_eq!(m.warm_pool_capacity.get(), 10);
365 assert_eq!(m.warm_pool_size.get(), 5);
366 assert_eq!(m.warm_pool_hits.get(), 1);
367 assert_eq!(m.warm_pool_misses.get(), 1);
368 }
369
370 #[test]
371 fn test_encode_prometheus_format() {
372 let m = RuntimeMetrics::new();
373 m.vm_created_total.inc();
374 m.exec_total.inc();
375 let output = m.encode();
376 assert!(output.contains("a3s_box_vm_created_total 1"));
377 assert!(output.contains("a3s_box_exec_total 1"));
378 assert!(output.contains("# HELP"));
379 assert!(output.contains("# TYPE"));
380 }
381
382 #[test]
383 fn test_metrics_clone() {
384 let m = RuntimeMetrics::new();
385 m.vm_created_total.inc();
386 let m2 = m.clone();
387 assert_eq!(m2.vm_created_total.get(), 1);
389 m.vm_created_total.inc();
390 assert_eq!(m2.vm_created_total.get(), 2);
391 }
392
393 #[test]
394 fn test_metrics_default() {
395 let m = RuntimeMetrics::default();
396 assert_eq!(m.vm_created_total.get(), 0);
397 }
398
399 #[test]
400 fn test_try_with_registry_reports_duplicate_registration() {
401 let registry = Registry::new();
402 let _first = RuntimeMetrics::try_with_registry(registry.clone()).unwrap();
403 let second = RuntimeMetrics::try_with_registry(registry);
404 assert!(second.is_err());
405 }
406}