clnrm_core/config/
otel.rs

1//! OpenTelemetry configuration types
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// OTEL configuration (v0.6.0 - v1.0)
7#[derive(Debug, Deserialize, Serialize, Clone)]
8pub struct OtelConfig {
9    /// OTEL exporter type (e.g., "stdout", "otlp")
10    pub exporter: String,
11    /// OTLP endpoint URL (e.g., "http://localhost:4318")
12    #[serde(default)]
13    pub endpoint: Option<String>,
14    /// Protocol to use (e.g., "http/protobuf", "grpc")
15    #[serde(default)]
16    pub protocol: Option<String>,
17    /// Sample ratio (0.0 to 1.0)
18    #[serde(default)]
19    pub sample_ratio: Option<f64>,
20    /// Resource attributes
21    #[serde(default)]
22    pub resources: Option<HashMap<String, String>>,
23    /// OTEL headers
24    #[serde(default)]
25    pub headers: Option<HashMap<String, String>>,
26    /// OTEL propagators
27    #[serde(default)]
28    pub propagators: Option<OtelPropagatorsConfig>,
29}
30
31/// Expectations configuration (v0.6.0)
32#[derive(Debug, Deserialize, Serialize, Clone, Default)]
33pub struct ExpectationsConfig {
34    /// Span expectations
35    #[serde(default)]
36    pub span: Vec<SpanExpectationConfig>,
37    /// Order expectations
38    #[serde(default)]
39    pub order: Option<OrderExpectationConfig>,
40    /// Status expectations
41    #[serde(default)]
42    pub status: Option<StatusExpectationConfig>,
43    /// Count expectations
44    #[serde(default)]
45    pub counts: Option<CountExpectationConfig>,
46    /// Window expectations
47    #[serde(default)]
48    pub window: Vec<WindowExpectationConfig>,
49    /// Graph expectations
50    #[serde(default)]
51    pub graph: Option<GraphExpectationConfig>,
52    /// Hermeticity expectations
53    #[serde(default)]
54    pub hermeticity: Option<HermeticityExpectationConfig>,
55}
56
57/// Span expectation configuration (v0.6.0 - v1.0)
58#[derive(Debug, Deserialize, Serialize, Clone)]
59pub struct SpanExpectationConfig {
60    /// Span name (can be glob pattern)
61    pub name: String,
62    /// Parent span name
63    #[serde(default)]
64    pub parent: Option<String>,
65    /// Span kind (e.g., "internal", "client", "server")
66    #[serde(default)]
67    pub kind: Option<String>,
68    /// Attribute expectations
69    #[serde(default)]
70    pub attrs: Option<SpanAttributesConfig>,
71    /// Event expectations
72    #[serde(default)]
73    pub events: Option<SpanEventsConfig>,
74    /// Duration expectations
75    #[serde(default)]
76    pub duration_ms: Option<DurationBoundConfig>,
77}
78
79/// Span events configuration
80#[derive(Debug, Deserialize, Serialize, Clone)]
81pub struct SpanEventsConfig {
82    /// Any of these events must be present
83    #[serde(default)]
84    pub any: Option<Vec<String>>,
85    /// All of these events must be present
86    #[serde(default)]
87    pub all: Option<Vec<String>>,
88}
89
90/// Duration bound configuration
91#[derive(Debug, Deserialize, Serialize, Clone)]
92pub struct DurationBoundConfig {
93    /// Minimum duration in milliseconds
94    #[serde(default)]
95    pub min: Option<f64>,
96    /// Maximum duration in milliseconds
97    #[serde(default)]
98    pub max: Option<f64>,
99}
100
101/// Span attributes configuration
102#[derive(Debug, Deserialize, Serialize, Clone)]
103pub struct SpanAttributesConfig {
104    /// All attributes must match
105    pub all: Option<HashMap<String, String>>,
106    /// Any attribute must match
107    pub any: Option<HashMap<String, String>>,
108}
109
110/// OpenTelemetry validation section in TOML
111#[derive(Debug, Deserialize, Serialize, Clone)]
112pub struct OtelValidationSection {
113    /// Enable OTEL validation
114    pub enabled: bool,
115    /// Validate spans
116    #[serde(default)]
117    pub validate_spans: Option<bool>,
118    /// Validate traces
119    #[serde(default)]
120    pub validate_traces: Option<bool>,
121    /// Validate exports
122    #[serde(default)]
123    pub validate_exports: Option<bool>,
124    /// Validate performance overhead
125    #[serde(default)]
126    pub validate_performance: Option<bool>,
127    /// Maximum allowed performance overhead in milliseconds
128    #[serde(default)]
129    pub max_overhead_ms: Option<f64>,
130    /// Expected spans configuration
131    #[serde(default)]
132    pub expected_spans: Option<Vec<ExpectedSpanConfig>>,
133    /// Expected traces configuration
134    #[serde(default)]
135    pub expected_traces: Option<Vec<ExpectedTraceConfig>>,
136    /// Graph topology expectations
137    #[serde(default)]
138    pub expect_graph: Option<GraphExpectationConfig>,
139    /// Count/cardinality expectations
140    #[serde(default)]
141    pub expect_counts: Option<CountExpectationConfig>,
142    /// Temporal window expectations
143    #[serde(default)]
144    pub expect_windows: Option<Vec<WindowExpectationConfig>>,
145    /// Hermeticity expectations
146    #[serde(default)]
147    pub expect_hermeticity: Option<HermeticityExpectationConfig>,
148    /// Temporal ordering expectations (v0.6.0)
149    #[serde(default)]
150    pub expect_order: Option<OrderExpectationConfig>,
151    /// Status code expectations (v0.6.0)
152    #[serde(default)]
153    pub expect_status: Option<StatusExpectationConfig>,
154}
155
156/// Expected span configuration from TOML
157#[derive(Debug, Deserialize, Serialize, Clone)]
158pub struct ExpectedSpanConfig {
159    /// Span name (operation name)
160    pub name: String,
161    /// Expected attributes
162    pub attributes: Option<HashMap<String, String>>,
163    /// Whether span is required
164    pub required: Option<bool>,
165    /// Minimum duration in milliseconds
166    pub min_duration_ms: Option<f64>,
167    /// Maximum duration in milliseconds
168    pub max_duration_ms: Option<f64>,
169}
170
171/// Expected trace configuration from TOML
172#[derive(Debug, Deserialize, Serialize, Clone)]
173pub struct ExpectedTraceConfig {
174    /// Trace ID (optional, for specific trace validation)
175    pub trace_id: Option<String>,
176    /// Expected span names in the trace
177    pub span_names: Vec<String>,
178    /// Whether all spans must be present
179    pub complete: Option<bool>,
180    /// Parent-child relationships (parent_name -> child_name)
181    pub parent_child: Option<Vec<(String, String)>>,
182}
183
184/// Graph topology expectation from TOML (v1.0 schema)
185#[derive(Debug, Deserialize, Serialize, Clone)]
186pub struct GraphExpectationConfig {
187    /// Edges that must be present in the span graph (parent, child)
188    /// Format: [["parent", "child"], ...]
189    #[serde(default)]
190    pub must_include: Option<Vec<Vec<String>>>,
191    /// Edges that must not exist in the span graph (forbidden crossings)
192    /// Format: [["a", "b"], ...]
193    #[serde(default)]
194    pub must_not_cross: Option<Vec<Vec<String>>>,
195    /// Whether the graph must be acyclic
196    #[serde(default)]
197    pub acyclic: Option<bool>,
198}
199
200/// Count bound configuration for cardinality expectations
201#[derive(Debug, Deserialize, Serialize, Clone)]
202pub struct CountBoundConfig {
203    /// Greater than or equal to (>=)
204    #[serde(default)]
205    pub gte: Option<usize>,
206    /// Less than or equal to (<=)
207    #[serde(default)]
208    pub lte: Option<usize>,
209    /// Equal to (==)
210    #[serde(default)]
211    pub eq: Option<usize>,
212}
213
214/// Count expectations from TOML for span cardinalities
215#[derive(Debug, Deserialize, Serialize, Clone)]
216pub struct CountExpectationConfig {
217    /// Total span count bounds
218    #[serde(default)]
219    pub spans_total: Option<CountBoundConfig>,
220    /// Total event count bounds
221    #[serde(default)]
222    pub events_total: Option<CountBoundConfig>,
223    /// Total error count bounds
224    #[serde(default)]
225    pub errors_total: Option<CountBoundConfig>,
226    /// Per-span-name count bounds
227    #[serde(default)]
228    pub by_name: Option<HashMap<String, CountBoundConfig>>,
229}
230
231/// Temporal window expectation from TOML
232#[derive(Debug, Deserialize, Serialize, Clone)]
233pub struct WindowExpectationConfig {
234    /// Outer span name that defines the temporal window
235    pub outer: String,
236    /// Span names that must be temporally contained within the outer span
237    pub contains: Vec<String>,
238}
239
240/// Hermeticity expectation from TOML (v1.0 schema)
241#[derive(Debug, Deserialize, Serialize, Clone)]
242pub struct HermeticityExpectationConfig {
243    /// Whether external service calls are forbidden
244    #[serde(default)]
245    pub no_external_services: Option<bool>,
246    /// Resource attributes that must match exactly
247    #[serde(default, alias = "resource_attrs_must_match")]
248    pub resource_attrs: Option<ResourceAttrsConfig>,
249    /// Span attribute keys that are forbidden (e.g., "net.peer.name")
250    #[serde(default, alias = "span_attrs_forbid_keys")]
251    pub span_attrs: Option<SpanAttrsConfig>,
252}
253
254/// Resource attributes configuration for hermeticity validation
255#[derive(Debug, Deserialize, Serialize, Clone)]
256pub struct ResourceAttrsConfig {
257    /// Attributes that must match exactly
258    #[serde(default)]
259    pub must_match: Option<HashMap<String, String>>,
260}
261
262/// Span attributes configuration for hermeticity validation
263#[derive(Debug, Deserialize, Serialize, Clone)]
264pub struct SpanAttrsConfig {
265    /// Attribute keys that are forbidden
266    #[serde(default)]
267    pub forbid_keys: Option<Vec<String>>,
268}
269
270/// Temporal ordering expectations (v1.0 schema)
271#[derive(Debug, Deserialize, Serialize, Clone)]
272pub struct OrderExpectationConfig {
273    /// Edges where first must temporally precede second
274    /// Format: [["first", "second"], ...]
275    #[serde(default)]
276    pub must_precede: Option<Vec<Vec<String>>>,
277    /// Edges where first must temporally follow second
278    /// Format: [["first", "second"], ...]
279    #[serde(default)]
280    pub must_follow: Option<Vec<Vec<String>>>,
281}
282
283/// Status code expectations (v0.6.0)
284#[derive(Debug, Deserialize, Serialize, Clone)]
285pub struct StatusExpectationConfig {
286    /// Expected status for all spans ("OK", "ERROR", "UNSET")
287    #[serde(default)]
288    pub all: Option<String>,
289    /// Expected status by span name pattern (supports globs)
290    #[serde(default)]
291    pub by_name: Option<HashMap<String, String>>,
292}
293
294/// OTEL headers configuration (v0.6.0)
295#[derive(Debug, Deserialize, Serialize, Clone, Default)]
296pub struct OtelHeadersConfig {
297    /// Custom OTLP headers (e.g., Authorization)
298    #[serde(flatten)]
299    pub headers: HashMap<String, String>,
300}
301
302/// OTEL propagators configuration (v0.6.0)
303#[derive(Debug, Deserialize, Serialize, Clone)]
304pub struct OtelPropagatorsConfig {
305    /// Propagators to use (e.g., ["tracecontext", "baggage"])
306    pub r#use: Vec<String>,
307}
308
309impl OtelConfig {
310    /// Validate the OTEL configuration
311    pub fn validate(&self) -> crate::error::Result<()> {
312        // Validate exporter type
313        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        // Validate sample ratio if present
324        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        // Validate protocol if present
333        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    /// Validate the graph expectation configuration
351    pub fn validate(&self) -> crate::error::Result<()> {
352        // Validate must_include edges
353        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        // Validate must_not_cross edges
369        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    /// Validate the status expectation configuration
390    pub fn validate(&self) -> crate::error::Result<()> {
391        // Validate 'all' status if present
392        if let Some(ref status) = self.all {
393            Self::validate_status_value(status)?;
394        }
395
396        // Validate 'by_name' statuses if present
397        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}