1use crate::error::{CleanroomError, Result};
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10#[derive(Debug, Deserialize, Serialize, Clone)]
15pub struct WeaverConfig {
16 #[serde(default = "default_true")]
19 pub enabled: bool,
20
21 #[serde(default = "default_registry_path")]
24 pub registry_path: String,
25
26 #[serde(default)]
30 pub otlp_port: u16,
31
32 #[serde(default)]
36 pub admin_port: u16,
37
38 #[serde(default = "default_output_dir")]
41 pub output_dir: String,
42
43 #[serde(default)]
46 pub stream: bool,
47
48 #[serde(default)]
51 pub fail_fast: bool,
52
53 #[serde(default)]
55 pub validation: Option<ValidationConfig>,
56
57 #[serde(default, rename = "80_20")]
59 pub eighty_twenty: Option<EightyTwentyConfig>,
60
61 #[serde(default)]
63 pub collector: Option<CollectorConfig>,
64
65 #[serde(default)]
67 pub reports: Option<ReportsConfig>,
68
69 #[serde(default)]
71 pub performance: Option<PerformanceConfig>,
72}
73
74#[derive(Debug, Deserialize, Serialize, Clone)]
76pub struct ValidationConfig {
77 #[serde(default = "default_strict_mode")]
80 pub mode: ValidationMode,
81
82 #[serde(default = "default_true")]
85 pub fail_on_violation: bool,
86
87 #[serde(default)]
90 pub fail_on_missing_optional: bool,
91
92 #[serde(default = "default_coverage_threshold")]
96 pub coverage_threshold: f64,
97
98 #[serde(default = "default_inactivity_timeout")]
101 pub inactivity_timeout: u64,
102
103 #[serde(default = "default_diagnostic_format")]
106 pub diagnostic_format: DiagnosticFormat,
107}
108
109#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
111#[serde(rename_all = "snake_case")]
112pub enum ValidationMode {
113 Strict,
115 Lenient,
117 #[serde(rename = "80_20")]
119 EightyTwenty,
120 Minimal,
122}
123
124#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
126#[serde(rename_all = "snake_case")]
127pub enum DiagnosticFormat {
128 Ansi,
130 Json,
132 GhWorkflowCommand,
134 Auto,
136}
137
138#[derive(Debug, Deserialize, Serialize, Clone)]
142pub struct EightyTwentyConfig {
143 #[serde(default)]
146 pub enabled: bool,
147
148 #[serde(default)]
151 pub critical_spans: Vec<String>,
152
153 #[serde(default)]
156 pub required_attributes: Vec<String>,
157
158 #[serde(default)]
161 pub optional_attributes: Vec<String>,
162
163 #[serde(default = "default_full_coverage")]
166 pub critical_span_coverage: f64,
167
168 #[serde(default = "default_full_coverage")]
171 pub required_attribute_coverage: f64,
172
173 #[serde(default = "default_half_coverage")]
176 pub optional_attribute_coverage: f64,
177}
178
179#[derive(Debug, Deserialize, Serialize, Clone)]
181pub struct CollectorConfig {
182 #[serde(default)]
185 pub use_existing: bool,
186
187 #[serde(default)]
190 pub endpoint: Option<String>,
191
192 #[serde(default = "default_true")]
195 pub auto_start: bool,
196
197 #[serde(default = "default_collector_image")]
200 pub image: String,
201
202 #[serde(default = "default_health_check_timeout")]
205 pub health_check_timeout: u64,
206
207 #[serde(default = "default_startup_grace_period")]
210 pub startup_grace_period: u64,
211}
212
213#[derive(Debug, Deserialize, Serialize, Clone)]
215pub struct ReportsConfig {
216 #[serde(default = "default_true")]
219 pub json_report: bool,
220
221 #[serde(default = "default_json_report_file")]
224 pub json_report_file: String,
225
226 #[serde(default)]
229 pub html_report: bool,
230
231 #[serde(default = "default_html_report_file")]
234 pub html_report_file: String,
235
236 #[serde(default)]
239 pub junit_report: bool,
240
241 #[serde(default = "default_junit_report_file")]
244 pub junit_report_file: String,
245
246 #[serde(default = "default_true")]
249 pub include_samples: bool,
250
251 #[serde(default = "default_max_samples")]
254 pub max_samples_per_violation: u32,
255}
256
257#[derive(Debug, Deserialize, Serialize, Clone)]
259pub struct PerformanceConfig {
260 #[serde(default = "default_buffer_size")]
263 pub buffer_size: u64,
264
265 #[serde(default = "default_max_workers")]
268 pub max_workers: u32,
269
270 #[serde(default = "default_true")]
273 pub batching: bool,
274
275 #[serde(default = "default_batch_size")]
278 pub batch_size: u32,
279
280 #[serde(default = "default_batch_timeout")]
283 pub batch_timeout_ms: u64,
284}
285
286fn default_true() -> bool {
291 true
292}
293
294fn default_registry_path() -> String {
295 "registry".to_string()
296}
297
298fn default_output_dir() -> String {
299 "./validation_output".to_string()
300}
301
302fn default_strict_mode() -> ValidationMode {
303 ValidationMode::Strict
304}
305
306fn default_coverage_threshold() -> f64 {
307 80.0
308}
309
310fn default_inactivity_timeout() -> u64 {
311 5
312}
313
314fn default_diagnostic_format() -> DiagnosticFormat {
315 DiagnosticFormat::Ansi
316}
317
318fn default_full_coverage() -> f64 {
319 100.0
320}
321
322fn default_half_coverage() -> f64 {
323 50.0
324}
325
326fn default_collector_image() -> String {
327 "otel/opentelemetry-collector:latest".to_string()
328}
329
330fn default_health_check_timeout() -> u64 {
331 30
332}
333
334fn default_startup_grace_period() -> u64 {
335 2
336}
337
338fn default_json_report_file() -> String {
339 "validation_report.json".to_string()
340}
341
342fn default_html_report_file() -> String {
343 "validation_report.html".to_string()
344}
345
346fn default_junit_report_file() -> String {
347 "weaver_validation.xml".to_string()
348}
349
350fn default_max_samples() -> u32 {
351 3
352}
353
354fn default_buffer_size() -> u64 {
355 1048576 }
357
358fn default_max_workers() -> u32 {
359 4
360}
361
362fn default_batch_size() -> u32 {
363 100
364}
365
366fn default_batch_timeout() -> u64 {
367 1000
368}
369
370impl Default for WeaverConfig {
375 fn default() -> Self {
376 Self {
377 enabled: true,
378 registry_path: "registry".to_string(),
379 otlp_port: 0,
380 admin_port: 0,
381 output_dir: "./validation_output".to_string(),
382 stream: false,
383 fail_fast: false,
384 validation: Some(ValidationConfig::default()),
385 eighty_twenty: None,
386 collector: Some(CollectorConfig::default()),
387 reports: Some(ReportsConfig::default()),
388 performance: Some(PerformanceConfig::default()),
389 }
390 }
391}
392
393impl Default for ValidationConfig {
394 fn default() -> Self {
395 Self {
396 mode: ValidationMode::Strict,
397 fail_on_violation: true,
398 fail_on_missing_optional: false,
399 coverage_threshold: 80.0,
400 inactivity_timeout: 5,
401 diagnostic_format: DiagnosticFormat::Ansi,
402 }
403 }
404}
405
406impl Default for EightyTwentyConfig {
407 fn default() -> Self {
408 Self {
409 enabled: false,
410 critical_spans: Vec::new(),
411 required_attributes: Vec::new(),
412 optional_attributes: Vec::new(),
413 critical_span_coverage: 100.0,
414 required_attribute_coverage: 100.0,
415 optional_attribute_coverage: 50.0,
416 }
417 }
418}
419
420impl Default for CollectorConfig {
421 fn default() -> Self {
422 Self {
423 use_existing: false,
424 endpoint: None,
425 auto_start: true,
426 image: "otel/opentelemetry-collector:latest".to_string(),
427 health_check_timeout: 30,
428 startup_grace_period: 2,
429 }
430 }
431}
432
433impl Default for ReportsConfig {
434 fn default() -> Self {
435 Self {
436 json_report: true,
437 json_report_file: "validation_report.json".to_string(),
438 html_report: false,
439 html_report_file: "validation_report.html".to_string(),
440 junit_report: false,
441 junit_report_file: "weaver_validation.xml".to_string(),
442 include_samples: true,
443 max_samples_per_violation: 3,
444 }
445 }
446}
447
448impl Default for PerformanceConfig {
449 fn default() -> Self {
450 Self {
451 buffer_size: 1048576,
452 max_workers: 4,
453 batching: true,
454 batch_size: 100,
455 batch_timeout_ms: 1000,
456 }
457 }
458}
459
460impl WeaverConfig {
461 pub fn validate(&self) -> Result<()> {
463 if self.otlp_port > 0 && self.otlp_port < 1024 {
465 return Err(CleanroomError::validation_error(
466 "OTLP port must be >= 1024 or 0 for auto-discovery",
467 ));
468 }
469
470 if self.admin_port > 0 && self.admin_port < 1024 {
471 return Err(CleanroomError::validation_error(
472 "Admin port must be >= 1024 or 0 for auto-discovery",
473 ));
474 }
475
476 if self.otlp_port == self.admin_port && self.otlp_port > 0 {
478 return Err(CleanroomError::validation_error(
479 "OTLP port and admin port must be different",
480 ));
481 }
482
483 if let Some(ref validation) = self.validation {
485 validation.validate()?;
486 }
487
488 if let Some(ref eighty_twenty) = self.eighty_twenty {
490 eighty_twenty.validate()?;
491 }
492
493 if let Some(ref validation) = self.validation {
495 if validation.mode == ValidationMode::EightyTwenty && self.eighty_twenty.is_none() {
496 return Err(CleanroomError::validation_error(
497 "80/20 validation mode requires [weaver.80_20] section with critical_spans and required_attributes",
498 ));
499 }
500 }
501
502 if let Some(ref collector) = self.collector {
504 collector.validate()?;
505 }
506
507 Ok(())
508 }
509
510 pub fn to_telemetry_config(&self) -> Result<crate::telemetry::weaver_controller::WeaverConfig> {
512 use crate::telemetry::weaver_controller::WeaverConfig as TelemetryWeaverConfig;
513
514 let registry_path =
516 if self.registry_path.starts_with('/') || self.registry_path.starts_with("~/") {
517 PathBuf::from(self.registry_path.replace(
519 "~/",
520 &format!(
521 "{}/",
522 std::env::var("HOME").unwrap_or_else(|_| ".".to_string())
523 ),
524 ))
525 } else {
526 PathBuf::from(&self.registry_path)
529 };
530
531 Ok(TelemetryWeaverConfig {
532 registry_path,
533 otlp_port: self.otlp_port,
534 admin_port: self.admin_port,
535 output_dir: PathBuf::from(&self.output_dir),
536 stream: self.stream,
537 })
538 }
539}
540
541impl ValidationConfig {
542 pub fn validate(&self) -> Result<()> {
544 if !(0.0..=100.0).contains(&self.coverage_threshold) {
546 return Err(CleanroomError::validation_error(format!(
547 "Coverage threshold must be between 0.0 and 100.0, got {}",
548 self.coverage_threshold
549 )));
550 }
551
552 if self.inactivity_timeout == 0 {
554 return Err(CleanroomError::validation_error(
555 "Inactivity timeout must be greater than 0 seconds",
556 ));
557 }
558
559 Ok(())
560 }
561}
562
563impl EightyTwentyConfig {
564 pub fn validate(&self) -> Result<()> {
566 if self.enabled && self.critical_spans.is_empty() {
568 return Err(CleanroomError::validation_error(
569 "80/20 mode requires at least one critical span in critical_spans list",
570 ));
571 }
572
573 if self.enabled && self.required_attributes.is_empty() {
575 return Err(CleanroomError::validation_error(
576 "80/20 mode requires at least one required attribute in required_attributes list",
577 ));
578 }
579
580 if !(0.0..=100.0).contains(&self.critical_span_coverage) {
582 return Err(CleanroomError::validation_error(format!(
583 "Critical span coverage must be between 0.0 and 100.0, got {}",
584 self.critical_span_coverage
585 )));
586 }
587
588 if !(0.0..=100.0).contains(&self.required_attribute_coverage) {
589 return Err(CleanroomError::validation_error(format!(
590 "Required attribute coverage must be between 0.0 and 100.0, got {}",
591 self.required_attribute_coverage
592 )));
593 }
594
595 if !(0.0..=100.0).contains(&self.optional_attribute_coverage) {
596 return Err(CleanroomError::validation_error(format!(
597 "Optional attribute coverage must be between 0.0 and 100.0, got {}",
598 self.optional_attribute_coverage
599 )));
600 }
601
602 Ok(())
603 }
604}
605
606impl CollectorConfig {
607 pub fn validate(&self) -> Result<()> {
609 if self.use_existing && self.endpoint.is_none() {
611 return Err(CleanroomError::validation_error(
612 "Collector endpoint must be provided when use_existing = true",
613 ));
614 }
615
616 if let Some(ref endpoint) = self.endpoint {
618 if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
619 return Err(CleanroomError::validation_error(format!(
620 "Collector endpoint must start with http:// or https://, got: {}",
621 endpoint
622 )));
623 }
624 }
625
626 if self.health_check_timeout == 0 {
628 return Err(CleanroomError::validation_error(
629 "Health check timeout must be greater than 0 seconds",
630 ));
631 }
632
633 Ok(())
634 }
635}