1use serde::{Deserialize, Serialize};
2
3use crate::engine_config::ConfigError;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "lowercase")]
7pub enum TelemetryCarrier {
8 Durable,
9 Ephemeral,
10}
11
12impl TelemetryCarrier {
13 pub const fn as_str(self) -> &'static str {
14 match self {
15 Self::Durable => "durable",
16 Self::Ephemeral => "ephemeral",
17 }
18 }
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23pub enum TelemetryFailurePosture {
24 Stop,
25 Gap,
26}
27
28impl TelemetryFailurePosture {
29 pub const fn as_str(self) -> &'static str {
30 match self {
31 Self::Stop => "stop",
32 Self::Gap => "gap",
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
38pub struct TelemetryPolicy {
39 pub classified: bool,
40 pub carrier: TelemetryCarrier,
41 pub failure_posture: TelemetryFailurePosture,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45pub struct TelemetryChannelConfig {
46 pub kinds: Vec<String>,
47 pub carrier: TelemetryCarrier,
48 pub failure_posture: TelemetryFailurePosture,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(try_from = "RawTelemetryConfig")]
53pub struct TelemetryConfig {
54 pub stream: String,
55 pub default_carrier: Option<TelemetryCarrier>,
56 pub channels: Vec<TelemetryChannelConfig>,
57}
58
59impl Default for TelemetryConfig {
60 fn default() -> Self {
61 Self {
62 stream: default_stream(),
63 default_carrier: None,
64 channels: Vec::new(),
65 }
66 }
67}
68
69impl TelemetryConfig {
70 pub fn validate(&self) -> Result<(), ConfigError> {
71 if self.stream.len() > 512 || self.stream.contains('\0') {
72 return Err(invalid(
73 "telemetry.stream",
74 "must be at most 512 UTF-8 bytes and contain no U+0000",
75 ));
76 }
77 for (index, channel) in self.channels.iter().enumerate() {
78 let entry = format!("telemetry.channels[{index}]");
79 if channel.kinds.is_empty() {
80 return Err(invalid(&entry, "kinds must not be empty"));
81 }
82 for kind in &channel.kinds {
83 let suffix = kind.strip_prefix("*.");
84 if kind.is_empty()
85 || kind.chars().any(|c| c.is_whitespace() || c.is_control())
86 || (kind.contains('*')
87 && !suffix.is_some_and(|s| !s.is_empty() && !s.contains('*')))
88 {
89 return Err(invalid(
90 &entry,
91 format!(
92 "invalid kind pattern {kind:?}; expected a nonempty exact name or *.suffix without whitespace or control characters"
93 ),
94 ));
95 }
96 for (previous_index, previous) in self.channels[..index].iter().enumerate() {
97 for previous_kind in &previous.kinds {
98 if patterns_overlap(kind, previous_kind) {
99 return Err(invalid(
100 &entry,
101 format!(
102 "kind pattern {kind:?} overlaps {previous_kind:?} in telemetry.channels[{previous_index}]"
103 ),
104 ));
105 }
106 }
107 }
108 }
109 }
110 Ok(())
111 }
112
113 pub fn validate_activation(&self) -> Result<(), ConfigError> {
114 self.declared_default()?;
115 self.validate()
116 }
117
118 fn declared_default(&self) -> Result<TelemetryCarrier, ConfigError> {
119 self.default_carrier.ok_or_else(|| {
120 invalid(
121 "telemetry.default_carrier",
122 "must be explicitly declared when the telemetry pack is loaded",
123 )
124 })
125 }
126
127 pub fn policy_for_kind(&self, kind: &str) -> Result<TelemetryPolicy, ConfigError> {
128 let default_carrier = self.declared_default()?;
129 for channel in &self.channels {
130 if channel
131 .kinds
132 .iter()
133 .any(|pattern| pattern_matches(pattern, kind))
134 {
135 return Ok(TelemetryPolicy {
136 classified: true,
137 carrier: channel.carrier,
138 failure_posture: channel.failure_posture,
139 });
140 }
141 }
142 Ok(TelemetryPolicy {
143 classified: false,
144 carrier: default_carrier,
145 failure_posture: match default_carrier {
146 TelemetryCarrier::Durable => TelemetryFailurePosture::Stop,
147 TelemetryCarrier::Ephemeral => TelemetryFailurePosture::Gap,
148 },
149 })
150 }
151}
152
153fn pattern_matches(pattern: &str, kind: &str) -> bool {
154 match pattern.strip_prefix('*') {
155 Some(suffix) => kind.ends_with(suffix),
156 None => pattern == kind,
157 }
158}
159
160fn patterns_overlap(first: &str, second: &str) -> bool {
161 match (first.strip_prefix('*'), second.strip_prefix('*')) {
162 (Some(first), Some(second)) => first.ends_with(second) || second.ends_with(first),
163 (Some(_), None) => pattern_matches(first, second),
164 (None, Some(_)) => pattern_matches(second, first),
165 (None, None) => first == second,
166 }
167}
168
169fn invalid(entry: impl Into<String>, reason: impl Into<String>) -> ConfigError {
170 ConfigError::InvalidTelemetryConfig {
171 entry: entry.into(),
172 reason: reason.into(),
173 }
174}
175
176fn default_stream() -> String {
177 "telemetry".to_string()
178}
179
180#[derive(Deserialize)]
182#[serde(deny_unknown_fields)]
183struct RawTelemetryConfig {
184 #[serde(default = "default_stream")]
185 stream: String,
186 #[serde(default)]
187 default_carrier: Option<String>,
188 #[serde(default)]
189 channels: Vec<RawTelemetryChannelConfig>,
190}
191
192#[derive(Deserialize)]
193#[serde(deny_unknown_fields)]
194struct RawTelemetryChannelConfig {
195 kinds: Vec<String>,
196 carrier: String,
197 failure_posture: String,
198}
199
200fn parse_carrier(value: &str, entry: &str) -> Result<TelemetryCarrier, ConfigError> {
201 match value {
202 "durable" => Ok(TelemetryCarrier::Durable),
203 "ephemeral" => Ok(TelemetryCarrier::Ephemeral),
204 _ => Err(invalid(
205 entry,
206 format!("unknown carrier {value:?}; expected durable or ephemeral"),
207 )),
208 }
209}
210
211impl TryFrom<RawTelemetryConfig> for TelemetryConfig {
212 type Error = ConfigError;
213
214 fn try_from(raw: RawTelemetryConfig) -> Result<Self, Self::Error> {
215 let default_carrier = raw
216 .default_carrier
217 .as_deref()
218 .map(|value| parse_carrier(value, "telemetry.default_carrier"))
219 .transpose()?;
220 let mut channels = Vec::with_capacity(raw.channels.len());
221 for (index, channel) in raw.channels.into_iter().enumerate() {
222 let entry = format!("telemetry.channels[{index}]");
223 let carrier = parse_carrier(&channel.carrier, &entry)?;
224 let failure_posture = match channel.failure_posture.as_str() {
225 "stop" => TelemetryFailurePosture::Stop,
226 "gap" => TelemetryFailurePosture::Gap,
227 value => {
228 return Err(invalid(
229 &entry,
230 format!("unknown failure_posture {value:?}; expected stop or gap"),
231 ));
232 }
233 };
234 channels.push(TelemetryChannelConfig {
235 kinds: channel.kinds,
236 carrier,
237 failure_posture,
238 });
239 }
240 let config = Self {
241 stream: raw.stream,
242 default_carrier,
243 channels,
244 };
245 config.validate()?;
246 Ok(config)
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn missing_default_is_structurally_valid_but_cannot_activate() {
256 for text in ["", "stream = \"events\"\n"] {
257 let missing: TelemetryConfig = toml::from_str(text).unwrap();
258 assert_eq!(missing.default_carrier, None);
259 missing.validate().unwrap();
260 assert!(missing
261 .validate_activation()
262 .unwrap_err()
263 .to_string()
264 .contains("telemetry.default_carrier"));
265 assert!(missing.policy_for_kind("unclassified").is_err());
266 assert_eq!(
267 serde_json::from_value::<TelemetryConfig>(serde_json::to_value(&missing).unwrap())
268 .unwrap(),
269 missing
270 );
271 }
272 for carrier in ["durable", "ephemeral"] {
273 let declared: TelemetryConfig =
274 toml::from_str(&format!("default_carrier = {carrier:?}")).unwrap();
275 declared.validate_activation().unwrap();
276 assert!(!declared.policy_for_kind("unclassified").unwrap().classified);
277 }
278 }
279
280 #[test]
281 fn classification_tracks_matching_even_when_the_carrier_equals_the_default() {
282 for carrier in [TelemetryCarrier::Durable, TelemetryCarrier::Ephemeral] {
283 let config = TelemetryConfig {
284 default_carrier: Some(carrier),
285 channels: vec![TelemetryChannelConfig {
286 kinds: vec!["*.heartbeat".into(), "run.started".into()],
287 carrier,
288 failure_posture: TelemetryFailurePosture::Stop,
289 }],
290 ..TelemetryConfig::default()
291 };
292 let round_trip: TelemetryConfig =
293 serde_json::from_value(serde_json::to_value(&config).unwrap()).unwrap();
294 for kind in ["run.heartbeat", "run.started", "unclassified"] {
295 let policy = config.policy_for_kind(kind).unwrap();
296 assert_eq!(policy.carrier, carrier);
297 assert_eq!(policy.classified, kind != "unclassified");
298 assert_eq!(round_trip.policy_for_kind(kind).unwrap(), policy);
299 }
300 }
301 }
302
303 #[test]
304 fn directly_constructed_config_rejects_ambiguous_channels() {
305 let mut config = TelemetryConfig {
306 channels: vec![
307 TelemetryChannelConfig {
308 kinds: vec!["*.heartbeat".to_string()],
309 carrier: TelemetryCarrier::Ephemeral,
310 failure_posture: TelemetryFailurePosture::Gap,
311 },
312 TelemetryChannelConfig {
313 kinds: vec!["run.heartbeat".to_string()],
314 carrier: TelemetryCarrier::Durable,
315 failure_posture: TelemetryFailurePosture::Stop,
316 },
317 ],
318 ..TelemetryConfig::default()
319 };
320 let error = config.validate().expect_err("overlap must refuse");
321 assert!(matches!(
322 error,
323 ConfigError::InvalidTelemetryConfig { entry, .. }
324 if entry == "telemetry.channels[1]"
325 ));
326
327 config.channels[1].kinds.clear();
328 let error = config.validate().expect_err("empty channel must refuse");
329 assert!(
330 error.to_string().contains("telemetry.channels[1]"),
331 "{error}"
332 );
333 }
334
335 #[test]
336 fn disjoint_suffixes_and_same_channel_patterns_remain_valid() {
337 let config: TelemetryConfig = toml::from_str(
338 r#"
339default_carrier = "durable"
340[[channels]]
341kinds = ["*.heartbeat", "run.heartbeat"]
342carrier = "ephemeral"
343failure_posture = "gap"
344[[channels]]
345kinds = ["*.notheartbeat", "heartbeat"]
346carrier = "durable"
347failure_posture = "stop"
348"#,
349 )
350 .expect("distinct suffixes have no common kind");
351 assert_eq!(
352 config.policy_for_kind("run.heartbeat").unwrap().carrier,
353 TelemetryCarrier::Ephemeral
354 );
355 assert_eq!(
356 config.policy_for_kind("run.notheartbeat").unwrap().carrier,
357 TelemetryCarrier::Durable
358 );
359 assert_eq!(
360 config.policy_for_kind("heartbeat").unwrap().carrier,
361 TelemetryCarrier::Durable
362 );
363 }
364
365 #[test]
366 fn config_serializes_effective_policy_and_round_trips() {
367 let config = TelemetryConfig {
368 default_carrier: Some(TelemetryCarrier::Ephemeral),
369 channels: vec![TelemetryChannelConfig {
370 kinds: vec!["run.started".to_string()],
371 carrier: TelemetryCarrier::Durable,
372 failure_posture: TelemetryFailurePosture::Stop,
373 }],
374 ..TelemetryConfig::default()
375 };
376 let value = serde_json::to_value(&config).unwrap();
377 assert_eq!(value["stream"], "telemetry");
378 assert_eq!(value["default_carrier"], "ephemeral");
379 assert_eq!(value["channels"][0]["carrier"], "durable");
380 assert_eq!(value["channels"][0]["failure_posture"], "stop");
381 assert_eq!(
382 serde_json::from_value::<TelemetryConfig>(value).unwrap(),
383 config
384 );
385 }
386}