Skip to main content

vtcode_exec_events/
otel_support.rs

1use opentelemetry::KeyValue;
2use opentelemetry::trace::{Span, Status, Tracer};
3
4use super::{EventEmitter, ThreadEvent, ThreadItemDetails};
5
6/// Emits [`ThreadEvent`]s as OpenTelemetry spans and span events.
7///
8/// Each `ThreadEvent` is recorded as an OTel span with attributes derived
9/// from the event payload.  Harness events are attached as span events
10/// with their own attributes (event kind, message, path, etc.).
11///
12/// # Usage
13///
14/// ```rust,ignore
15/// // Requires concrete SDK type (e.g. opentelemetry_sdk::trace::SdkTracerProvider)
16/// # use vtcode_exec_events::OtelEmitter;
17/// # let tracer = opentelemetry_sdk::trace::SdkTracerProvider::default()
18/// #     .tracer("vtcode");
19/// # let mut emitter = OtelEmitter::new(tracer);
20/// ```
21pub 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;