1use crate::redaction::FieldClass;
14
15pub const SPAN_SCHEMA_VERSION: u32 = 1;
17pub const SCHEMA_URL: &str = "https://opentelemetry.io/schemas/1.41.1";
19
20pub mod span {
22 pub const DECISION: &str = "rustev.decision";
23 pub const STEP: &str = "rustev.step";
24 pub const ATTEMPT: &str = "rustev.attempt";
25 pub const POLICY: &str = "rustev.policy";
26 pub const ALL: [&str; 4] = [DECISION, STEP, ATTEMPT, POLICY];
27}
28
29pub mod event {
31 pub const INPUT: &str = "rustev.input";
32 pub const NOTICE: &str = "rustev.notice";
33 pub const TRANSITION: &str = "rustev.transition";
34 pub const EXCLUDED: &str = "rustev.excluded";
35 pub const POSITION: &str = "rustev.position";
36 pub const ALL: [&str; 5] = [INPUT, NOTICE, TRANSITION, EXCLUDED, POSITION];
37}
38
39pub mod fixed {
41 pub const SCHEMA_VERSION: &str = "rustev.otel.schema_version";
42 pub const POLICY_ID: &str = "rustev.redaction.policy_id";
43 pub const KEY_ID: &str = "rustev.redaction.key_id";
44 pub const WITHHELD: &str = "rustev.redaction.withheld";
45}
46
47pub const PARAM_PREFIX: &str = "rustev.outcome.param.";
49pub const EXTRA_PREFIX: &str = "rustev.exchange.extra.";
51pub const EXTRA_WITHHELD: &str = "rustev.exchange.extra.*";
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum ClassRule {
58 Class(FieldClass),
60 Lineage,
62 Fixed,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct CatalogueField {
69 pub name: &'static str,
71 pub rule: ClassRule,
72 pub template: bool,
74 pub content: bool,
76}
77
78const fn f(name: &'static str, class: FieldClass) -> CatalogueField {
79 CatalogueField {
80 name,
81 rule: ClassRule::Class(class),
82 template: false,
83 content: false,
84 }
85}
86
87const fn fixed_field(name: &'static str) -> CatalogueField {
88 CatalogueField {
89 name,
90 rule: ClassRule::Fixed,
91 template: false,
92 content: false,
93 }
94}
95
96use FieldClass::{
97 ContentDigest as CD, Enum as E, FreeTextDetail as FT, Id as ID, ModelIdentity as MI,
98 Number as N, PlanMetadata as PM, ThirdParty as TP,
99};
100
101pub const CATALOGUE: &[CatalogueField] = &[
103 f("error.type", E),
104 f("gen_ai.operation.name", E),
105 f("gen_ai.provider.name", MI),
106 f("gen_ai.request.model", MI),
107 f("gen_ai.response.id", ID),
108 f("gen_ai.response.model", MI),
109 f("gen_ai.token.type", E),
110 f("gen_ai.usage.input_tokens", N),
111 f("gen_ai.usage.output_tokens", N),
112 f("rustev.acquisition.attempt_id", ID),
113 f("rustev.acquisition.cache", E),
114 f("rustev.acquisition.cache.age_ms", N),
115 f("rustev.acquisition.cancellation", E),
116 f("rustev.acquisition.constructed", E),
117 f("rustev.acquisition.cost.bound", E),
118 f("rustev.acquisition.cost.charge", N),
119 f("rustev.acquisition.cost.reserved", N),
120 f("rustev.acquisition.derivation", E),
121 f("rustev.acquisition.digest", CD),
122 f("rustev.acquisition.dispatched_ms", N),
123 f("rustev.acquisition.end", E),
124 f("rustev.acquisition.ended_ms", N),
125 f("rustev.acquisition.guard", PM),
126 f("rustev.acquisition.item.digest", CD),
127 f("rustev.acquisition.operation", PM),
128 f("rustev.acquisition.outcome", E),
129 f("rustev.acquisition.page", N),
130 f("rustev.acquisition.rank", N),
131 f("rustev.acquisition.relaxation", N),
132 f("rustev.acquisition.remote_state", E),
133 f("rustev.acquisition.request", CD),
134 f("rustev.acquisition.request_constructed", E),
135 f("rustev.acquisition.source", PM),
136 f("rustev.acquisition.step", PM),
137 f("rustev.acquisition.stopped_by", E),
138 f("rustev.acquisition.tier", PM),
139 f("rustev.acquisition.tier_index", N),
140 f("rustev.acquisition.truncated", N),
141 f("rustev.acquisition.unique_items_added", N),
142 f("rustev.attempt.artifact", PM),
143 f("rustev.attempt.backend_id", PM),
144 f("rustev.attempt.cancel", E),
145 f("rustev.attempt.charge_kind", E),
146 f("rustev.attempt.charge_units", N),
147 f("rustev.attempt.cost.bound", E),
148 f("rustev.attempt.cost.reserved", N),
149 f("rustev.attempt.end", E),
150 f("rustev.attempt.failure_class", E),
151 f("rustev.attempt.failure_detail", FT),
152 f("rustev.attempt.id", ID),
153 f("rustev.attempt.ordinal", N),
154 f("rustev.attempt.remote_state", E),
155 f("rustev.attempt.retry_index", N),
156 f("rustev.attempt.target", N),
157 f("rustev.batch.id", ID),
158 f("rustev.batch.member", N),
159 f("rustev.cost.bound_violations", N),
160 f("rustev.cost.estimated", N),
161 f("rustev.cost.final", E),
162 f("rustev.cost.liability", N),
163 f("rustev.cost.limit", N),
164 f("rustev.cost.mode", E),
165 f("rustev.cost.observed", N),
166 f("rustev.cost.within_guaranteed_cap", E),
167 f("rustev.deadline.remaining_ms", N),
168 f("rustev.decision.id", ID),
169 f("rustev.definition.id", PM),
170 f("rustev.derivation", E),
171 f("rustev.events_cut", N),
172 f("rustev.evidence.sink.result", E),
173 f("rustev.exchange.bytes_sent", E),
174 CatalogueField {
175 name: EXTRA_PREFIX,
176 rule: ClassRule::Class(TP),
177 template: true,
178 content: false,
179 },
180 f("rustev.exchange.late", E),
181 f("rustev.exchange.member_detail", FT),
182 f("rustev.exchange.reported_cost", N),
183 f("rustev.exchange.reported_currency", E),
184 f("rustev.exchange.request_digest", CD),
185 f("rustev.exchange.response_digest", CD),
186 f("rustev.exchange.retry_after", TP),
187 f("rustev.exchange.route", MI),
188 f("rustev.exchange.status", N),
189 f("rustev.exchange.truncated", E),
190 f("rustev.excluded.candidate", ID),
191 f("rustev.excluded.reasons", PM),
192 f("rustev.execution.policy_id", PM),
193 f("rustev.fallbacks.kind", E),
194 f("rustev.generation.attempt", ID),
195 f("rustev.generation.backend_id", PM),
196 f("rustev.generation.byte_length", N),
197 CatalogueField {
198 name: "rustev.generation.context",
199 rule: ClassRule::Lineage,
200 template: false,
201 content: true,
202 },
203 f("rustev.generation.context_digest", CD),
204 f("rustev.generation.counts", N),
205 f("rustev.generation.derivation", E),
206 f("rustev.generation.field", PM),
207 f("rustev.generation.helpers_version", PM),
208 f("rustev.generation.item", ID),
209 f("rustev.generation.max_items", N),
210 f("rustev.generation.notice", E),
211 f("rustev.generation.origin", E),
212 f("rustev.generation.original_decision_id", ID),
213 f("rustev.generation.projection", PM),
214 f("rustev.generation.reason", E),
215 f("rustev.generation.source", E),
216 f("rustev.generation.state", E),
217 f("rustev.generation.step", PM),
218 f("rustev.generation.template_id", CD),
219 f("rustev.generation.template_name", PM),
220 f("rustev.generation.text_origin", E),
221 f("rustev.generation.truncated", E),
222 CatalogueField {
223 name: "rustev.generation.value",
224 rule: ClassRule::Class(FieldClass::GeneratedText),
225 template: false,
226 content: true,
227 },
228 f("rustev.generation.value_digest", CD),
229 f("rustev.generation.worst_case_projection_bytes", N),
230 f("rustev.generation.worst_case_requests", N),
231 f("rustev.input.as_of_ms", N),
232 f("rustev.input.field", PM),
233 f("rustev.input.provenance", E),
234 f("rustev.input.source", ID),
235 f("rustev.input.status", E),
236 f("rustev.input.unresolved_reason", E),
237 f("rustev.lineage.inputs", PM),
238 f("rustev.lineage.steps", PM),
239 f("rustev.notice.kind", E),
240 f("rustev.notice.reason", PM),
241 f("rustev.notice.subject", PM),
242 fixed_field(fixed::SCHEMA_VERSION),
243 f("rustev.outcome", E),
244 f("rustev.outcome.action", PM),
245 f("rustev.outcome.escalate_reason", PM),
246 CatalogueField {
247 name: PARAM_PREFIX,
248 rule: ClassRule::Lineage,
249 template: true,
250 content: false,
251 },
252 f("rustev.outcome.unresolved_detail", FT),
253 f("rustev.outcome.unresolved_fields", PM),
254 f("rustev.outcome.unresolved_reason", E),
255 f("rustev.plan.id", PM),
256 f("rustev.policy.excluded_count", N),
257 f("rustev.policy.rules_fired", PM),
258 f("rustev.policy.truncated", N),
259 f("rustev.position.candidate", ID),
260 f("rustev.position.position", N),
261 CatalogueField {
262 name: "rustev.position.score",
263 rule: ClassRule::Lineage,
264 template: false,
265 content: false,
266 },
267 fixed_field(fixed::KEY_ID),
268 fixed_field(fixed::POLICY_ID),
269 fixed_field(fixed::WITHHELD),
270 f("rustev.screening.artifact", PM),
271 f("rustev.screening.backend_id", PM),
272 f("rustev.screening.cache", E),
273 f("rustev.screening.categories", E),
274 f("rustev.screening.consequence", E),
275 f("rustev.screening.descriptor", PM),
276 f("rustev.screening.entry", PM),
277 f("rustev.screening.field", PM),
278 f("rustev.screening.item", ID),
279 f("rustev.screening.items", ID),
280 f("rustev.screening.source_attempt", ID),
281 f("rustev.screening.step", PM),
282 f("rustev.screening.text_digest", CD),
283 f("rustev.screening.units.basis", E),
284 f("rustev.screening.units.bound_violation.computed", N),
285 f("rustev.screening.units.bound_violation.reported", N),
286 f("rustev.screening.units.charged", N),
287 f("rustev.screening.verdict", E),
288 f("rustev.served.disclosure", E),
289 f("rustev.snapshot.id", CD),
290 f("rustev.step.backend_id", PM),
291 f("rustev.step.derivation", E),
292 f("rustev.step.id", PM),
293 f("rustev.step.instance", ID),
294 f("rustev.step.kind", E),
295 f("rustev.step.operator", PM),
296 f("rustev.step.optimization.mechanism", E),
297 f("rustev.step.optimization.outcome", E),
298 f("rustev.step.result", E),
299 f("rustev.step.reused_from", ID),
300 f("rustev.step.status", E),
301 f("rustev.step.target", N),
302 f("rustev.step.unresolved_reason", E),
303 f("rustev.step.value_kind", E),
304 f("rustev.telemetry.lost.reason", E),
305 f("rustev.termination", E),
306 f("rustev.time_anchor", E),
307 f("rustev.timing", E),
308 f("rustev.timing.deadline_expired", E),
309 f("rustev.timing.deadline_ms", N),
310 f("rustev.timing.elapsed_ms", N),
311 f("rustev.timing.queued_ms", N),
312 f("rustev.transition.after", N),
313 f("rustev.transition.class", E),
314 f("rustev.transition.delay_ms", N),
315 f("rustev.transition.kind", E),
316 f("rustev.transition.reason", FT),
317 f("rustev.transition.to", N),
318 f("rustev.unresolved.scope", E),
319];
320
321fn is_segment(s: &str) -> bool {
325 !s.is_empty()
326 && s.len() <= 128
327 && !s
328 .chars()
329 .any(|c| c == '.' || c == '*' || c.is_whitespace() || c.is_control())
330}
331
332pub fn lookup(name: &str) -> Option<&'static CatalogueField> {
334 CATALOGUE.iter().find(|e| {
335 if e.template {
336 name.strip_prefix(e.name).is_some_and(is_segment)
337 } else {
338 e.name == name
339 }
340 })
341}
342
343pub fn event_field(event: &str, key: &str) -> String {
345 format!("{event}.{key}")
346}
347
348pub fn withheld_name(field: &str) -> &str {
351 if field.starts_with(EXTRA_PREFIX) {
352 EXTRA_WITHHELD
353 } else {
354 field
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 #[test]
363 fn the_catalogue_is_sorted_and_unique() {
364 for w in CATALOGUE.windows(2) {
365 assert!(w[0].name < w[1].name, "{} / {}", w[0].name, w[1].name);
366 }
367 }
368
369 #[test]
370 fn templates_match_one_segment() {
371 assert!(lookup("rustev.outcome.param.amount").is_some());
372 assert!(lookup("rustev.outcome.param.").is_none());
373 assert!(lookup("rustev.outcome.param.a.b").is_none());
374 assert_eq!(
375 lookup("rustev.exchange.extra.x_cache").map(|e| e.rule),
376 Some(ClassRule::Class(FieldClass::ThirdParty))
377 );
378 assert!(lookup("rustev.exchange.extra.*").is_none());
379 assert!(lookup("rustev.nope").is_none());
380 }
381
382 #[test]
383 fn fixed_fields_are_the_four_of_3_4_4() {
384 let fixed: Vec<_> = CATALOGUE
385 .iter()
386 .filter(|e| e.rule == ClassRule::Fixed)
387 .map(|e| e.name)
388 .collect();
389 assert_eq!(
390 fixed,
391 vec![
392 fixed::SCHEMA_VERSION,
393 fixed::KEY_ID,
394 fixed::POLICY_ID,
395 fixed::WITHHELD
396 ]
397 );
398 }
399
400 #[test]
401 fn generation_content_is_explicit() {
402 assert_eq!(
403 CATALOGUE
404 .iter()
405 .filter(|e| e.content)
406 .map(|e| e.name)
407 .collect::<Vec<_>>(),
408 vec!["rustev.generation.context", "rustev.generation.value"]
409 );
410 }
411
412 #[test]
413 fn extras_are_withheld_without_their_key() {
414 assert_eq!(
415 withheld_name("rustev.exchange.extra.secret"),
416 EXTRA_WITHHELD
417 );
418 assert_eq!(withheld_name("rustev.decision.id"), "rustev.decision.id");
419 }
420}