1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::VecDeque;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum MetricKind {
11 CpuLoad,
13 MemoryPressure,
15 Latency,
17 Coherence,
19 ErrorRate,
21 DiskIo,
23 SwapUsage,
25 ImaginationQuality,
28 ResearchOutput,
30 ScenarioConfidence,
32 SimulationVariance,
34 ConformalCoverage,
39 BrierScore,
42}
43
44impl MetricKind {
45 #[must_use]
47 pub const fn as_str(self) -> &'static str {
48 match self {
49 Self::CpuLoad => "cpu_load",
50 Self::MemoryPressure => "memory_pressure",
51 Self::Latency => "latency",
52 Self::Coherence => "coherence",
53 Self::ErrorRate => "error_rate",
54 Self::DiskIo => "disk_io",
55 Self::SwapUsage => "swap_usage",
56 Self::ImaginationQuality => "imagination_quality",
57 Self::ResearchOutput => "research_output",
58 Self::ScenarioConfidence => "scenario_confidence",
59 Self::SimulationVariance => "simulation_variance",
60 Self::ConformalCoverage => "conformal_coverage",
61 Self::BrierScore => "brier_score",
62 }
63 }
64
65 #[must_use]
67 pub const fn all() -> &'static [Self] {
68 &[
69 Self::CpuLoad,
70 Self::MemoryPressure,
71 Self::Latency,
72 Self::Coherence,
73 Self::ErrorRate,
74 Self::DiskIo,
75 Self::SwapUsage,
76 Self::ImaginationQuality,
77 Self::ResearchOutput,
78 Self::ScenarioConfidence,
79 Self::SimulationVariance,
80 Self::ConformalCoverage,
81 Self::BrierScore,
82 ]
83 }
84
85 #[must_use]
87 pub const fn higher_is_better(self) -> bool {
88 matches!(
89 self,
90 Self::Coherence
91 | Self::ImaginationQuality
92 | Self::ResearchOutput
93 | Self::ScenarioConfidence
94 | Self::ConformalCoverage
95 )
96 }
97
98 #[must_use]
100 pub const fn default_warning(self) -> f32 {
101 match self {
102 Self::CpuLoad | Self::MemoryPressure | Self::SwapUsage | Self::DiskIo => 0.7,
103 Self::Latency => 10.0,
104 Self::Coherence => 0.3, Self::ErrorRate => 0.1,
106 Self::ImaginationQuality => 0.4,
107 Self::ResearchOutput => 0.3,
108 Self::ScenarioConfidence => 0.4,
109 Self::SimulationVariance => 0.3, Self::ConformalCoverage => 0.85,
113 Self::BrierScore => 0.15,
116 }
117 }
118
119 #[must_use]
121 pub const fn default_critical(self) -> f32 {
122 match self {
123 Self::CpuLoad | Self::MemoryPressure | Self::SwapUsage | Self::DiskIo => 0.9,
124 Self::Latency => 50.0,
125 Self::Coherence => 0.1,
126 Self::ErrorRate => 0.3,
127 Self::ImaginationQuality => 0.2,
128 Self::ResearchOutput => 0.1,
129 Self::ScenarioConfidence => 0.2,
130 Self::SimulationVariance => 0.5,
131 Self::ConformalCoverage => 0.8,
132 Self::BrierScore => 0.3,
133 }
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct MetricSample {
140 pub kind: MetricKind,
142 pub value: f32,
144 pub timestamp: DateTime<Utc>,
146}
147
148pub struct MetricTracker {
150 history: Vec<VecDeque<MetricSample>>,
152 capacity: usize,
154}
155
156impl MetricTracker {
157 #[must_use]
159 pub fn new(capacity: usize) -> Self {
160 let mut history = Vec::with_capacity(MetricKind::all().len());
161 for _ in MetricKind::all() {
162 history.push(VecDeque::with_capacity(capacity));
163 }
164 Self { history, capacity }
165 }
166
167 fn index(kind: MetricKind) -> usize {
169 MetricKind::all()
170 .iter()
171 .position(|k| *k == kind)
172 .unwrap_or(0)
173 }
174
175 pub fn record(&mut self, sample: MetricSample) {
177 let idx = Self::index(sample.kind);
178 let buf = &mut self.history[idx];
179 if buf.len() >= self.capacity {
180 buf.pop_front();
181 }
182 buf.push_back(sample);
183 }
184
185 #[must_use]
187 pub fn history(&self, kind: MetricKind) -> Option<&VecDeque<MetricSample>> {
188 let buf = &self.history[Self::index(kind)];
189 if buf.is_empty() { None } else { Some(buf) }
190 }
191
192 #[must_use]
194 pub fn latest(&self, kind: MetricKind) -> Option<&MetricSample> {
195 self.history(kind).and_then(|h| h.back())
196 }
197
198 #[must_use]
200 pub fn sample_count(&self, kind: MetricKind) -> usize {
201 self.history(kind)
202 .map_or(0, std::collections::VecDeque::len)
203 }
204
205 pub fn tracked_kinds(&self) -> impl Iterator<Item = MetricKind> + '_ {
207 MetricKind::all()
208 .iter()
209 .copied()
210 .filter(|kind| self.sample_count(*kind) > 0)
211 }
212
213 #[must_use]
215 pub fn tracked_count(&self) -> usize {
216 self.tracked_kinds().count()
217 }
218
219 #[must_use]
221 pub fn ewma(&self, kind: MetricKind, alpha: f32) -> Option<f32> {
222 let history = self.history(kind)?;
223 if history.is_empty() {
224 return None;
225 }
226
227 let alpha = alpha.clamp(0.0, 1.0);
228 let mut ewma = history.front().unwrap().value;
229 for sample in history.iter().skip(1) {
230 ewma = alpha.mul_add(sample.value, (1.0 - alpha) * ewma);
231 }
232 Some(ewma)
233 }
234
235 #[must_use]
238 pub fn slope(&self, kind: MetricKind) -> Option<f32> {
239 let history = self.history(kind)?;
240 let n = history.len();
241 if n < 2 {
242 return None;
243 }
244
245 let first = history.front().unwrap().value;
247 let last = history.back().unwrap().value;
248 Some((last - first) / (n as f32 - 1.0))
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn metric_kind_as_str() {
258 assert_eq!(MetricKind::CpuLoad.as_str(), "cpu_load");
259 assert_eq!(MetricKind::Coherence.as_str(), "coherence");
260 assert_eq!(MetricKind::ErrorRate.as_str(), "error_rate");
261 }
262
263 #[test]
264 fn metric_kind_higher_is_better() {
265 assert!(MetricKind::Coherence.higher_is_better());
266 assert!(!MetricKind::CpuLoad.higher_is_better());
267 assert!(!MetricKind::ErrorRate.higher_is_better());
268 }
269
270 #[test]
271 fn metric_kind_default_thresholds() {
272 assert_eq!(MetricKind::CpuLoad.default_warning(), 0.7);
273 assert_eq!(MetricKind::CpuLoad.default_critical(), 0.9);
274 assert_eq!(MetricKind::Coherence.default_warning(), 0.3);
275 assert_eq!(MetricKind::Coherence.default_critical(), 0.1);
276 }
277
278 #[test]
279 fn metric_tracker_record_and_history() {
280 let mut tracker = MetricTracker::new(100);
281 tracker.record(MetricSample {
282 kind: MetricKind::CpuLoad,
283 value: 0.3,
284 timestamp: Utc::now(),
285 });
286 tracker.record(MetricSample {
287 kind: MetricKind::CpuLoad,
288 value: 0.5,
289 timestamp: Utc::now(),
290 });
291
292 let hist = tracker.history(MetricKind::CpuLoad).unwrap();
293 assert_eq!(hist.len(), 2);
294 assert_eq!(tracker.sample_count(MetricKind::CpuLoad), 2);
295 }
296
297 #[test]
298 fn metric_tracker_empty_returns_none() {
299 let tracker = MetricTracker::new(100);
300 assert!(tracker.history(MetricKind::CpuLoad).is_none());
301 assert!(tracker.latest(MetricKind::CpuLoad).is_none());
302 }
303
304 #[test]
305 fn metric_tracker_latest() {
306 let mut tracker = MetricTracker::new(100);
307 tracker.record(MetricSample {
308 kind: MetricKind::CpuLoad,
309 value: 0.3,
310 timestamp: Utc::now(),
311 });
312 tracker.record(MetricSample {
313 kind: MetricKind::CpuLoad,
314 value: 0.7,
315 timestamp: Utc::now(),
316 });
317 let latest = tracker.latest(MetricKind::CpuLoad).unwrap();
318 assert!((latest.value - 0.7).abs() < 0.001);
319 }
320
321 #[test]
322 fn metric_tracker_ring_buffer_capacity() {
323 let mut tracker = MetricTracker::new(3);
324 for v in [0.1, 0.2, 0.3, 0.4, 0.5] {
325 tracker.record(MetricSample {
326 kind: MetricKind::CpuLoad,
327 value: v,
328 timestamp: Utc::now(),
329 });
330 }
331 let hist = tracker.history(MetricKind::CpuLoad).unwrap();
332 assert_eq!(hist.len(), 3);
333 assert!((hist.front().unwrap().value - 0.3).abs() < 0.001);
335 assert!((hist.back().unwrap().value - 0.5).abs() < 0.001);
337 }
338
339 #[test]
340 fn metric_tracker_tracked_kinds() {
341 let mut tracker = MetricTracker::new(100);
342 tracker.record(MetricSample {
343 kind: MetricKind::CpuLoad,
344 value: 0.3,
345 timestamp: Utc::now(),
346 });
347 tracker.record(MetricSample {
348 kind: MetricKind::MemoryPressure,
349 value: 0.2,
350 timestamp: Utc::now(),
351 });
352
353 let kinds: Vec<_> = tracker.tracked_kinds().collect();
354 assert_eq!(kinds.len(), 2);
355 assert!(kinds.contains(&MetricKind::CpuLoad));
356 assert!(kinds.contains(&MetricKind::MemoryPressure));
357 assert_eq!(tracker.tracked_count(), 2);
358 }
359
360 #[test]
361 fn metric_tracker_ewma() {
362 let mut tracker = MetricTracker::new(100);
363 for v in [0.1, 0.2, 0.3, 0.4, 0.5] {
364 tracker.record(MetricSample {
365 kind: MetricKind::CpuLoad,
366 value: v,
367 timestamp: Utc::now(),
368 });
369 }
370 let ewma = tracker.ewma(MetricKind::CpuLoad, 0.3).unwrap();
371 assert!(ewma > 0.1 && ewma < 0.5);
373 }
374
375 #[test]
376 fn metric_tracker_ewma_alpha_clamped() {
377 let mut tracker = MetricTracker::new(100);
378 tracker.record(MetricSample {
379 kind: MetricKind::CpuLoad,
380 value: 0.5,
381 timestamp: Utc::now(),
382 });
383 tracker.record(MetricSample {
384 kind: MetricKind::CpuLoad,
385 value: 0.9,
386 timestamp: Utc::now(),
387 });
388 let ewma = tracker.ewma(MetricKind::CpuLoad, 2.0).unwrap();
390 assert!((ewma - 0.9).abs() < 0.001);
391 }
392
393 #[test]
394 fn metric_tracker_ewma_empty() {
395 let tracker = MetricTracker::new(100);
396 assert!(tracker.ewma(MetricKind::CpuLoad, 0.3).is_none());
397 }
398
399 #[test]
400 fn metric_tracker_slope_increasing() {
401 let mut tracker = MetricTracker::new(100);
402 for v in [0.1, 0.2, 0.3, 0.4, 0.5] {
403 tracker.record(MetricSample {
404 kind: MetricKind::CpuLoad,
405 value: v,
406 timestamp: Utc::now(),
407 });
408 }
409 let slope = tracker.slope(MetricKind::CpuLoad).unwrap();
410 assert!(slope > 0.0);
411 assert!((slope - 0.1).abs() < 0.001);
412 }
413
414 #[test]
415 fn metric_tracker_slope_decreasing() {
416 let mut tracker = MetricTracker::new(100);
417 for v in [0.5, 0.4, 0.3, 0.2, 0.1] {
418 tracker.record(MetricSample {
419 kind: MetricKind::CpuLoad,
420 value: v,
421 timestamp: Utc::now(),
422 });
423 }
424 let slope = tracker.slope(MetricKind::CpuLoad).unwrap();
425 assert!(slope < 0.0);
426 }
427
428 #[test]
429 fn metric_tracker_slope_insufficient_data() {
430 let mut tracker = MetricTracker::new(100);
431 tracker.record(MetricSample {
432 kind: MetricKind::CpuLoad,
433 value: 0.3,
434 timestamp: Utc::now(),
435 });
436 assert!(tracker.slope(MetricKind::CpuLoad).is_none());
437 }
438
439 #[test]
440 fn metric_sample_serialization() {
441 let sample = MetricSample {
442 kind: MetricKind::CpuLoad,
443 value: 0.42,
444 timestamp: Utc::now(),
445 };
446 let json = serde_json::to_string(&sample).unwrap();
447 let back: MetricSample = serde_json::from_str(&json).unwrap();
448 assert_eq!(back.kind, MetricKind::CpuLoad);
449 assert!((back.value - 0.42).abs() < 0.001);
450 }
451
452 #[test]
453 fn metric_kind_all_count() {
454 assert_eq!(MetricKind::all().len(), 13);
455 }
456}