1use crate::error::{CleanroomError, Result};
27use serde::{Deserialize, Serialize};
28use std::path::{Path, PathBuf};
29use std::process::Command;
30use tracing::{debug, info, warn};
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct RegistryStatistics {
38 pub total_groups: usize,
40 pub total_attributes: usize,
42 pub required_attributes: usize,
44 pub recommended_attributes: usize,
46 pub optional_attributes: usize,
48 pub total_spans: usize,
50 pub total_metrics: usize,
52 pub total_events: usize,
54 pub required_coverage: f64,
56}
57
58impl RegistryStatistics {
59 pub fn coverage_percentage(&self) -> f64 {
61 if self.total_attributes == 0 {
62 return 0.0;
63 }
64 (self.required_attributes as f64 / self.total_attributes as f64) * 100.0
65 }
66
67 pub fn is_production_ready(&self) -> bool {
69 self.coverage_percentage() >= 80.0
70 }
71
72 pub fn quality_score(&self) -> f64 {
80 let coverage_score = (self.required_coverage * 40.0).min(40.0);
82
83 let recommended_ratio = if self.total_attributes > 0 {
85 (self.recommended_attributes as f64 / self.total_attributes as f64) * 30.0
86 } else {
87 0.0
88 };
89
90 let has_spans = if self.total_spans > 0 { 7.0 } else { 0.0 };
92 let has_metrics = if self.total_metrics > 0 { 7.0 } else { 0.0 };
93 let has_events = if self.total_events > 0 { 6.0 } else { 0.0 };
94 let diversity_score = has_spans + has_metrics + has_events;
95
96 let total_signals = self.total_spans + self.total_metrics + self.total_events;
98 let completeness_score = if total_signals > 0 {
99 let avg_attrs = self.total_attributes as f64 / total_signals as f64;
100 (avg_attrs / 10.0 * 10.0).min(10.0) } else {
102 0.0
103 };
104
105 coverage_score + recommended_ratio + diversity_score + completeness_score
106 }
107
108 pub fn health_status(&self) -> HealthStatus {
110 let score = self.quality_score();
111 match score {
112 s if s >= 90.0 => HealthStatus::Excellent,
113 s if s >= 75.0 => HealthStatus::Good,
114 s if s >= 60.0 => HealthStatus::Fair,
115 s if s >= 40.0 => HealthStatus::Poor,
116 _ => HealthStatus::Critical,
117 }
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
123pub enum HealthStatus {
124 Critical,
126 Poor,
128 Fair,
130 Good,
132 Excellent,
134}
135
136impl std::fmt::Display for HealthStatus {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 match self {
139 HealthStatus::Excellent => write!(f, "Excellent (90-100)"),
140 HealthStatus::Good => write!(f, "Good (75-89)"),
141 HealthStatus::Fair => write!(f, "Fair (60-74)"),
142 HealthStatus::Poor => write!(f, "Poor (40-59)"),
143 HealthStatus::Critical => write!(f, "Critical (0-39)"),
144 }
145 }
146}
147
148pub struct WeaverStats {
153 registry_path: PathBuf,
154}
155
156impl WeaverStats {
157 pub fn new<P: AsRef<Path>>(registry_path: P) -> Self {
159 Self {
160 registry_path: registry_path.as_ref().to_path_buf(),
161 }
162 }
163
164 pub fn collect(&self) -> Result<RegistryStatistics> {
175 info!(
176 "📊 Collecting statistics from registry: {}",
177 self.registry_path.display()
178 );
179
180 if !self.registry_path.exists() {
182 return Err(CleanroomError::validation_error(format!(
183 "Registry not found: {}",
184 self.registry_path.display()
185 )));
186 }
187
188 let output = Command::new("weaver")
190 .args([
191 "registry",
192 "stats",
193 "--registry",
194 &self.registry_path.display().to_string(),
195 ])
196 .output()
197 .map_err(|e| {
198 CleanroomError::internal_error(format!(
199 "Failed to run weaver stats (is it installed?): {}",
200 e
201 ))
202 })?;
203
204 if !output.status.success() {
205 let stderr = String::from_utf8_lossy(&output.stderr);
206 return Err(CleanroomError::validation_error(format!(
207 "Weaver stats failed: {}",
208 stderr
209 )));
210 }
211
212 let stdout = String::from_utf8_lossy(&output.stdout);
213 debug!("Weaver stats output: {}", stdout);
214
215 self.parse_stats_output(&stdout)
217 }
218
219 fn parse_stats_output(&self, output: &str) -> Result<RegistryStatistics> {
223 let mut stats = RegistryStatistics {
224 total_groups: 0,
225 total_attributes: 0,
226 required_attributes: 0,
227 recommended_attributes: 0,
228 optional_attributes: 0,
229 total_spans: 0,
230 total_metrics: 0,
231 total_events: 0,
232 required_coverage: 0.0,
233 };
234
235 for line in output.lines() {
237 let line = line.trim();
238
239 if line.starts_with("Total groups:") {
247 stats.total_groups = self.parse_number(line)?;
248 } else if line.starts_with("Total attributes:") {
249 stats.total_attributes = self.parse_number(line)?;
250 } else if line.starts_with("Required attributes:") {
251 stats.required_attributes = self.parse_number(line)?;
252 } else if line.starts_with("Recommended attributes:") {
253 stats.recommended_attributes = self.parse_number(line)?;
254 } else if line.starts_with("Optional attributes:") {
255 stats.optional_attributes = self.parse_number(line)?;
256 } else if line.starts_with("Total spans:") {
257 stats.total_spans = self.parse_number(line)?;
258 } else if line.starts_with("Total metrics:") {
259 stats.total_metrics = self.parse_number(line)?;
260 } else if line.starts_with("Total events:") {
261 stats.total_events = self.parse_number(line)?;
262 }
263 }
264
265 if stats.total_attributes > 0 {
267 stats.required_coverage =
268 stats.required_attributes as f64 / stats.total_attributes as f64;
269 }
270
271 info!("✅ Statistics collected successfully");
272 debug!("Stats: {:?}", stats);
273
274 Ok(stats)
275 }
276
277 fn parse_number(&self, line: &str) -> Result<usize> {
279 line.split(':')
280 .nth(1)
281 .and_then(|s| s.trim().parse().ok())
282 .ok_or_else(|| {
283 CleanroomError::serialization_error(format!(
284 "Failed to parse number from: {}",
285 line
286 ))
287 })
288 }
289
290 pub fn generate_report(&self, stats: &RegistryStatistics) -> String {
292 let mut report = String::new();
293
294 report.push_str("📊 Weaver Registry Statistics Report\n");
295 report.push_str("═══════════════════════════════════════\n\n");
296
297 report.push_str("📦 Registry Overview:\n");
299 report.push_str(&format!(" Groups: {}\n", stats.total_groups));
300 report.push_str(&format!(
301 " Total Attributes: {}\n",
302 stats.total_attributes
303 ));
304 report.push_str(&format!(
305 " - Required: {} ({:.1}%)\n",
306 stats.required_attributes,
307 stats.coverage_percentage()
308 ));
309 report.push_str(&format!(
310 " - Recommended: {}\n",
311 stats.recommended_attributes
312 ));
313 report.push_str(&format!(" - Optional: {}\n", stats.optional_attributes));
314 report.push('\n');
315
316 report.push_str("📡 Signal Types:\n");
318 report.push_str(&format!(" Spans: {}\n", stats.total_spans));
319 report.push_str(&format!(" Metrics: {}\n", stats.total_metrics));
320 report.push_str(&format!(" Events: {}\n", stats.total_events));
321 report.push('\n');
322
323 let score = stats.quality_score();
325 let status = stats.health_status();
326 report.push_str("🏆 Quality Metrics:\n");
327 report.push_str(&format!(" Quality Score: {:.1}/100\n", score));
328 report.push_str(&format!(" Health Status: {}\n", status));
329 report.push_str(&format!(
330 " Production Ready: {}\n",
331 if stats.is_production_ready() {
332 "✅ YES"
333 } else {
334 "❌ NO"
335 }
336 ));
337 report.push('\n');
338
339 report.push_str("💡 Recommendations:\n");
341 if stats.coverage_percentage() < 80.0 {
342 report.push_str(" ⚠️ Increase required attribute coverage to >= 80%\n");
343 }
344 if stats.total_spans == 0 {
345 report.push_str(" ⚠️ Add span definitions for tracing\n");
346 }
347 if stats.total_metrics == 0 {
348 report.push_str(" ⚠️ Add metric definitions for monitoring\n");
349 }
350 if stats.total_events == 0 {
351 report.push_str(" ⚠️ Consider adding event definitions for lifecycle tracking\n");
352 }
353 if stats.health_status() == HealthStatus::Excellent {
354 report.push_str(" ✅ Registry is in excellent shape!\n");
355 }
356
357 report
358 }
359
360 pub fn validate_cicd_gate(&self, stats: &RegistryStatistics) -> Result<()> {
364 let mut errors = Vec::new();
365
366 if stats.coverage_percentage() < 80.0 {
368 errors.push(format!(
369 "Coverage {:.1}% below minimum 80%",
370 stats.coverage_percentage()
371 ));
372 }
373
374 if stats.total_spans == 0 && stats.total_metrics == 0 && stats.total_events == 0 {
376 errors.push("No signals defined (need at least spans, metrics, or events)".to_string());
377 }
378
379 let score = stats.quality_score();
381 if score < 75.0 {
382 errors.push(format!("Quality score {:.1} below minimum 75", score));
383 }
384
385 if errors.is_empty() {
386 info!("✅ CI/CD gate passed");
387 Ok(())
388 } else {
389 warn!("❌ CI/CD gate failed:");
390 for error in &errors {
391 warn!(" - {}", error);
392 }
393 Err(CleanroomError::validation_error(format!(
394 "CI/CD gate failed: {}",
395 errors.join(", ")
396 )))
397 }
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn test_registry_statistics_coverage_percentage() {
407 let stats = RegistryStatistics {
408 total_groups: 5,
409 total_attributes: 100,
410 required_attributes: 80,
411 recommended_attributes: 15,
412 optional_attributes: 5,
413 total_spans: 10,
414 total_metrics: 5,
415 total_events: 3,
416 required_coverage: 0.8,
417 };
418
419 assert_eq!(stats.coverage_percentage(), 80.0);
420 }
421
422 #[test]
423 fn test_is_production_ready() {
424 let ready = RegistryStatistics {
425 total_attributes: 100,
426 required_attributes: 85,
427 required_coverage: 0.85,
428 ..Default::default()
429 };
430 assert!(ready.is_production_ready());
431
432 let not_ready = RegistryStatistics {
433 total_attributes: 100,
434 required_attributes: 70,
435 required_coverage: 0.70,
436 ..Default::default()
437 };
438 assert!(!not_ready.is_production_ready());
439 }
440
441 #[test]
442 fn test_quality_score_excellent() {
443 let stats = RegistryStatistics {
444 total_groups: 10,
445 total_attributes: 150,
446 required_attributes: 120,
447 recommended_attributes: 80, optional_attributes: 5,
449 total_spans: 15,
450 total_metrics: 10,
451 total_events: 5,
452 required_coverage: 1.0, };
454
455 let score = stats.quality_score();
456 assert!(score >= 75.0, "Expected good score, got {}", score);
461 assert!(stats.health_status() >= HealthStatus::Good);
462 }
463
464 #[test]
465 fn test_quality_score_poor() {
466 let stats = RegistryStatistics {
467 total_attributes: 50,
468 required_attributes: 15,
469 recommended_attributes: 5,
470 optional_attributes: 30,
471 total_spans: 2,
472 total_metrics: 0,
473 total_events: 0,
474 required_coverage: 0.3,
475 ..Default::default()
476 };
477
478 let score = stats.quality_score();
479 assert!(score < 60.0, "Expected poor score, got {}", score);
480 }
481
482 #[test]
483 fn test_health_status_display() {
484 assert_eq!(HealthStatus::Excellent.to_string(), "Excellent (90-100)");
485 assert_eq!(HealthStatus::Good.to_string(), "Good (75-89)");
486 assert_eq!(HealthStatus::Critical.to_string(), "Critical (0-39)");
487 }
488
489 #[test]
490 fn test_weaver_stats_creation() {
491 let stats = WeaverStats::new("registry/");
492 assert_eq!(stats.registry_path, PathBuf::from("registry/"));
493 }
494
495 #[test]
496 fn test_parse_number() {
497 let stats = WeaverStats::new("test");
498 assert_eq!(stats.parse_number("Total groups: 42").unwrap(), 42);
499 assert_eq!(stats.parse_number("Required attributes: 89").unwrap(), 89);
500 assert!(stats.parse_number("Invalid line").is_err());
501 }
502}
503
504impl Default for RegistryStatistics {
505 fn default() -> Self {
506 Self {
507 total_groups: 0,
508 total_attributes: 0,
509 required_attributes: 0,
510 recommended_attributes: 0,
511 optional_attributes: 0,
512 total_spans: 0,
513 total_metrics: 0,
514 total_events: 0,
515 required_coverage: 0.0,
516 }
517 }
518}