Skip to content

Commit 3e7e268

Browse files
adopt OTel semantic conventions for agents and frameworks
Signed-off-by: krisztianfekete <[email protected]>
1 parent ecaa7b4 commit 3e7e268

File tree

2 files changed

+85
-13
lines changed

2 files changed

+85
-13
lines changed

src/google/adk/telemetry.py

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,13 @@ def trace_tool_call(
7070
function_response_event: The event with the function response details.
7171
"""
7272
span = trace.get_current_span()
73-
span.set_attribute('gen_ai.system', 'gcp.vertex.agent')
73+
74+
# Standard OpenTelemetry GenAI attributes as of SemConv 1.36.0 for Agents and Frameworks
75+
span.set_attribute('gen_ai.system', 'gcp.vertex_ai')
7476
span.set_attribute('gen_ai.operation.name', 'execute_tool')
7577
span.set_attribute('gen_ai.tool.name', tool.name)
7678
span.set_attribute('gen_ai.tool.description', tool.description)
79+
7780
tool_call_id = '<not specified>'
7881
tool_response = '<not specified>'
7982
if function_response_event.content.parts:
@@ -86,6 +89,7 @@ def trace_tool_call(
8689

8790
span.set_attribute('gen_ai.tool.call.id', tool_call_id)
8891

92+
# Vendor-specific attributes (moved from gen_ai.* to gcp.vertex.agent.*)
8993
if not isinstance(tool_response, dict):
9094
tool_response = {'result': tool_response}
9195
span.set_attribute(
@@ -121,12 +125,15 @@ def trace_merged_tool_calls(
121125
"""
122126

123127
span = trace.get_current_span()
124-
span.set_attribute('gen_ai.system', 'gcp.vertex.agent')
128+
129+
# Standard OpenTelemetry GenAI attributes
130+
span.set_attribute('gen_ai.system', 'gcp.vertex_ai')
125131
span.set_attribute('gen_ai.operation.name', 'execute_tool')
126132
span.set_attribute('gen_ai.tool.name', '(merged tools)')
127133
span.set_attribute('gen_ai.tool.description', '(merged tools)')
128134
span.set_attribute('gen_ai.tool.call.id', response_event_id)
129135

136+
# Vendor-specific attributes
130137
span.set_attribute('gcp.vertex.agent.tool_call_args', 'N/A')
131138
span.set_attribute('gcp.vertex.agent.event_id', response_event_id)
132139
try:
@@ -167,23 +174,38 @@ def trace_call_llm(
167174
llm_response: The LLM response object.
168175
"""
169176
span = trace.get_current_span()
170-
# Special standard Open Telemetry GenaI attributes that indicate
171-
# that this is a span related to a Generative AI system.
172-
span.set_attribute('gen_ai.system', 'gcp.vertex.agent')
177+
178+
# Standard OpenTelemetry GenAI attributes
179+
span.set_attribute('gen_ai.system', 'gcp.vertex_ai')
180+
span.set_attribute('gen_ai.operation.name', 'generate_content')
173181
span.set_attribute('gen_ai.request.model', llm_request.model)
182+
183+
if hasattr(llm_response, 'id') and llm_response.id:
184+
span.set_attribute('gen_ai.response.id', llm_response.id)
185+
186+
# Set response model if different from request model
187+
if (
188+
hasattr(llm_response, 'model')
189+
and llm_response.model
190+
and llm_response.model != llm_request.model
191+
):
192+
span.set_attribute('gen_ai.response.model', llm_response.model)
193+
174194
span.set_attribute(
175195
'gcp.vertex.agent.invocation_id', invocation_context.invocation_id
176196
)
177197
span.set_attribute(
178198
'gcp.vertex.agent.session_id', invocation_context.session.id
179199
)
180200
span.set_attribute('gcp.vertex.agent.event_id', event_id)
201+
181202
# Consider removing once GenAI SDK provides a way to record this info.
182203
span.set_attribute(
183204
'gcp.vertex.agent.llm_request',
184205
_safe_json_serialize(_build_llm_request_for_trace(llm_request)),
185206
)
186-
# Consider removing once GenAI SDK provides a way to record this info.
207+
208+
# Standard GenAI request attributes
187209
if llm_request.config:
188210
if llm_request.config.top_p:
189211
span.set_attribute(
@@ -195,6 +217,14 @@ def trace_call_llm(
195217
'gen_ai.request.max_tokens',
196218
llm_request.config.max_output_tokens,
197219
)
220+
if (
221+
hasattr(llm_request.config, 'temperature')
222+
and llm_request.config.temperature is not None
223+
):
224+
span.set_attribute(
225+
'gen_ai.request.temperature',
226+
llm_request.config.temperature,
227+
)
198228

199229
try:
200230
llm_response_json = llm_response.model_dump_json(exclude_none=True)
@@ -206,6 +236,7 @@ def trace_call_llm(
206236
llm_response_json,
207237
)
208238

239+
# Standard GenAI usage and response attributes
209240
if llm_response.usage_metadata is not None:
210241
span.set_attribute(
211242
'gen_ai.usage.input_tokens',
@@ -239,6 +270,8 @@ def trace_send_data(
239270
data: A list of content objects.
240271
"""
241272
span = trace.get_current_span()
273+
274+
# Vendor-specific attributes (moved from gen_ai.* to gcp.vertex.agent.*)
242275
span.set_attribute(
243276
'gcp.vertex.agent.invocation_id', invocation_context.invocation_id
244277
)
@@ -286,3 +319,41 @@ def _build_llm_request_for_trace(llm_request: LlmRequest) -> dict[str, Any]:
286319
)
287320
)
288321
return result
322+
323+
324+
def _create_span_name(operation_name: str, model_name: str) -> str:
325+
"""Creates a span name following OpenTelemetry GenAI conventions.
326+
327+
Args:
328+
operation_name: The GenAI operation name (e.g., 'generate_content', 'execute_tool').
329+
model_name: The model name being used.
330+
331+
Returns:
332+
A span name in the format '{operation_name} {model_name}'.
333+
"""
334+
return f'{operation_name} {model_name}'
335+
336+
337+
def add_genai_prompt_event(span: trace.Span, prompt_content: str):
338+
"""Adds a GenAI prompt event to the span following OpenTelemetry conventions.
339+
340+
Args:
341+
span: The OpenTelemetry span to add the event to.
342+
prompt_content: The prompt content as a JSON string.
343+
"""
344+
span.add_event(
345+
name='gen_ai.content.prompt', attributes={'gen_ai.prompt': prompt_content}
346+
)
347+
348+
349+
def add_genai_completion_event(span: trace.Span, completion_content: str):
350+
"""Adds a GenAI completion event to the span following OpenTelemetry conventions.
351+
352+
Args:
353+
span: The OpenTelemetry span to add the event to.
354+
completion_content: The completion content as a JSON string.
355+
"""
356+
span.add_event(
357+
name='gen_ai.content.completion',
358+
attributes={'gen_ai.completion': completion_content},
359+
)

tests/unittests/test_telemetry.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,15 @@ async def test_trace_call_llm(monkeypatch, mock_span_fixture):
114114
trace_call_llm(invocation_context, 'test_event_id', llm_request, llm_response)
115115

116116
expected_calls = [
117-
mock.call('gen_ai.system', 'gcp.vertex.agent'),
117+
mock.call('gen_ai.system', 'gcp.vertex_ai'),
118+
mock.call('gen_ai.operation.name', 'generate_content'),
118119
mock.call('gen_ai.request.top_p', 0.95),
119120
mock.call('gen_ai.request.max_tokens', 1024),
120121
mock.call('gen_ai.usage.input_tokens', 50),
121122
mock.call('gen_ai.usage.output_tokens', 50),
122123
mock.call('gen_ai.response.finish_reasons', ['stop']),
123124
]
124-
assert mock_span_fixture.set_attribute.call_count == 12
125+
assert mock_span_fixture.set_attribute.call_count == 13
125126
mock_span_fixture.set_attribute.assert_has_calls(
126127
expected_calls, any_order=True
127128
)
@@ -173,9 +174,9 @@ async def test_trace_call_llm_with_binary_content(
173174

174175
# Verify basic telemetry attributes are set
175176
expected_calls = [
176-
mock.call('gen_ai.system', 'gcp.vertex.agent'),
177+
mock.call('gen_ai.system', 'gcp.vertex_ai'),
177178
]
178-
assert mock_span_fixture.set_attribute.call_count == 7
179+
assert mock_span_fixture.set_attribute.call_count == 8
179180
mock_span_fixture.set_attribute.assert_has_calls(expected_calls)
180181

181182
# Verify binary content is replaced with '<not serializable>' in JSON
@@ -230,7 +231,7 @@ def test_trace_tool_call_with_scalar_response(
230231
# Assert
231232
assert mock_span_fixture.set_attribute.call_count == 10
232233
expected_calls = [
233-
mock.call('gen_ai.system', 'gcp.vertex.agent'),
234+
mock.call('gen_ai.system', 'gcp.vertex_ai'),
234235
mock.call('gen_ai.operation.name', 'execute_tool'),
235236
mock.call('gen_ai.tool.name', mock_tool_fixture.name),
236237
mock.call('gen_ai.tool.description', mock_tool_fixture.description),
@@ -289,7 +290,7 @@ def test_trace_tool_call_with_dict_response(
289290

290291
# Assert
291292
expected_calls = [
292-
mock.call('gen_ai.system', 'gcp.vertex.agent'),
293+
mock.call('gen_ai.system', 'gcp.vertex_ai'),
293294
mock.call('gen_ai.operation.name', 'execute_tool'),
294295
mock.call('gen_ai.tool.name', mock_tool_fixture.name),
295296
mock.call('gen_ai.tool.description', mock_tool_fixture.description),
@@ -328,7 +329,7 @@ def test_trace_merged_tool_calls_sets_correct_attributes(
328329
)
329330

330331
expected_calls = [
331-
mock.call('gen_ai.system', 'gcp.vertex.agent'),
332+
mock.call('gen_ai.system', 'gcp.vertex_ai'),
332333
mock.call('gen_ai.operation.name', 'execute_tool'),
333334
mock.call('gen_ai.tool.name', '(merged tools)'),
334335
mock.call('gen_ai.tool.description', '(merged tools)'),

0 commit comments

Comments
 (0)