1use std::collections::HashMap;
2use std::sync::{
3 Arc, RwLock,
4 atomic::{AtomicBool, Ordering},
5};
6use tracing::{Id, Subscriber, debug};
7use tracing_subscriber::{Layer, layer::Context, registry::LookupSpan};
8
9pub trait SpanSink: Send + Sync {
17 fn export_span(&self, span_name: &str, attributes: HashMap<String, String>);
19}
20
21#[derive(Debug, Clone, Default)]
27pub struct AdkSpanExporter {
28 trace_dict: Arc<RwLock<HashMap<String, HashMap<String, String>>>>,
30 collecting: Arc<AtomicBool>,
32}
33
34impl AdkSpanExporter {
35 pub fn new() -> Self {
37 Self {
38 trace_dict: Arc::new(RwLock::new(HashMap::new())),
39 collecting: Arc::new(AtomicBool::new(false)),
40 }
41 }
42
43 pub fn get_trace_dict(&self) -> HashMap<String, HashMap<String, String>> {
45 self.trace_dict.read().unwrap_or_else(|e| e.into_inner()).clone()
46 }
47
48 pub fn get_trace_by_event_id(&self, event_id: &str) -> Option<HashMap<String, String>> {
50 debug!("AdkSpanExporter::get_trace_by_event_id called with event_id: {}", event_id);
51 let trace_dict = self.trace_dict.read().unwrap_or_else(|e| e.into_inner());
52 let result = trace_dict.get(event_id).cloned().or_else(|| {
53 trace_dict
54 .values()
55 .find(|attributes| {
56 attributes.get("gcp.vertex.agent.event_id").is_some_and(|id| id == event_id)
57 })
58 .cloned()
59 });
60 debug!("get_trace_by_event_id result for event_id '{}': {:?}", event_id, result.is_some());
61 result
62 }
63
64 pub fn is_collecting(&self) -> bool {
70 self.collecting.load(Ordering::Acquire)
71 }
72
73 pub fn get_session_trace(&self, session_id: &str) -> Vec<HashMap<String, String>> {
75 debug!("AdkSpanExporter::get_session_trace called with session_id: {}", session_id);
76 let trace_dict = self.trace_dict.read().unwrap_or_else(|e| e.into_inner());
77
78 let mut spans = Vec::new();
79 for (_event_id, attributes) in trace_dict.iter() {
80 if let Some(span_session_id) = attributes.get("gcp.vertex.agent.session_id")
82 && span_session_id == session_id
83 {
84 spans.push(attributes.clone());
85 }
86 }
87
88 debug!("get_session_trace result for session_id '{}': {} spans", session_id, spans.len());
89 spans
90 }
91}
92
93impl SpanSink for AdkSpanExporter {
94 fn export_span(&self, span_name: &str, attributes: HashMap<String, String>) {
96 if is_runtime_span(span_name) {
97 if let Some(event_id) = attributes.get("gcp.vertex.agent.event_id") {
98 debug!(
99 "AdkSpanExporter: Storing span '{}' with event_id '{}'",
100 span_name, event_id
101 );
102 let storage_key =
103 attributes.get("span_id").cloned().unwrap_or_else(|| event_id.clone());
104 let mut trace_dict = self.trace_dict.write().unwrap_or_else(|e| e.into_inner());
105 trace_dict.insert(storage_key, attributes);
106 self.collecting.store(true, Ordering::Release);
107 debug!("AdkSpanExporter: Span stored, total spans: {}", trace_dict.len());
108 } else {
109 debug!("AdkSpanExporter: Skipping span '{}' - no event_id found", span_name);
110 }
111 } else {
112 debug!("AdkSpanExporter: Skipping span '{}' - not in allowed list", span_name);
113 }
114 }
115}
116
117pub(crate) fn is_runtime_span(span_name: &str) -> bool {
118 span_name == "agent.execute"
119 || span_name == "call_llm"
120 || span_name == "send_data"
121 || span_name.starts_with("execute_tool")
122 || matches!(span_name, "team.run" | "team.member.run" | "team.relationship.execute")
123}
124
125pub struct AdkSpanLayer {
128 exporter: Arc<dyn SpanSink>,
129}
130
131impl AdkSpanLayer {
132 pub fn new<S: SpanSink + 'static>(exporter: Arc<S>) -> Self {
133 Self { exporter }
134 }
135}
136
137#[derive(Clone)]
138struct SpanFields {
139 values: HashMap<String, String>,
140 event_id_declared: bool,
141}
142
143#[derive(Clone)]
144struct SpanTiming {
145 start_time: std::time::Instant,
146}
147
148impl<S> Layer<S> for AdkSpanLayer
149where
150 S: Subscriber + for<'a> LookupSpan<'a>,
151{
152 fn on_new_span(&self, attrs: &tracing::span::Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
153 let Some(span) = ctx.span(id) else { return };
154 let mut extensions = span.extensions_mut();
155
156 extensions.insert(SpanTiming { start_time: std::time::Instant::now() });
158
159 let mut visitor = StringVisitor::default();
161 attrs.record(&mut visitor);
162 let mut fields_map = visitor.0;
163 let event_id_declared = fields_map.contains_key("gcp.vertex.agent.event_id");
164
165 if let Some(parent) = span.parent()
167 && let Some(parent_fields) = parent.extensions().get::<SpanFields>()
168 {
169 let context_keys = [
170 "gcp.vertex.agent.session_id",
171 "gcp.vertex.agent.invocation_id",
172 "gcp.vertex.agent.event_id",
173 "gen_ai.conversation.id",
174 #[cfg(feature = "genai-semconv")]
175 "gen_ai.provider.name",
176 #[cfg(feature = "genai-semconv")]
177 "gen_ai.system",
178 ];
179
180 for key in context_keys {
181 if !fields_map.contains_key(key)
182 && let Some(val) = parent_fields.values.get(key)
183 {
184 fields_map.insert(key.to_string(), val.clone());
185 }
186 }
187 }
188
189 extensions.insert(SpanFields { values: fields_map, event_id_declared });
190 }
191
192 fn on_record(&self, id: &Id, values: &tracing::span::Record<'_>, ctx: Context<'_, S>) {
193 let Some(span) = ctx.span(id) else { return };
194 let mut extensions = span.extensions_mut();
195 if let Some(fields) = extensions.get_mut::<SpanFields>() {
196 let mut visitor = StringVisitor::default();
197 values.record(&mut visitor);
198 for (k, v) in visitor.0 {
199 if k == "gcp.vertex.agent.event_id" {
200 fields.event_id_declared = true;
201 }
202 fields.values.insert(k, v);
203 }
204 }
205 }
206
207 fn on_close(&self, id: Id, ctx: Context<'_, S>) {
208 let Some(span) = ctx.span(&id) else { return };
209 let extensions = span.extensions();
210
211 let timing = extensions.get::<SpanTiming>();
213 let end_time = std::time::Instant::now();
214 let duration_nanos =
215 timing.map(|t| end_time.duration_since(t.start_time).as_nanos() as u64).unwrap_or(0);
216
217 let span_fields = extensions.get::<SpanFields>();
219 let event_id_declared = span_fields.is_some_and(|fields| fields.event_id_declared);
220 let mut attributes = span_fields.map(|fields| fields.values.clone()).unwrap_or_default();
221
222 let metadata = span.metadata();
224 let span_name =
225 attributes.get("otel.name").cloned().unwrap_or_else(|| metadata.name().to_string());
226
227 let now_nanos = std::time::SystemTime::now()
229 .duration_since(std::time::UNIX_EPOCH)
230 .unwrap_or_default()
231 .as_nanos() as u64;
232
233 let generated_span_id = format!("{:016x}", id.into_u64());
240 let invocation_id = attributes
241 .get("gcp.vertex.agent.invocation_id")
242 .cloned()
243 .unwrap_or_else(|| generated_span_id.clone());
244 let event_id = attributes
245 .get("gcp.vertex.agent.event_id")
246 .cloned()
247 .unwrap_or_else(|| generated_span_id.clone());
248 let span_id = if event_id_declared && span_name != "send_data" {
249 event_id
250 } else {
251 generated_span_id
252 };
253
254 attributes.insert("span_name".to_string(), span_name.clone());
255 attributes.insert("trace_id".to_string(), invocation_id); attributes.insert("span_id".to_string(), span_id);
257 attributes.insert("start_time".to_string(), (now_nanos - duration_nanos).to_string());
258 attributes.insert("end_time".to_string(), now_nanos.to_string());
259
260 self.exporter.export_span(&span_name, attributes);
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use std::sync::Arc;
271 use tracing_subscriber::{
272 EnvFilter,
273 filter::filter_fn,
274 layer::{Layer, SubscriberExt},
275 };
276
277 #[test]
278 fn test_conversation_id_propagates_to_child_spans() {
279 let exporter = Arc::new(AdkSpanExporter::new());
280 let layer = AdkSpanLayer::new(exporter.clone());
281 let subscriber = tracing_subscriber::registry().with(layer);
282
283 tracing::subscriber::with_default(subscriber, || {
284 let parent = tracing::info_span!(
285 "agent.execute",
286 "gcp.vertex.agent.event_id" = "evt-parent",
287 "gcp.vertex.agent.invocation_id" = "inv-1",
288 "gcp.vertex.agent.session_id" = "session-1",
289 "gen_ai.conversation.id" = "session-1",
290 "agent.name" = "test-agent"
291 );
292
293 let _parent_guard = parent.enter();
294
295 let child = tracing::info_span!(
296 "call_llm",
297 "gcp.vertex.agent.event_id" = "evt-child",
298 "gcp.vertex.agent.llm_request" = "{}"
299 );
300 let _child_guard = child.enter();
301 tracing::info!("child span body");
302 });
303
304 let child_trace =
305 exporter.get_trace_by_event_id("evt-child").expect("child span should be exported");
306 assert_eq!(
307 child_trace.get("gen_ai.conversation.id").map(String::as_str),
308 Some("session-1")
309 );
310 }
311
312 #[test]
313 fn console_log_filter_does_not_suppress_runtime_span_capture() {
314 let exporter = Arc::new(AdkSpanExporter::new());
315 let capture = AdkSpanLayer::new(exporter.clone()).with_filter(filter_fn(|metadata| {
316 metadata.is_span() && is_runtime_span(metadata.name())
317 }));
318 let console = tracing_subscriber::fmt::layer()
319 .with_writer(std::io::sink)
320 .with_filter(EnvFilter::new("warn"));
321 let subscriber = tracing_subscriber::registry().with(console).with(capture);
322
323 tracing::subscriber::with_default(subscriber, || {
324 let span = tracing::info_span!(
325 "agent.execute",
326 "gcp.vertex.agent.event_id" = "evt-filtered-console",
327 "gcp.vertex.agent.invocation_id" = "inv-filtered-console",
328 "gcp.vertex.agent.session_id" = "session-filtered-console"
329 );
330 let _guard = span.enter();
331 });
332
333 assert!(exporter.get_trace_by_event_id("evt-filtered-console").is_some());
334 assert!(exporter.is_collecting());
335 }
336
337 #[test]
338 fn inherited_event_ids_do_not_overwrite_team_relationship_spans() {
339 let exporter = Arc::new(AdkSpanExporter::new());
340 let layer = AdkSpanLayer::new(exporter.clone());
341 let subscriber = tracing_subscriber::registry().with(layer);
342
343 tracing::subscriber::with_default(subscriber, || {
344 let parent = tracing::info_span!(
345 "agent.execute",
346 "gcp.vertex.agent.event_id" = "evt-team",
347 "gcp.vertex.agent.invocation_id" = "inv-team",
348 "gcp.vertex.agent.session_id" = "session-team"
349 );
350 let parent_guard = parent.enter();
351 let relationship = tracing::info_span!(
352 "team.relationship.execute",
353 team.name = "support",
354 team.relationship.from = "supervisor",
355 team.relationship.to = "billing",
356 team.relationship.kind = "handoff",
357 team.edge.id = "edge-1"
358 );
359 let relationship_guard = relationship.enter();
360 drop(relationship_guard);
361 drop(relationship);
362 drop(parent_guard);
363 drop(parent);
364 });
365
366 let spans = exporter.get_session_trace("session-team");
367 assert_eq!(spans.len(), 2);
368 assert!(spans.iter().any(|span| {
369 span.get("span_name").is_some_and(|name| name == "team.relationship.execute")
370 }));
371 let unique_span_ids = spans
372 .iter()
373 .filter_map(|span| span.get("span_id"))
374 .collect::<std::collections::HashSet<_>>();
375 assert_eq!(unique_span_ids.len(), 2);
376 }
377
378 #[test]
379 fn send_data_does_not_overwrite_call_llm_for_the_same_event() {
380 let exporter = Arc::new(AdkSpanExporter::new());
381 let layer = AdkSpanLayer::new(exporter.clone());
382 let subscriber = tracing_subscriber::registry().with(layer);
383
384 tracing::subscriber::with_default(subscriber, || {
385 let call_llm = tracing::info_span!(
386 "call_llm",
387 "gcp.vertex.agent.event_id" = "evt-model",
388 "gcp.vertex.agent.invocation_id" = "inv-model",
389 "gcp.vertex.agent.session_id" = "session-model"
390 );
391 drop(call_llm);
392
393 let send_data = tracing::info_span!(
394 "send_data",
395 "gcp.vertex.agent.event_id" = "evt-model",
396 "gcp.vertex.agent.invocation_id" = "inv-model",
397 "gcp.vertex.agent.session_id" = "session-model"
398 );
399 drop(send_data);
400 });
401
402 let spans = exporter.get_session_trace("session-model");
403 assert_eq!(spans.len(), 2);
404 assert!(
405 spans.iter().any(|span| span.get("span_name").is_some_and(|name| name == "call_llm"))
406 );
407 assert!(
408 spans.iter().any(|span| span.get("span_name").is_some_and(|name| name == "send_data"))
409 );
410 }
411}
412
413#[derive(Default)]
414struct StringVisitor(HashMap<String, String>);
415
416impl tracing::field::Visit for StringVisitor {
417 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
418 self.0.insert(field.name().to_string(), format!("{:?}", value));
419 }
420
421 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
422 self.0.insert(field.name().to_string(), value.to_string());
423 }
424
425 fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
426 self.0.insert(field.name().to_string(), value.to_string());
427 }
428
429 fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
430 self.0.insert(field.name().to_string(), value.to_string());
431 }
432
433 fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
434 self.0.insert(field.name().to_string(), value.to_string());
435 }
436
437 fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
438 self.0.insert(field.name().to_string(), value.to_string());
439 }
440}