gemini_adk_rs/telemetry/
spans.rs

1//! OpenTelemetry span definitions for agent lifecycle operations.
2//!
3//! Each span carries contextual fields for agent-level correlation.
4
5/// Create a span for an agent's run lifecycle.
6pub fn agent_run_span(agent_name: &str, session_id: &str) -> tracing::Span {
7    tracing::info_span!(
8        "gemini.agent.run",
9        agent_name = agent_name,
10        session_id = session_id,
11    )
12}
13
14/// Create a span for an agent transfer.
15pub fn agent_transfer_span(from: &str, to: &str, session_id: &str) -> tracing::Span {
16    tracing::info_span!(
17        "gemini.agent.transfer",
18        from = from,
19        to = to,
20        session_id = session_id,
21    )
22}
23
24/// Create a span for tool dispatch within an agent.
25pub fn tool_dispatch_span(tool_name: &str, tool_class: &str, session_id: &str) -> tracing::Span {
26    tracing::info_span!(
27        "gemini.agent.tool_dispatch",
28        tool_name = tool_name,
29        tool_class = tool_class,
30        session_id = session_id,
31    )
32}
33
34/// Create a span for an agent-as-tool invocation.
35pub fn agent_tool_span(agent_name: &str, parent_agent: &str) -> tracing::Span {
36    tracing::info_span!(
37        "gemini.agent.agent_tool",
38        agent_name = agent_name,
39        parent_agent = parent_agent,
40    )
41}
42
43/// Create a span for the top-level runner.
44pub fn runner_span(root_agent: &str) -> tracing::Span {
45    tracing::info_span!("gemini.agent.runner", root_agent = root_agent)
46}
47
48/// Create a span for an LLM generate call.
49pub fn call_llm_span(model_id: &str, agent_name: &str, session_id: &str) -> tracing::Span {
50    tracing::info_span!(
51        "gemini.agent.call_llm",
52        model_id = model_id,
53        agent_name = agent_name,
54        session_id = session_id,
55    )
56}
57
58/// Create a span for a full invocation (top-level).
59pub fn invocation_span(invocation_id: &str, root_agent: &str) -> tracing::Span {
60    tracing::info_span!(
61        "gemini.agent.invocation",
62        invocation_id = invocation_id,
63        root_agent = root_agent,
64    )
65}
66
67/// Create a span for a phase transition.
68pub fn phase_transition_span(from_phase: &str, to_phase: &str, session_id: &str) -> tracing::Span {
69    tracing::info_span!(
70        "gemini.agent.phase_transition",
71        from_phase = from_phase,
72        to_phase = to_phase,
73        session_id = session_id,
74    )
75}
76
77/// Create a span for an extraction operation.
78pub fn extraction_span(extractor_name: &str, session_id: &str) -> tracing::Span {
79    tracing::info_span!(
80        "gemini.agent.extraction",
81        extractor_name = extractor_name,
82        session_id = session_id,
83    )
84}
85
86// ── OpenTelemetry GenAI semantic conventions ───────────────────────────────
87//
88// Spans named and attributed as the GenAI semantic conventions define them,
89// so any OpenTelemetry backend that understands model calls (latency by
90// model, token cost, tool timelines) reads these without configuration.
91// `otel.name` sets the exported span name; message content is not recorded.
92
93/// The `invoke_agent {agent}` span around one run of a text agent.
94pub fn invoke_agent_span(agent_name: &str) -> tracing::Span {
95    tracing::info_span!(
96        "invoke_agent",
97        otel.name = %format_args!("invoke_agent {agent_name}"),
98        gen_ai.operation.name = "invoke_agent",
99        gen_ai.agent.name = agent_name,
100    )
101}
102
103/// The `chat {model}` span around one model call, with the request's
104/// settings; [`record_chat_response`] adds what came back.
105pub fn chat_span(model: &str, request: &crate::llm::LlmRequest) -> tracing::Span {
106    tracing::info_span!(
107        "chat",
108        otel.name = %format_args!("chat {model}"),
109        gen_ai.operation.name = "chat",
110        gen_ai.request.model = model,
111        gen_ai.request.temperature = request.temperature.map(f64::from),
112        gen_ai.request.top_p = request.top_p.map(f64::from),
113        gen_ai.request.top_k = request.top_k,
114        gen_ai.request.max_tokens = request.max_output_tokens,
115        gen_ai.response.finish_reasons = tracing::field::Empty,
116        gen_ai.usage.input_tokens = tracing::field::Empty,
117        gen_ai.usage.output_tokens = tracing::field::Empty,
118        error.type = tracing::field::Empty,
119    )
120}
121
122/// Record a model call's outcome on its [`chat_span`].
123pub fn record_chat_response(
124    span: &tracing::Span,
125    outcome: Result<&crate::llm::LlmResponse, &crate::llm::LlmError>,
126) {
127    match outcome {
128        Ok(response) => {
129            if let Some(reason) = &response.finish_reason {
130                span.record("gen_ai.response.finish_reasons", reason.as_str());
131            }
132            if let Some(usage) = response.usage {
133                span.record("gen_ai.usage.input_tokens", usage.prompt_tokens);
134                span.record("gen_ai.usage.output_tokens", usage.completion_tokens);
135            }
136        }
137        Err(error) => {
138            let kind = match error.status() {
139                Some(status) => status.to_string(),
140                None => "error".to_string(),
141            };
142            span.record("error.type", kind.as_str());
143        }
144    }
145}
146
147/// The `execute_tool {tool}` span around one tool call.
148pub fn execute_tool_span(tool_name: &str, call_id: Option<&str>) -> tracing::Span {
149    tracing::info_span!(
150        "execute_tool",
151        otel.name = %format_args!("execute_tool {tool_name}"),
152        gen_ai.operation.name = "execute_tool",
153        gen_ai.tool.name = tool_name,
154        gen_ai.tool.call.id = call_id,
155        error.type = tracing::field::Empty,
156    )
157}