vtcode_exec_events/
otel_support.rs1use opentelemetry::KeyValue;
2use opentelemetry::trace::{Span, Status, Tracer};
3
4use super::{EventEmitter, ThreadEvent, ThreadItemDetails};
5
6pub struct OtelEmitter<T: Tracer> {
22 tracer: T,
23}
24
25impl<T: Tracer> OtelEmitter<T> {
26 pub fn new(tracer: T) -> Self {
27 Self { tracer }
28 }
29}
30
31impl<T: Tracer> EventEmitter for OtelEmitter<T> {
32 fn emit(&mut self, event: &ThreadEvent) {
33 let span_name = match event {
34 ThreadEvent::ThreadStarted(_) => "thread.started",
35 ThreadEvent::ThreadCompleted(_) => "thread.completed",
36 ThreadEvent::ContextReset(_) => "context.reset",
37 ThreadEvent::TurnStarted(_) => "turn.started",
38 ThreadEvent::TurnCompleted(_) => "turn.completed",
39 ThreadEvent::TurnFailed(_) => "turn.failed",
40 ThreadEvent::ItemStarted(_) => "item.started",
41 ThreadEvent::ItemUpdated(_) => "item.updated",
42 ThreadEvent::ItemCompleted(_) => "item.completed",
43 ThreadEvent::Error(_) => "error",
44 _ => "event",
45 };
46
47 let mut span = self.tracer.start(span_name);
48
49 match event {
50 ThreadEvent::ThreadStarted(e) => {
51 span.set_attribute(KeyValue::new("thread_id", e.thread_id.clone()));
52 }
53 ThreadEvent::ThreadCompleted(e) => {
54 if let Some(ref cost) = e.total_cost_usd {
55 span.set_attribute(KeyValue::new("total_cost_usd", cost.as_f64().unwrap_or(0.0)));
56 }
57 span.set_attribute(KeyValue::new(
58 "input_tokens",
59 i64::try_from(e.usage.input_tokens).unwrap_or(i64::MAX),
60 ));
61 span.set_attribute(KeyValue::new(
62 "output_tokens",
63 i64::try_from(e.usage.output_tokens).unwrap_or(i64::MAX),
64 ));
65 span.set_attribute(KeyValue::new("completion_subtype", e.subtype.as_str().to_string()));
66 }
67 ThreadEvent::ContextReset(e) => {
68 span.set_attribute(KeyValue::new("thread_id", e.thread_id.clone()));
69 span.set_attribute(KeyValue::new("turn_id", e.turn_id.clone()));
70 span.set_attribute(KeyValue::new("plan_preserved", e.plan_preserved));
71 span.set_attribute(KeyValue::new(
72 "previous_context_usage_percent",
73 e.previous_context_usage_percent as i64,
74 ));
75 span.set_attribute(KeyValue::new("tool_budget_reset", e.tool_budget_reset));
76 }
77 ThreadEvent::TurnCompleted(e) => {
78 span.set_attribute(KeyValue::new(
79 "turn_input_tokens",
80 i64::try_from(e.usage.input_tokens).unwrap_or(i64::MAX),
81 ));
82 span.set_attribute(KeyValue::new(
83 "turn_output_tokens",
84 i64::try_from(e.usage.output_tokens).unwrap_or(i64::MAX),
85 ));
86 }
87 ThreadEvent::ItemCompleted(e) => {
88 if let ThreadItemDetails::Harness(harness) = &e.item.details {
89 span.set_attribute(KeyValue::new("harness_event", format!("{:?}", harness.event)));
90 if let Some(ref msg) = harness.message {
91 span.set_attribute(KeyValue::new("harness_message", msg.clone()));
92 }
93 if let Some(ref path) = harness.path {
94 span.set_attribute(KeyValue::new("harness_path", path.clone()));
95 }
96 if let Some(dur) = harness.duration_ms {
97 span.set_attribute(KeyValue::new("duration_ms", i64::try_from(dur).unwrap_or(i64::MAX)));
98 }
99 let mut event_attrs = vec![KeyValue::new("event_kind", format!("{:?}", harness.event))];
100 if let Some(ref msg) = harness.message {
101 event_attrs.push(KeyValue::new("message", msg.clone()));
102 }
103 span.add_event("harness_event", event_attrs);
104 }
105 }
106 ThreadEvent::Error(e) => {
107 span.set_status(Status::Error { description: e.message.clone().into() });
108 span.set_attribute(KeyValue::new("error_message", e.message.clone()));
109 }
110 _ => {}
111 }
112
113 span.end();
114 }
115}
116
117pub use OtelEmitter as PublicOtelEmitter;