1use std::collections::{BTreeMap, BTreeSet};
11use std::fmt;
12use std::sync::Arc;
13
14use rustev_contract::definition::{Operation, ProvenanceClass, StepBody};
15use rustev_contract::evidence::{EvidenceRecord, InputStatus, StepEvidence, StepStatus};
16use rustev_contract::judgment::{OutValue, Outcome, Unresolved};
17use rustev_contract::plan::{Plan, PlanStepDetail};
18use rustev_contract::redaction::{Applied, FieldClass, Redactor, lineage_class, truncate_value};
19use rustev_contract::remote::{Disclosure, ExchangeRecord, MemberOutcome};
20use rustev_contract::run::{
21 AttemptEnd, AttemptRecord, Cancellation, Charge, CoreEvidence, CostBound, RecordedExecution,
22 RequestRecord, RequestResult, RunRecord, Transition,
23};
24use rustev_contract::schema;
25use rustev_contract::trace::{
26 Mark, SpanKind, SpanStatus, TRACE, TraceAttr, TraceEvent, TraceLine, TraceLineError,
27 TraceResource, TraceScope, TraceValue,
28};
29use rustev_contract::trace_fields::{
30 PARAM_PREFIX, SCHEMA_URL, SPAN_SCHEMA_VERSION, event, fixed, span, withheld_name,
31};
32use serde::Serialize;
33
34use crate::ids::{IdSource, hex};
35
36pub const MAX_EVENTS_PER_SPAN: usize = 128;
38pub const SCOPE_NAME: &str = "rustev-otel";
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum AnchorKind {
44 Begin,
46 Delivery,
48}
49
50impl AnchorKind {
51 fn name(self) -> &'static str {
52 match self {
53 AnchorKind::Begin => "begin",
54 AnchorKind::Delivery => "delivery",
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct Anchor {
63 pub unix_nanos: u64,
64 pub kind: AnchorKind,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct RootIds {
70 pub trace_id: [u8; 16],
71 pub span_id: [u8; 8],
72 pub parent_span_id: Option<[u8; 8]>,
73}
74
75#[derive(Debug, Clone, Default, PartialEq, Eq)]
78pub struct UsageKeys {
79 pub input: Option<String>,
80 pub output: Option<String>,
81}
82
83#[derive(Debug, Clone, Default)]
87pub struct Bindings {
88 pub providers: BTreeMap<String, String>,
90 pub usage_keys: BTreeMap<String, UsageKeys>,
92}
93
94pub struct ProjectionInput<'a> {
96 pub record: &'a RunRecord,
97 pub plan: Option<&'a Plan>,
99 pub exchanges: &'a BTreeMap<String, Arc<ExchangeRecord>>,
101 pub bindings: &'a Bindings,
102 pub anchor: Anchor,
103 pub root: RootIds,
104 pub value_bound: usize,
106}
107
108#[derive(Debug, Clone, PartialEq)]
110pub struct Projected {
111 pub lines: Vec<TraceLine>,
113 pub attributes_cut: u64,
115 pub events_cut: u64,
117 pub facts: Facts,
120}
121
122#[derive(Debug, Clone, Default, PartialEq)]
125pub struct Facts {
126 pub outcome: String,
127 pub termination: String,
128 pub plan_id: String,
129 pub outcome_unresolved_reason: Option<String>,
130 pub step_unresolved_reasons: Vec<String>,
132 pub fallbacks: u64,
134 pub elapsed_ms: u64,
135 pub final_cost: String,
136 pub cost_units: u64,
138 pub attempts: Vec<AttemptFacts>,
139 pub screening: Vec<(String, String)>,
141 pub screening_units: Option<u64>,
143}
144
145#[derive(Debug, Clone, PartialEq)]
147pub struct AttemptFacts {
148 pub line: usize,
150 pub step_kind: String,
151 pub end: String,
152 pub backend_id: String,
153 pub duration_ms: u64,
154 pub genai: Option<GenAiFacts>,
156}
157
158#[derive(Debug, Clone, PartialEq)]
160pub struct GenAiFacts {
161 pub operation: Option<String>,
162 pub provider: String,
163 pub request_model: String,
164 pub input_tokens: Option<u64>,
165 pub output_tokens: Option<u64>,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct ProjectionUnknown {
171 pub what: String,
172}
173
174impl fmt::Display for ProjectionUnknown {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 write!(f, "projection_unknown: {}", self.what)
177 }
178}
179
180impl std::error::Error for ProjectionUnknown {}
181
182fn unknown(what: impl Into<String>) -> ProjectionUnknown {
183 ProjectionUnknown { what: what.into() }
184}
185
186fn name_of<T: Serialize>(v: &T) -> String {
188 match serde_json::to_value(v) {
189 Ok(serde_json::Value::String(s)) => s,
190 _ => "unknown".to_string(),
191 }
192}
193
194fn num(n: u64) -> TraceValue {
197 match i64::try_from(n) {
198 Ok(i) => TraceValue::Int(i),
199 Err(_) => TraceValue::Str(n.to_string()),
200 }
201}
202
203fn strs(items: &[String]) -> TraceValue {
204 TraceValue::StrArray(items.to_vec())
205}
206
207fn ms_to_nanos(ms: u64) -> u64 {
208 ms.saturating_mul(1_000_000)
209}
210
211struct SpanBuilder<'r> {
213 redactor: &'r Redactor,
214 bound: usize,
215 attributes: BTreeMap<String, TraceAttr>,
216 events: Vec<TraceEvent>,
217 withheld: BTreeSet<String>,
218 attributes_cut: u64,
219 events_cut: u64,
220}
221
222impl<'r> SpanBuilder<'r> {
223 fn new(redactor: &'r Redactor, bound: usize) -> Self {
224 SpanBuilder {
225 redactor,
226 bound,
227 attributes: BTreeMap::new(),
228 events: Vec::new(),
229 withheld: BTreeSet::new(),
230 attributes_cut: 0,
231 events_cut: 0,
232 }
233 }
234
235 fn redact(&mut self, field: &str, class: FieldClass, value: &TraceValue) -> Option<TraceAttr> {
237 match self.redactor.apply(field, class, value) {
238 Applied::Dropped => {
239 self.withheld.insert(withheld_name(field).to_string());
240 None
241 }
242 Applied::Kept(mut a) => {
243 let (v, cut) = truncate_value(&a.v, self.bound);
244 if cut {
245 self.attributes_cut += 1;
246 a.v = v;
247 if a.r == Mark::Allow {
248 a.r = Mark::Truncated;
249 }
250 }
251 Some(a)
252 }
253 }
254 }
255
256 fn put(&mut self, field: &str, class: FieldClass, value: TraceValue) {
257 if let Some(a) = self.redact(field, class, &value) {
258 self.attributes.insert(field.to_string(), a);
259 }
260 }
261
262 fn put_fixed(&mut self, field: &str, value: TraceValue) {
264 self.attributes.insert(
265 field.to_string(),
266 TraceAttr {
267 v: value,
268 r: Mark::Allow,
269 },
270 );
271 }
272
273 fn event(&mut self, name: &str, time: u64, attrs: Vec<(&str, FieldClass, TraceValue)>) {
275 if self.events.len() >= MAX_EVENTS_PER_SPAN {
276 self.events_cut += 1;
277 return;
278 }
279 let mut out = BTreeMap::new();
280 for (key, class, value) in attrs {
281 let field = format!("{name}.{key}");
282 if let Some(a) = self.redact(&field, class, &value) {
283 out.insert(key.to_string(), a);
284 }
285 }
286 self.events.push(TraceEvent {
287 name: name.to_string(),
288 time_unix_nanos: time,
289 attributes: out,
290 });
291 }
292}
293
294struct Draft<'r> {
296 name: &'static str,
297 span_id: [u8; 8],
298 parent: Option<[u8; 8]>,
299 kind: SpanKind,
300 start: u64,
301 end: u64,
302 status: SpanStatus,
303 b: SpanBuilder<'r>,
304}
305
306struct Ctx<'a> {
308 evidence: Option<&'a EvidenceRecord>,
309 plan: Option<&'a Plan>,
310 anchor_kind: AnchorKind,
311}
312
313impl Ctx<'_> {
314 fn step_body(&self, step: &str) -> Option<&StepBody> {
315 self.plan?
316 .definition
317 .steps
318 .iter()
319 .find(|s| s.id == step)
320 .map(|s| &s.body)
321 }
322
323 fn step_kind(&self, step: &str) -> Result<String, ProjectionUnknown> {
324 if self.plan.is_none() {
325 return Ok("unknown".into());
326 }
327 match self.step_body(step) {
328 Some(StepBody::Acquire(_)) => Err(unknown(format!("step kind acquire ({step})"))),
329 Some(StepBody::Widen(_)) => Err(unknown(format!("step kind widen ({step})"))),
330 Some(StepBody::Rank(_)) => Err(unknown(format!("step kind rank ({step})"))),
331 Some(StepBody::Select(_)) => Err(unknown(format!("step kind select ({step})"))),
332 Some(StepBody::Generate(_)) => Err(unknown(format!("step kind generate ({step})"))),
333 Some(StepBody::Extract(_)) => Err(unknown(format!("step kind extract ({step})"))),
334 Some(StepBody::AcceptModelDerived(_)) => {
335 Err(unknown(format!("step kind accept_model_derived ({step})")))
336 }
337 Some(StepBody::Exact(_)) => Ok("exact".into()),
338 Some(StepBody::Semantic(s)) => Ok(operation_name(s.operation).into()),
339 Some(StepBody::Corroborate(_)) => {
340 Err(unknown(format!("step kind corroborate ({step})")))
341 }
342 None => Err(unknown(format!(
343 "step {step} is not in the registered plan"
344 ))),
345 }
346 }
347
348 fn operation(&self, step: &str) -> Option<&'static str> {
349 match self.step_body(step)? {
350 StepBody::Semantic(s) => Some(operation_name(s.operation)),
351 _ => None,
352 }
353 }
354
355 fn lineage_inputs(&self) -> Vec<ProvenanceClass> {
358 let Some(ev) = self.evidence else {
359 return vec![];
360 };
361 ev.judgment
362 .lineage
363 .inputs
364 .iter()
365 .map(|field| {
366 ev.inputs
367 .iter()
368 .find(|i| &i.field == field)
369 .and_then(|i| match &i.status {
370 InputStatus::Resolved { provenance, .. } => Some(*provenance),
371 InputStatus::Unresolved(_) => None,
372 })
373 .unwrap_or(ProvenanceClass::AttributedClaim)
374 })
375 .collect()
376 }
377}
378
379fn operation_name(op: Operation) -> &'static str {
380 match op {
381 Operation::Classify => "classify",
382 Operation::Proposition => "proposition",
383 Operation::Rubric => "rubric",
384 Operation::Rank => "rank",
385 Operation::Generate => "generate",
386 Operation::Extract => "extract",
387 }
388}
389
390fn unresolved_parts(u: &Unresolved) -> (&'static str, Option<&[String]>, Option<&str>) {
392 let fields = match u {
393 Unresolved::MissingEvidence { fields }
394 | Unresolved::StaleEvidence { fields }
395 | Unresolved::InvalidInput { fields, .. }
396 | Unresolved::InsufficientGrade { fields } => Some(fields.as_slice()),
397 Unresolved::Conflict { facts } => Some(facts.as_slice()),
398 _ => None,
399 };
400 let detail = match u {
401 Unresolved::InvalidInput { detail, .. }
402 | Unresolved::BackendUnavailable { detail }
403 | Unresolved::InvalidBackendOutput { detail } => Some(detail.as_str()),
404 _ => None,
405 };
406 (u.kind().name(), fields, detail)
407}
408
409fn param_value(v: &OutValue) -> (TraceValue, bool) {
413 match v {
414 OutValue::Bool(b) => (TraceValue::Bool(*b), true),
415 OutValue::Enum(s) => (TraceValue::Str(s.clone()), true),
416 OutValue::Integer(i) => (TraceValue::Int(*i), false),
417 OutValue::Text(s) => (TraceValue::Str(s.clone()), false),
418 OutValue::Decimal(d) => (TraceValue::Str(d.to_string()), false),
419 OutValue::Timestamp(t) => (TraceValue::Int(t.as_ms()), false),
420 OutValue::Duration(d) => (num(d.0), false),
421 other => (
422 TraceValue::Str(
423 rustev_contract::canonical::record_canonical_bytes(other)
424 .ok()
425 .and_then(|b| String::from_utf8(b).ok())
426 .unwrap_or_else(|| "null".into()),
427 ),
428 false,
429 ),
430 }
431}
432
433pub fn project(
437 input: &ProjectionInput<'_>,
438 redactor: &Redactor,
439 ids: &mut dyn IdSource,
440) -> Result<Projected, ProjectionUnknown> {
441 let rec = input.record;
442 if ![schema::RUN, schema::RUN_V2, schema::RUN_V3].contains(&rec.schema.as_str()) {
443 return Err(unknown(format!("run record {}", rec.schema)));
444 }
445 let evidence = match &rec.core {
446 CoreEvidence::Recorded(ev) => {
447 if ![schema::EVIDENCE, schema::EVIDENCE_V2].contains(&ev.schema.as_str())
448 || ![schema::JUDGMENT, schema::JUDGMENT_V2].contains(&ev.judgment.schema.as_str())
449 {
450 return Err(unknown(format!(
451 "{} with {}",
452 ev.schema, ev.judgment.schema
453 )));
454 }
455 Some(ev.as_ref())
456 }
457 CoreEvidence::NotProduced => None,
458 };
459 if let Some(ev) = evidence
460 && matches!(
461 ev.judgment.outcome,
462 Outcome::NeedsInput(_) | Outcome::UnresolvedAction { .. }
463 )
464 {
465 return Err(unknown("judgment outcome of a later spec"));
466 }
467 if let Some(p) = input.plan
468 && ![schema::PLAN, schema::PLAN_V3].contains(&p.schema.as_str())
469 {
470 return Err(unknown(format!("plan {}", p.schema)));
471 }
472
473 let a = input.anchor.unix_nanos;
474 let root_start = a.saturating_add(ms_to_nanos(rec.timing.queued_ms));
475 let root_end = a
476 .saturating_add(ms_to_nanos(rec.timing.elapsed_ms))
477 .max(root_start);
478 let ctx = Ctx {
479 evidence,
480 plan: input.plan,
481
482 anchor_kind: input.anchor.kind,
483 };
484 let mut facts = Facts {
485 termination: name_of(&rec.termination),
486 plan_id: rec.plan_id.to_string(),
487 elapsed_ms: rec.timing.elapsed_ms,
488 final_cost: name_of(&rec.cost.final_cost),
489 cost_units: rec.cost.observed.saturating_add(rec.cost.estimated),
490 ..Facts::default()
491 };
492
493 if let Some(entries) = evidence.and_then(|e| e.screening.as_ref()) {
494 use rustev_contract::screening::ScreenVerdict;
495 facts.screening = entries
496 .iter()
497 .map(|e| {
498 let verdict = match e.verdict {
499 ScreenVerdict::Pass => "pass",
500 ScreenVerdict::Blocked => "blocked",
501 ScreenVerdict::Unavailable { .. } => "unavailable",
502 ScreenVerdict::Empty => "empty",
503 };
504 (e.entry.clone(), verdict.into())
505 })
506 .collect();
507 }
508 if evidence.is_some_and(|e| e.screening.is_some())
509 || input.plan.is_some_and(|p| p.screening.is_some())
510 || rec.requests.iter().any(|r| r.screen.is_some())
511 {
512 facts.screening_units = Some(
513 rec.requests
514 .iter()
515 .filter_map(|r| r.screen.as_ref())
516 .fold(0u64, |sum, r| sum.saturating_add(r.units.charged)),
517 );
518 }
519
520 let mut root = Draft {
522 name: span::DECISION,
523 span_id: input.root.span_id,
524 parent: input.root.parent_span_id,
525 kind: SpanKind::Internal,
526 start: root_start,
527 end: root_end,
528 status: SpanStatus::Unset,
529 b: SpanBuilder::new(redactor, input.value_bound),
530 };
531 root_attributes(&mut root.b, rec, evidence, &ctx, &mut facts, root_start);
532
533 let mut drafts: Vec<Draft<'_>> = vec![root];
536 let mut used = vec![false; rec.requests.len()];
537 let mut order: Vec<(Option<&StepEvidence>, Option<&RequestRecord>)> = vec![];
538 for se in evidence.map(|e| e.steps.as_slice()).unwrap_or(&[]) {
539 let req = rec
540 .requests
541 .iter()
542 .enumerate()
543 .find(|(i, r)| !used[*i] && r.step == se.step && r.instance == se.instance);
544 if let Some((i, _)) = req {
545 used[i] = true;
546 }
547 order.push((Some(se), req.map(|(_, r)| r)));
548 }
549 for (i, r) in rec.requests.iter().enumerate() {
550 if !used[i] {
551 order.push((None, Some(r)));
552 }
553 }
554 for (se, req) in order {
555 let step_id = se.map(|s| s.step.as_str()).or(req.map(|r| r.step.as_str()));
556 let step_id = step_id.unwrap_or_default();
557 let kind = ctx.step_kind(step_id)?;
558 let step_span = ids.span_id();
559 let step_index = drafts.len();
560 let mut step = Draft {
561 name: span::STEP,
562 span_id: step_span,
563 parent: Some(input.root.span_id),
564 kind: SpanKind::Internal,
565 start: root_end,
566 end: root_end,
567 status: SpanStatus::Unset,
568 b: SpanBuilder::new(redactor, input.value_bound),
569 };
570 step_attributes(&mut step.b, se, req, &kind, &ctx, step_id, &mut facts);
571 drafts.push(step);
572 let mut window: Option<(u64, u64)> = None;
573 if let Some(r) = req {
574 for (n, att) in r.attempts.iter().enumerate() {
575 let start = a.saturating_add(ms_to_nanos(att.dispatched_ms));
576 let end = a.saturating_add(ms_to_nanos(att.ended_ms)).max(start);
577 window = Some(match window {
578 None => (start, end),
579 Some((s, e)) => (s.min(start), e.max(end)),
580 });
581 let retry_index = r.attempts[..n]
582 .iter()
583 .filter(|p| p.target == att.target)
584 .count() as u64;
585 let mut d = Draft {
586 name: span::ATTEMPT,
587 span_id: ids.span_id(),
588 parent: Some(step_span),
589 kind: SpanKind::Internal,
590 start,
591 end,
592 status: SpanStatus::Unset,
593 b: SpanBuilder::new(redactor, input.value_bound),
594 };
595 let line = drafts.len();
596 let genai =
597 attempt_attributes(&mut d, att, r, retry_index, ctx.operation(step_id), input);
598 facts.attempts.push(AttemptFacts {
599 line,
600 step_kind: kind.clone(),
601 end: attempt_end_name(&att.end).to_string(),
602 backend_id: att.backend_id.clone(),
603 duration_ms: att.ended_ms.saturating_sub(att.dispatched_ms),
604 genai,
605 });
606 drafts.push(d);
607 }
608 let at = window.map(|w| w.0).unwrap_or(root_end);
610 for t in &r.transitions {
611 transition_event(&mut drafts[step_index].b, t, at);
612 if matches!(t, Transition::Fallback { .. }) {
613 facts.fallbacks += 1;
614 }
615 }
616 }
617 let s = &mut drafts[step_index];
618 match window {
619 Some((start, end)) => {
620 s.start = start;
621 s.end = end;
622 }
623 None => {
624 s.b.put("rustev.timing", FieldClass::Enum, "not_recorded".into())
625 }
626 }
627 }
628
629 let mut policy = Draft {
631 name: span::POLICY,
632 span_id: ids.span_id(),
633 parent: Some(input.root.span_id),
634 kind: SpanKind::Internal,
635 start: root_end,
636 end: root_end,
637 status: SpanStatus::Unset,
638 b: SpanBuilder::new(redactor, input.value_bound),
639 };
640 policy
641 .b
642 .put("rustev.timing", FieldClass::Enum, "not_recorded".into());
643 if let Some(ev) = evidence {
644 policy_attributes(&mut policy.b, ev, &ctx, root_end);
645 }
646 drafts.push(policy);
647
648 let trace_id = hex(&input.root.trace_id);
650 let mut out = Projected {
651 lines: Vec::with_capacity(drafts.len()),
652 attributes_cut: 0,
653 events_cut: 0,
654 facts,
655 };
656 for (i, mut d) in drafts.into_iter().enumerate() {
657 if i == 0 {
658 d.b.put_fixed(
659 fixed::POLICY_ID,
660 TraceValue::Str(redactor.policy_id().to_string()),
661 );
662 d.b.put_fixed(
663 fixed::KEY_ID,
664 TraceValue::Str(redactor.key_id().to_string()),
665 );
666 }
667 let line = finish(d, &trace_id, i == 0, &mut out)?;
668 out.lines.push(line);
669 }
670 Ok(out)
671}
672
673fn finish(
676 mut d: Draft<'_>,
677 trace_id: &str,
678 is_root: bool,
679 out: &mut Projected,
680) -> Result<TraceLine, ProjectionUnknown> {
681 loop {
682 if d.b.events_cut > 0 {
683 d.b.put("rustev.events_cut", FieldClass::Number, num(d.b.events_cut));
684 }
685 let mut attributes = d.b.attributes.clone();
686 attributes.insert(
687 fixed::WITHHELD.to_string(),
688 TraceAttr {
689 v: TraceValue::StrArray(d.b.withheld.iter().cloned().collect()),
690 r: Mark::Allow,
691 },
692 );
693 let line = TraceLine {
694 schema: TRACE.to_string(),
695 schema_version: SPAN_SCHEMA_VERSION,
696 trace_id: trace_id.to_string(),
697 span_id: hex(&d.span_id),
698 parent_span_id: d.parent.map(|p| hex(&p)),
699 name: d.name.to_string(),
700 kind: d.kind,
701 start_unix_nanos: d.start,
702 end_unix_nanos: d.end,
703 status: d.status,
704 attributes,
705 events: d.b.events.clone(),
706 resource: is_root.then(resource),
707 };
708 match line.to_line() {
709 Ok(_) => {
710 out.attributes_cut += d.b.attributes_cut;
711 out.events_cut += d.b.events_cut;
712 return Ok(line);
713 }
714 Err(TraceLineError::TooLong { .. }) if !d.b.events.is_empty() => {
715 let drop = d.b.events.len().div_ceil(2);
716 d.b.events.truncate(d.b.events.len() - drop);
717 d.b.events_cut += drop as u64;
718 }
719 Err(TraceLineError::TooLong { .. }) => {
720 let mut changed = false;
723 for a in d.b.attributes.values_mut() {
724 let (v, cut) = truncate_value(&a.v, 256);
725 if cut {
726 a.v = v;
727 if a.r == Mark::Allow {
728 a.r = Mark::Truncated;
729 }
730 d.b.attributes_cut += 1;
731 changed = true;
732 }
733 }
734 if !changed {
735 return Err(unknown("a span that cannot fit one line"));
736 }
737 }
738 Err(e) => return Err(unknown(format!("span line: {e}"))),
739 }
740 }
741}
742
743fn resource() -> TraceResource {
744 let mut attributes = BTreeMap::new();
745 attributes.insert(
746 fixed::SCHEMA_VERSION.to_string(),
747 TraceAttr {
748 v: TraceValue::Int(i64::from(SPAN_SCHEMA_VERSION)),
749 r: Mark::Allow,
750 },
751 );
752 TraceResource {
753 attributes,
754 schema_url: SCHEMA_URL.to_string(),
755 scope: TraceScope {
756 name: SCOPE_NAME.to_string(),
757 version: env!("CARGO_PKG_VERSION").to_string(),
758 schema_url: SCHEMA_URL.to_string(),
759 },
760 }
761}
762
763fn root_attributes(
764 b: &mut SpanBuilder<'_>,
765 rec: &RunRecord,
766 evidence: Option<&EvidenceRecord>,
767 ctx: &Ctx<'_>,
768 facts: &mut Facts,
769 root_start: u64,
770) {
771 use FieldClass::*;
772 b.put("rustev.decision.id", Id, rec.decision_id.as_str().into());
773 b.put("rustev.plan.id", PlanMetadata, rec.plan_id.as_str().into());
774 b.put(
775 "rustev.execution.policy_id",
776 PlanMetadata,
777 match &rec.execution {
778 RecordedExecution::None => "none".into(),
779 RecordedExecution::Declared(id) => id.as_str().into(),
780 },
781 );
782 b.put("rustev.termination", Enum, facts.termination.clone().into());
783 match evidence {
784 None => {
785 b.put("rustev.outcome", Enum, "not_produced".into());
786 facts.outcome = "not_produced".into();
787 }
788 Some(ev) => {
789 b.put(
790 "rustev.definition.id",
791 PlanMetadata,
792 ev.definition_id.as_str().into(),
793 );
794 b.put(
795 "rustev.snapshot.id",
796 ContentDigest,
797 ev.snapshot_id.as_str().into(),
798 );
799 let j = &ev.judgment;
800 let outcome = match &j.outcome {
801 Outcome::Propose { action, params } => {
802 b.put(
803 "rustev.outcome.action",
804 PlanMetadata,
805 action.as_str().into(),
806 );
807 let inputs = ctx.lineage_inputs();
808 for (name, v) in params {
809 let (value, bool_or_enum) = param_value(v);
810 let class = lineage_class(bool_or_enum, inputs.iter().copied());
811 b.put(&format!("{PARAM_PREFIX}{name}"), class, value);
812 }
813 "propose"
814 }
815 Outcome::Escalate { reason } => {
816 b.put(
817 "rustev.outcome.escalate_reason",
818 PlanMetadata,
819 reason.as_str().into(),
820 );
821 "escalate"
822 }
823 Outcome::Unresolved(u) => {
824 let (kind, fields, detail) = unresolved_parts(u);
825 b.put("rustev.outcome.unresolved_reason", Enum, kind.into());
826 facts.outcome_unresolved_reason = Some(kind.into());
827 if let Some(f) = fields {
828 b.put("rustev.outcome.unresolved_fields", PlanMetadata, strs(f));
829 }
830 if let Some(d) = detail {
831 b.put("rustev.outcome.unresolved_detail", FreeTextDetail, d.into());
832 }
833 "unresolved"
834 }
835 Outcome::Refused { .. } => "refused",
836 Outcome::NeedsInput(_) | Outcome::UnresolvedAction { .. } => "unknown",
840 };
841 facts.outcome = outcome.into();
842 b.put("rustev.outcome", Enum, outcome.into());
843 b.put("rustev.derivation", Enum, name_of(&j.derivation).into());
844 b.put(
845 "rustev.lineage.inputs",
846 PlanMetadata,
847 strs(&j.lineage.inputs),
848 );
849 b.put("rustev.lineage.steps", PlanMetadata, strs(&j.lineage.steps));
850 for i in &ev.inputs {
851 let mut attrs = vec![("field", PlanMetadata, i.field.as_str().into())];
852 match &i.status {
853 InputStatus::Resolved {
854 provenance,
855 as_of_ms,
856 source,
857 } => {
858 attrs.push(("status", Enum, "resolved".into()));
859 attrs.push(("provenance", Enum, name_of(provenance).into()));
860 attrs.push(("as_of_ms", Number, TraceValue::Int(as_of_ms.as_ms())));
861 attrs.push(("source", Id, source.as_str().into()));
862 }
863 InputStatus::Unresolved(u) => {
864 attrs.push(("status", Enum, "unresolved".into()));
865 attrs.push(("unresolved_reason", Enum, u.kind().name().into()));
866 }
867 }
868 b.event(event::INPUT, root_start, attrs);
869 }
870 for n in &j.notices {
871 b.event(
872 event::NOTICE,
873 root_start,
874 vec![
875 ("kind", Enum, name_of(&n.kind).into()),
876 ("subject", PlanMetadata, n.subject.as_str().into()),
877 ("reason", PlanMetadata, n.reason.as_str().into()),
878 ],
879 );
880 }
881 }
882 }
883 let c = &rec.cost;
884 b.put("rustev.cost.mode", Enum, name_of(&c.mode).into());
885 b.put("rustev.cost.final", Enum, name_of(&c.final_cost).into());
886 b.put("rustev.cost.limit", Number, num(c.limit));
887 b.put("rustev.cost.observed", Number, num(c.observed));
888 b.put("rustev.cost.estimated", Number, num(c.estimated));
889 b.put("rustev.cost.liability", Number, num(c.liability));
890 b.put(
891 "rustev.cost.bound_violations",
892 Number,
893 num(c.bound_violations),
894 );
895 b.put(
896 "rustev.cost.within_guaranteed_cap",
897 Enum,
898 c.within_guaranteed_cap.into(),
899 );
900 let t = &rec.timing;
901 b.put("rustev.timing.queued_ms", Number, num(t.queued_ms));
902 b.put("rustev.timing.elapsed_ms", Number, num(t.elapsed_ms));
903 b.put("rustev.timing.deadline_ms", Number, num(t.deadline_ms));
904 b.put(
905 "rustev.timing.deadline_expired",
906 Enum,
907 t.deadline_expired.into(),
908 );
909 b.put(
910 "rustev.deadline.remaining_ms",
911 Number,
912 num(t.deadline_ms.saturating_sub(t.elapsed_ms)),
913 );
914 b.put("rustev.time_anchor", Enum, ctx.anchor_kind.name().into());
915}
916
917fn step_attributes(
918 b: &mut SpanBuilder<'_>,
919 se: Option<&StepEvidence>,
920 req: Option<&RequestRecord>,
921 kind: &str,
922 ctx: &Ctx<'_>,
923 step_id: &str,
924 facts: &mut Facts,
925) {
926 use FieldClass::*;
927 b.put("rustev.step.id", PlanMetadata, step_id.into());
928 let instance = se.map(|s| &s.instance).or(req.map(|r| &r.instance));
929 if let Some(inst) = instance.filter(|i| !i.is_empty()) {
930 b.put("rustev.step.instance", Id, strs(inst));
931 }
932 b.put("rustev.step.kind", Enum, kind.into());
933 if let Some(p) = ctx.plan
934 && let Some(ps) = p.steps.iter().find(|s| s.id == step_id)
935 {
936 match &ps.detail {
937 PlanStepDetail::Exact { op, version } => b.put(
938 "rustev.step.operator",
939 PlanMetadata,
940 format!("{op}@{version}").into(),
941 ),
942 PlanStepDetail::Semantic(s) => b.put(
943 "rustev.step.backend_id",
944 PlanMetadata,
945 s.backend_id.as_str().into(),
946 ),
947 PlanStepDetail::Unsupported { .. }
948 | PlanStepDetail::Corroborate
949 | PlanStepDetail::Acquire
950 | PlanStepDetail::Widen
951 | PlanStepDetail::Generate(_)
952 | PlanStepDetail::Extract(_)
953 | PlanStepDetail::AcceptModelDerived(_)
954 | PlanStepDetail::Rank(_)
955 | PlanStepDetail::Select(_) => {}
956 }
957 }
958 if let Some(s) = se {
959 match &s.status {
960 StepStatus::Skipped { .. } => b.put("rustev.step.status", Enum, "skipped".into()),
961 StepStatus::Resolved { kind, derivation } => {
962 b.put("rustev.step.status", Enum, "resolved".into());
963 b.put("rustev.step.value_kind", Enum, kind.as_str().into());
964 b.put("rustev.step.derivation", Enum, name_of(derivation).into());
965 }
966 StepStatus::Unresolved(u) => {
967 b.put("rustev.step.status", Enum, "unresolved".into());
968 let k = u.kind().name();
969 b.put("rustev.step.unresolved_reason", Enum, k.into());
970 facts.step_unresolved_reasons.push(k.into());
971 }
972 }
973 }
974 if let Some(r) = req {
975 match &r.result {
976 RequestResult::Output { target } => {
977 b.put("rustev.step.result", Enum, "output".into());
978 b.put("rustev.step.target", Number, num(u64::from(*target)));
979 }
980 RequestResult::Failed(_) => b.put("rustev.step.result", Enum, "failed".into()),
981 RequestResult::Withdrawn => {}
982 RequestResult::NotSupplied => b.put("rustev.step.result", Enum, "not_supplied".into()),
983 }
984 if let Some(o) = &r.optimization {
985 b.put(
986 "rustev.step.optimization.outcome",
987 Enum,
988 name_of(&o.outcome).into(),
989 );
990 b.put(
991 "rustev.step.optimization.mechanism",
992 Enum,
993 name_of(&o.mechanism).into(),
994 );
995 }
996 if let Some(from) = r.supplied_from()
997 && let Some(id) = from.reused_from
998 {
999 b.put("rustev.step.reused_from", Id, id.into());
1000 }
1001 }
1002}
1003
1004fn transition_event(b: &mut SpanBuilder<'_>, t: &Transition, at: u64) {
1005 use FieldClass::*;
1006 let attrs = match t {
1007 Transition::Retry {
1008 after,
1009 class,
1010 delay_ms,
1011 } => vec![
1012 ("kind", Enum, "retry".into()),
1013 ("class", Enum, class.name().into()),
1014 ("after", Number, num(u64::from(*after))),
1015 ("delay_ms", Number, num(*delay_ms)),
1016 ],
1017 Transition::Fallback { after, class, to } => vec![
1018 ("kind", Enum, "fallback".into()),
1019 ("class", Enum, class.name().into()),
1020 ("after", Number, num(u64::from(*after))),
1021 ("to", Number, num(u64::from(*to))),
1022 ],
1023 Transition::Stop { after, reason } => vec![
1024 ("kind", Enum, "stop".into()),
1025 ("after", Number, num(u64::from(*after))),
1026 ("reason", FreeTextDetail, reason.as_str().into()),
1027 ],
1028 };
1029 b.event(event::TRANSITION, at, attrs);
1030}
1031
1032fn attempt_end_name(e: &AttemptEnd) -> &'static str {
1033 match e {
1034 AttemptEnd::Output => "output",
1035 AttemptEnd::Failed { .. } => "failed",
1036 AttemptEnd::Deadline => "deadline",
1037 AttemptEnd::Cancelled => "cancelled",
1038 }
1039}
1040
1041fn attempt_attributes(
1043 d: &mut Draft<'_>,
1044 att: &AttemptRecord,
1045 req: &RequestRecord,
1046 retry_index: u64,
1047 operation: Option<&'static str>,
1048 input: &ProjectionInput<'_>,
1049) -> Option<GenAiFacts> {
1050 use FieldClass::*;
1051 let b = &mut d.b;
1052 b.put("rustev.attempt.id", Id, att.attempt_id.as_str().into());
1053 if let Some(n) = att
1054 .attempt_id
1055 .rsplit('/')
1056 .next()
1057 .and_then(|n| n.parse::<u64>().ok())
1058 {
1059 b.put("rustev.attempt.ordinal", Number, num(n));
1060 }
1061 b.put("rustev.attempt.retry_index", Number, num(retry_index));
1062 b.put("rustev.attempt.target", Number, num(u64::from(att.target)));
1063 b.put(
1064 "rustev.attempt.backend_id",
1065 PlanMetadata,
1066 att.backend_id.as_str().into(),
1067 );
1068 b.put(
1069 "rustev.attempt.artifact",
1070 PlanMetadata,
1071 att.artifact.as_str().into(),
1072 );
1073 b.put(
1074 "rustev.attempt.end",
1075 Enum,
1076 attempt_end_name(&att.end).into(),
1077 );
1078 match &att.end {
1079 AttemptEnd::Failed { class, detail } => {
1080 b.put("rustev.attempt.failure_class", Enum, class.name().into());
1081 b.put("error.type", Enum, class.name().into());
1082 b.put(
1083 "rustev.attempt.failure_detail",
1084 FreeTextDetail,
1085 detail.as_str().into(),
1086 );
1087 d.status = SpanStatus::Error;
1088 }
1089 AttemptEnd::Deadline => {
1090 b.put("error.type", Enum, "deadline".into());
1091 d.status = SpanStatus::Error;
1092 }
1093 AttemptEnd::Output | AttemptEnd::Cancelled => {}
1094 }
1095 b.put(
1096 "rustev.attempt.cancel",
1097 Enum,
1098 match att.cancellation {
1099 Cancellation::NotRequested => "not_requested".into(),
1100 Cancellation::Requested { answer } => name_of(&answer).into(),
1101 },
1102 );
1103 b.put(
1104 "rustev.attempt.remote_state",
1105 Enum,
1106 name_of(&att.remote).into(),
1107 );
1108 let (bound, _) = match att.cost.bound {
1109 CostBound::Bounded { max_units } => ("bounded", Some(max_units)),
1110 CostBound::Estimated { units } => ("estimated", Some(units)),
1111 CostBound::Unknown => ("unknown", None),
1112 };
1113 b.put("rustev.attempt.cost.bound", Enum, bound.into());
1114 b.put(
1115 "rustev.attempt.cost.reserved",
1116 Number,
1117 num(att.cost.reserved),
1118 );
1119 let (charge, units) = match att.cost.charge {
1120 Charge::Observed { units } => ("observed", Some(units)),
1121 Charge::Estimated { units } => ("estimated", Some(units)),
1122 Charge::Unknown => ("unknown", None),
1123 };
1124 b.put("rustev.attempt.charge_kind", Enum, charge.into());
1125 if let Some(u) = units {
1126 b.put("rustev.attempt.charge_units", Number, num(u));
1127 }
1128 if let Some(o) = &req.optimization {
1129 if let Some(id) = &o.batch_id {
1130 b.put("rustev.batch.id", Id, id.as_str().into());
1131 }
1132 if let Some(m) = o.batch_member {
1133 b.put("rustev.batch.member", Number, num(u64::from(m)));
1134 }
1135 }
1136
1137 let exchange = input.exchanges.get(&att.attempt_id);
1140 let provider = input.bindings.providers.get(&att.backend_id);
1141 if exchange.is_none() && provider.is_none() {
1142 return None;
1143 }
1144 d.kind = SpanKind::Client;
1145 let provider = provider.cloned().unwrap_or_else(|| "unknown".into());
1146 b.put(
1147 "gen_ai.provider.name",
1148 ModelIdentity,
1149 provider.as_str().into(),
1150 );
1151 if let Some(op) = operation {
1152 b.put("gen_ai.operation.name", Enum, op.into());
1153 }
1154 let request_model = exchange
1155 .and_then(|x| x.requested_model.clone())
1156 .unwrap_or_else(|| att.artifact.to_string());
1157 b.put(
1158 "gen_ai.request.model",
1159 ModelIdentity,
1160 request_model.as_str().into(),
1161 );
1162 let mut facts = GenAiFacts {
1163 operation: operation.map(str::to_string),
1164 provider,
1165 request_model,
1166 input_tokens: None,
1167 output_tokens: None,
1168 };
1169 let Some(x) = exchange else {
1170 b.put("gen_ai.response.model", ModelIdentity, "unknown".into());
1171 b.put("rustev.served.disclosure", Enum, "unknown".into());
1172 return Some(facts);
1173 };
1174 let response_model = match (&x.served.disclosure, &x.served.identity) {
1175 (Disclosure::Pinned | Disclosure::Reported, Some(id)) => id.clone(),
1176 _ => "unknown".into(),
1177 };
1178 b.put(
1179 "gen_ai.response.model",
1180 ModelIdentity,
1181 response_model.into(),
1182 );
1183 b.put(
1184 "rustev.served.disclosure",
1185 Enum,
1186 name_of(&x.served.disclosure).into(),
1187 );
1188 if let Some(g) = &x.generation_id {
1189 b.put("gen_ai.response.id", Id, g.as_str().into());
1190 }
1191 if let Some(keys) = input.bindings.usage_keys.get(&att.backend_id) {
1192 if let Some(n) = keys.input.as_ref().and_then(|k| x.usage.get(k)) {
1194 b.put("gen_ai.usage.input_tokens", Number, num(*n));
1195 facts.input_tokens = Some(*n);
1196 }
1197 if let Some(n) = keys.output.as_ref().and_then(|k| x.usage.get(k)) {
1198 b.put("gen_ai.usage.output_tokens", Number, num(*n));
1199 facts.output_tokens = Some(*n);
1200 }
1201 }
1202 if let Some(dg) = &x.request_digest {
1203 b.put(
1204 "rustev.exchange.request_digest",
1205 ContentDigest,
1206 dg.as_str().into(),
1207 );
1208 }
1209 if let Some(dg) = &x.response_digest {
1210 b.put(
1211 "rustev.exchange.response_digest",
1212 ContentDigest,
1213 dg.as_str().into(),
1214 );
1215 }
1216 if let Some(s) = x.status {
1217 b.put(
1218 "rustev.exchange.status",
1219 Number,
1220 TraceValue::Int(i64::from(s)),
1221 );
1222 }
1223 b.put("rustev.exchange.bytes_sent", Enum, x.bytes_sent.into());
1224 b.put("rustev.exchange.late", Enum, x.late.into());
1225 b.put("rustev.exchange.truncated", Enum, x.truncated.into());
1226 if !x.route.is_empty() {
1227 b.put("rustev.exchange.route", ModelIdentity, strs(&x.route));
1228 }
1229 if let Some(c) = &x.reported_cost {
1230 b.put(
1231 "rustev.exchange.reported_cost",
1232 Number,
1233 c.amount.as_str().into(),
1234 );
1235 b.put(
1236 "rustev.exchange.reported_currency",
1237 Enum,
1238 c.currency.as_str().into(),
1239 );
1240 }
1241 if let Some(r) = &x.retry_after {
1242 b.put("rustev.exchange.retry_after", ThirdParty, r.as_str().into());
1243 }
1244 for (k, v) in &x.provider_extras {
1245 b.put(
1246 &format!("rustev.exchange.extra.{k}"),
1247 ThirdParty,
1248 v.as_str().into(),
1249 );
1250 }
1251 if let Some(m) = x.members.iter().find(|m| m.attempt_id == att.attempt_id)
1252 && let MemberOutcome::Failure { detail, .. } = &m.outcome
1253 {
1254 b.put(
1255 "rustev.exchange.member_detail",
1256 FreeTextDetail,
1257 detail.as_str().into(),
1258 );
1259 }
1260 Some(facts)
1261}
1262
1263fn policy_attributes(b: &mut SpanBuilder<'_>, ev: &EvidenceRecord, ctx: &Ctx<'_>, at: u64) {
1264 use FieldClass::*;
1265 let j = &ev.judgment;
1266 b.put("rustev.policy.rules_fired", PlanMetadata, strs(&j.trace));
1267 let Outcome::Propose { params, .. } = &j.outcome else {
1268 return;
1269 };
1270 let inputs = ctx.lineage_inputs();
1271 let mut excluded = None::<u64>;
1272 let mut truncated = None::<u64>;
1273 let mut excl = |b: &mut SpanBuilder<'_>, list: &[rustev_contract::judgment::Excluded]| {
1274 *excluded.get_or_insert(0) += list.len() as u64;
1275 for e in list {
1276 b.event(
1277 event::EXCLUDED,
1278 at,
1279 vec![
1280 ("candidate", Id, e.id.as_str().into()),
1281 ("reasons", PlanMetadata, strs(&e.reasons)),
1282 ],
1283 );
1284 }
1285 };
1286 for v in params.values() {
1287 match v {
1288 OutValue::Filtered(f) => excl(b, &f.excluded),
1289 OutValue::Shortlist(s) => {
1290 *truncated.get_or_insert(0) += s.truncated;
1291 excl(b, &s.excluded);
1292 }
1293 OutValue::Ranking(r) => {
1294 excl(b, &r.excluded);
1295 let class = lineage_class(false, inputs.iter().copied());
1296 for e in &r.entries {
1297 b.event(
1298 event::POSITION,
1299 at,
1300 vec![
1301 ("position", Number, num(e.position)),
1302 ("candidate", Id, e.candidate.as_str().into()),
1303 ("score", class, TraceValue::Double(e.score)),
1304 ],
1305 );
1306 }
1307 }
1308 _ => {}
1309 }
1310 }
1311 if let Some(n) = excluded {
1312 b.put("rustev.policy.excluded_count", Number, num(n));
1313 }
1314 if let Some(n) = truncated {
1315 b.put("rustev.policy.truncated", Number, num(n));
1316 }
1317}