1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Deserialize, Serialize, Clone)]
8pub struct OtelConfig {
9 pub exporter: String,
11 #[serde(default)]
13 pub endpoint: Option<String>,
14 #[serde(default)]
16 pub protocol: Option<String>,
17 #[serde(default)]
19 pub sample_ratio: Option<f64>,
20 #[serde(default)]
22 pub resources: Option<HashMap<String, String>>,
23 #[serde(default)]
25 pub headers: Option<HashMap<String, String>>,
26 #[serde(default)]
28 pub propagators: Option<OtelPropagatorsConfig>,
29}
30
31#[derive(Debug, Deserialize, Serialize, Clone, Default)]
33pub struct ExpectationsConfig {
34 #[serde(default)]
36 pub span: Vec<SpanExpectationConfig>,
37 #[serde(default)]
39 pub order: Option<OrderExpectationConfig>,
40 #[serde(default)]
42 pub status: Option<StatusExpectationConfig>,
43 #[serde(default)]
45 pub counts: Option<CountExpectationConfig>,
46 #[serde(default)]
48 pub window: Vec<WindowExpectationConfig>,
49 #[serde(default)]
51 pub graph: Option<GraphExpectationConfig>,
52 #[serde(default)]
54 pub hermeticity: Option<HermeticityExpectationConfig>,
55}
56
57#[derive(Debug, Deserialize, Serialize, Clone)]
59pub struct SpanExpectationConfig {
60 pub name: String,
62 #[serde(default)]
64 pub parent: Option<String>,
65 #[serde(default)]
67 pub kind: Option<String>,
68 #[serde(default)]
70 pub attrs: Option<SpanAttributesConfig>,
71 #[serde(default)]
73 pub events: Option<SpanEventsConfig>,
74 #[serde(default)]
76 pub duration_ms: Option<DurationBoundConfig>,
77}
78
79#[derive(Debug, Deserialize, Serialize, Clone)]
81pub struct SpanEventsConfig {
82 #[serde(default)]
84 pub any: Option<Vec<String>>,
85 #[serde(default)]
87 pub all: Option<Vec<String>>,
88}
89
90#[derive(Debug, Deserialize, Serialize, Clone)]
92pub struct DurationBoundConfig {
93 #[serde(default)]
95 pub min: Option<f64>,
96 #[serde(default)]
98 pub max: Option<f64>,
99}
100
101#[derive(Debug, Deserialize, Serialize, Clone)]
103pub struct SpanAttributesConfig {
104 pub all: Option<HashMap<String, String>>,
106 pub any: Option<HashMap<String, String>>,
108}
109
110#[derive(Debug, Deserialize, Serialize, Clone)]
112pub struct OtelValidationSection {
113 pub enabled: bool,
115 #[serde(default)]
117 pub validate_spans: Option<bool>,
118 #[serde(default)]
120 pub validate_traces: Option<bool>,
121 #[serde(default)]
123 pub validate_exports: Option<bool>,
124 #[serde(default)]
126 pub validate_performance: Option<bool>,
127 #[serde(default)]
129 pub max_overhead_ms: Option<f64>,
130 #[serde(default)]
132 pub expected_spans: Option<Vec<ExpectedSpanConfig>>,
133 #[serde(default)]
135 pub expected_traces: Option<Vec<ExpectedTraceConfig>>,
136 #[serde(default)]
138 pub expect_graph: Option<GraphExpectationConfig>,
139 #[serde(default)]
141 pub expect_counts: Option<CountExpectationConfig>,
142 #[serde(default)]
144 pub expect_windows: Option<Vec<WindowExpectationConfig>>,
145 #[serde(default)]
147 pub expect_hermeticity: Option<HermeticityExpectationConfig>,
148 #[serde(default)]
150 pub expect_order: Option<OrderExpectationConfig>,
151 #[serde(default)]
153 pub expect_status: Option<StatusExpectationConfig>,
154}
155
156#[derive(Debug, Deserialize, Serialize, Clone)]
158pub struct ExpectedSpanConfig {
159 pub name: String,
161 pub attributes: Option<HashMap<String, String>>,
163 pub required: Option<bool>,
165 pub min_duration_ms: Option<f64>,
167 pub max_duration_ms: Option<f64>,
169}
170
171#[derive(Debug, Deserialize, Serialize, Clone)]
173pub struct ExpectedTraceConfig {
174 pub trace_id: Option<String>,
176 pub span_names: Vec<String>,
178 pub complete: Option<bool>,
180 pub parent_child: Option<Vec<(String, String)>>,
182}
183
184#[derive(Debug, Deserialize, Serialize, Clone)]
186pub struct GraphExpectationConfig {
187 #[serde(default)]
190 pub must_include: Option<Vec<Vec<String>>>,
191 #[serde(default)]
194 pub must_not_cross: Option<Vec<Vec<String>>>,
195 #[serde(default)]
197 pub acyclic: Option<bool>,
198}
199
200#[derive(Debug, Deserialize, Serialize, Clone)]
202pub struct CountBoundConfig {
203 #[serde(default)]
205 pub gte: Option<usize>,
206 #[serde(default)]
208 pub lte: Option<usize>,
209 #[serde(default)]
211 pub eq: Option<usize>,
212}
213
214#[derive(Debug, Deserialize, Serialize, Clone)]
216pub struct CountExpectationConfig {
217 #[serde(default)]
219 pub spans_total: Option<CountBoundConfig>,
220 #[serde(default)]
222 pub events_total: Option<CountBoundConfig>,
223 #[serde(default)]
225 pub errors_total: Option<CountBoundConfig>,
226 #[serde(default)]
228 pub by_name: Option<HashMap<String, CountBoundConfig>>,
229}
230
231#[derive(Debug, Deserialize, Serialize, Clone)]
233pub struct WindowExpectationConfig {
234 pub outer: String,
236 pub contains: Vec<String>,
238}
239
240#[derive(Debug, Deserialize, Serialize, Clone)]
242pub struct HermeticityExpectationConfig {
243 #[serde(default)]
245 pub no_external_services: Option<bool>,
246 #[serde(default, alias = "resource_attrs_must_match")]
248 pub resource_attrs: Option<ResourceAttrsConfig>,
249 #[serde(default, alias = "span_attrs_forbid_keys")]
251 pub span_attrs: Option<SpanAttrsConfig>,
252}
253
254#[derive(Debug, Deserialize, Serialize, Clone)]
256pub struct ResourceAttrsConfig {
257 #[serde(default)]
259 pub must_match: Option<HashMap<String, String>>,
260}
261
262#[derive(Debug, Deserialize, Serialize, Clone)]
264pub struct SpanAttrsConfig {
265 #[serde(default)]
267 pub forbid_keys: Option<Vec<String>>,
268}
269
270#[derive(Debug, Deserialize, Serialize, Clone)]
272pub struct OrderExpectationConfig {
273 #[serde(default)]
276 pub must_precede: Option<Vec<Vec<String>>>,
277 #[serde(default)]
280 pub must_follow: Option<Vec<Vec<String>>>,
281}
282
283#[derive(Debug, Deserialize, Serialize, Clone)]
285pub struct StatusExpectationConfig {
286 #[serde(default)]
288 pub all: Option<String>,
289 #[serde(default)]
291 pub by_name: Option<HashMap<String, String>>,
292}
293
294#[derive(Debug, Deserialize, Serialize, Clone, Default)]
296pub struct OtelHeadersConfig {
297 #[serde(flatten)]
299 pub headers: HashMap<String, String>,
300}
301
302#[derive(Debug, Deserialize, Serialize, Clone)]
304pub struct OtelPropagatorsConfig {
305 pub r#use: Vec<String>,
307}
308
309impl OtelConfig {
310 pub fn validate(&self) -> crate::error::Result<()> {
312 match self.exporter.to_lowercase().as_str() {
314 "stdout" | "otlp" | "jaeger" | "zipkin" => {}
315 _ => {
316 return Err(crate::error::CleanroomError::validation_error(format!(
317 "Invalid exporter type '{}'. Must be one of: stdout, otlp, jaeger, zipkin",
318 self.exporter
319 )))
320 }
321 }
322
323 if let Some(ratio) = self.sample_ratio {
325 if !(0.0..=1.0).contains(&ratio) {
326 return Err(crate::error::CleanroomError::validation_error(
327 "Sample ratio must be between 0.0 and 1.0",
328 ));
329 }
330 }
331
332 if let Some(ref protocol) = self.protocol {
334 match protocol.to_lowercase().as_str() {
335 "http/protobuf" | "grpc" | "http/json" => {}
336 _ => {
337 return Err(crate::error::CleanroomError::validation_error(format!(
338 "Invalid protocol '{}'. Must be one of: http/protobuf, grpc, http/json",
339 protocol
340 )))
341 }
342 }
343 }
344
345 Ok(())
346 }
347}
348
349impl GraphExpectationConfig {
350 pub fn validate(&self) -> crate::error::Result<()> {
352 if let Some(ref edges) = self.must_include {
354 for edge in edges {
355 if edge.len() != 2 {
356 return Err(crate::error::CleanroomError::validation_error(
357 "Graph edges must have exactly 2 elements [parent, child]",
358 ));
359 }
360 if edge[0].is_empty() || edge[1].is_empty() {
361 return Err(crate::error::CleanroomError::validation_error(
362 "Graph edge names cannot be empty",
363 ));
364 }
365 }
366 }
367
368 if let Some(ref edges) = self.must_not_cross {
370 for edge in edges {
371 if edge.len() != 2 {
372 return Err(crate::error::CleanroomError::validation_error(
373 "Graph edges must have exactly 2 elements [source, target]",
374 ));
375 }
376 if edge[0].is_empty() || edge[1].is_empty() {
377 return Err(crate::error::CleanroomError::validation_error(
378 "Graph edge names cannot be empty",
379 ));
380 }
381 }
382 }
383
384 Ok(())
385 }
386}
387
388impl StatusExpectationConfig {
389 pub fn validate(&self) -> crate::error::Result<()> {
391 if let Some(ref status) = self.all {
393 Self::validate_status_value(status)?;
394 }
395
396 if let Some(ref by_name) = self.by_name {
398 for (span_name, status) in by_name {
399 if span_name.is_empty() {
400 return Err(crate::error::CleanroomError::validation_error(
401 "Span name in status expectations cannot be empty",
402 ));
403 }
404 Self::validate_status_value(status)?;
405 }
406 }
407
408 Ok(())
409 }
410
411 fn validate_status_value(status: &str) -> crate::error::Result<()> {
412 match status.to_uppercase().as_str() {
413 "OK" | "ERROR" | "UNSET" => Ok(()),
414 _ => Err(crate::error::CleanroomError::validation_error(format!(
415 "Invalid status value '{}'. Must be one of: OK, ERROR, UNSET",
416 status
417 ))),
418 }
419 }
420}