Skip to main content

rustev_contract/
trace_fields.rs

1//! The field catalogue of span schema `rustev.otel/1` (spec 028, 3.5 and
2//! 3.6): the closed table of exportable field names and each one's class
3//! rule. A name outside it is never exported, and a reader withholds it.
4//!
5//! Span attributes are named as in 3.5.4. An event attribute is catalogued
6//! as `<event name>.<attribute key>` (`rustev.input.source` for the `source`
7//! attribute of a `rustev.input` event), and a metric attribute that is no
8//! span attribute as `<instrument>.<attribute key>`. Two names are
9//! templates whose last segment is instance-specific:
10//! `rustev.outcome.param.<name>` (the plan's parameter name) and
11//! `rustev.exchange.extra.<key>` (a key the provider supplies).
12
13use crate::redaction::FieldClass;
14
15/// The span schema version of `rustev.otel/1` (3.2.4).
16pub const SPAN_SCHEMA_VERSION: u32 = 1;
17/// The pinned semantic-conventions schema URL (3.2.2).
18pub const SCHEMA_URL: &str = "https://opentelemetry.io/schemas/1.41.1";
19
20/// Span names (3.5.1). Constant; they never carry a value (3.4.4).
21pub 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
29/// Event names (3.5.5).
30pub 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
39/// The fixed fields (3.4.4): always `allow`, never overridable.
40pub 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
47/// The template prefix of outcome parameters (3.5.4).
48pub const PARAM_PREFIX: &str = "rustev.outcome.param.";
49/// The template prefix of provider extras (3.5.4).
50pub const EXTRA_PREFIX: &str = "rustev.exchange.extra.";
51/// How a dropped provider extra is listed in `rustev.redaction.withheld`:
52/// once per span, never by its key (3.4.6).
53pub const EXTRA_WITHHELD: &str = "rustev.exchange.extra.*";
54
55/// How a catalogue field's class is decided.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum ClassRule {
58    /// Always this class.
59    Class(FieldClass),
60    /// The lineage rule of 3.3.2, from the value and its judgment lineage.
61    Lineage,
62    /// A fixed field (3.4.4).
63    Fixed,
64}
65
66/// One catalogue entry.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct CatalogueField {
69    /// The exact name, or for a template the prefix ending in `.`.
70    pub name: &'static str,
71    pub rule: ClassRule,
72    /// A template: matches `name` followed by one non-empty segment.
73    pub template: bool,
74    /// A GenAI content field (3.5.7), requiring an explicit content policy.
75    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
101/// The catalogue (3.5.4, 3.5.5, 3.6.1), sorted by name.
102pub 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
321/// One instance-specific segment of a template name: non-empty, at most
322/// 128 bytes, with no `.`, `*`, whitespace or control character (so
323/// `rustev.outcome.param.a.b` and `rustev.exchange.extra.*` are no field).
324fn 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
332/// The catalogue entry `name` names, exactly or as a template instance.
333pub 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
343/// The catalogue name of attribute `key` of event `event`.
344pub fn event_field(event: &str, key: &str) -> String {
345    format!("{event}.{key}")
346}
347
348/// The name a dropped field is listed under in `rustev.redaction.withheld`
349/// (3.4.6): its own name, except provider extras.
350pub 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}