Skip to content

Commit 7969654

Browse files
committed
Fix merge conflicts, update tool start/stop/fail to match api
1 parent e1bc9a2 commit 7969654

3 files changed

Lines changed: 36 additions & 162 deletions

File tree

util/opentelemetry-util-genai/src/opentelemetry/util/genai/handler.py

Lines changed: 23 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@
5656
5757
# Or, manage the lifecycle manually
5858
tool = ToolCall(name="get_weather", arguments={"location": "Paris"})
59-
handler.start_tool_call(tool)
59+
handler.start(tool)
6060
tool.tool_result = {"temp": 20}
61-
handler.stop_tool_call(tool)
61+
handler.stop(tool)
6262
"""
6363

6464
from __future__ import annotations
@@ -87,10 +87,10 @@
8787
_apply_embedding_finish_attributes,
8888
_apply_error_attributes,
8989
_apply_llm_finish_attributes,
90-
_get_embedding_span_name,
91-
_get_llm_span_name,
9290
_apply_tool_call_attributes,
9391
_finish_tool_call_span,
92+
_get_embedding_span_name,
93+
_get_llm_span_name,
9494
_get_tool_call_span_name,
9595
_maybe_emit_llm_event,
9696
)
@@ -166,17 +166,25 @@ def _record_embedding_metrics(
166166

167167
def _start(self, invocation: _T) -> _T:
168168
"""Start a GenAI invocation and create a pending span entry."""
169+
span_kind = SpanKind.CLIENT
169170
if isinstance(invocation, LLMInvocation):
170171
span_name = _get_llm_span_name(invocation)
171172
elif isinstance(invocation, EmbeddingInvocation):
172173
span_name = _get_embedding_span_name(invocation)
174+
elif isinstance(invocation, ToolCall):
175+
span_name = _get_tool_call_span_name(invocation)
176+
span_kind = SpanKind.INTERNAL
173177
else:
174178
span_name = ""
179+
175180
span = self._tracer.start_span(
176181
name=span_name,
177-
kind=SpanKind.CLIENT,
182+
kind=span_kind,
178183
)
179-
# Record a monotonic start timestamp (seconds) for duration
184+
if isinstance(invocation, ToolCall):
185+
_apply_tool_call_attributes(
186+
span, invocation, capture_content=False
187+
)
180188
# calculation using timeit.default_timer.
181189
invocation.monotonic_start_s = timeit.default_timer()
182190
invocation.span = span
@@ -200,6 +208,8 @@ def _stop(self, invocation: _T) -> _T:
200208
elif isinstance(invocation, EmbeddingInvocation):
201209
_apply_embedding_finish_attributes(span, invocation)
202210
self._record_embedding_metrics(invocation, span)
211+
elif isinstance(invocation, ToolCall):
212+
_finish_tool_call_span(span, invocation, capture_content=True)
203213
finally:
204214
# Detach context and end span even if finishing fails
205215
otel_context.detach(invocation.context_token)
@@ -230,6 +240,10 @@ def _fail(self, invocation: _T, error: Error) -> _T:
230240
self._record_embedding_metrics(
231241
invocation, span, error_type=error_type
232242
)
243+
elif isinstance(invocation, ToolCall):
244+
invocation.error_type = error_type
245+
_finish_tool_call_span(span, invocation, capture_content=True)
246+
span.set_status(Status(StatusCode.ERROR, error.message))
233247
finally:
234248
# Detach context and end span even if finishing fails
235249
otel_context.detach(invocation.context_token)
@@ -266,105 +280,6 @@ def fail_llm(
266280
"""Fail an LLM invocation and end its span with error status."""
267281
return self._fail(invocation, error)
268282

269-
def start_tool_call(
270-
self,
271-
tool_call: ToolCall,
272-
) -> ToolCall:
273-
"""Start a tool call execution and create a span.
274-
275-
Creates an execute_tool span per span.gen_ai.execute_tool.internal spec:
276-
- Span kind: INTERNAL
277-
- Span name: "execute_tool {tool_name}"
278-
- Required attribute: gen_ai.operation.name = "execute_tool"
279-
280-
Args:
281-
tool_call: ToolCall instance to track
282-
283-
Returns:
284-
The same ToolCall with span and context_token set
285-
"""
286-
# Create span with INTERNAL kind per spec
287-
span = self._tracer.start_span(
288-
name=_get_tool_call_span_name(tool_call),
289-
kind=SpanKind.INTERNAL,
290-
)
291-
292-
# Apply initial attributes (but not result yet)
293-
# capture_content=False for start, only structure attributes
294-
_apply_tool_call_attributes(span, tool_call, capture_content=False)
295-
296-
# Record monotonic start time for duration calculation
297-
tool_call.monotonic_start_s = timeit.default_timer()
298-
299-
# Attach to context
300-
tool_call.span = span
301-
tool_call.context_token = otel_context.attach(
302-
set_span_in_context(span)
303-
)
304-
305-
return tool_call
306-
307-
def stop_tool_call(self, tool_call: ToolCall) -> ToolCall: # pylint: disable=no-self-use
308-
"""Finalize a tool call execution successfully.
309-
310-
Applies final attributes including tool_result, sets OK status, and ends span.
311-
312-
Args:
313-
tool_call: ToolCall instance with span to finalize
314-
315-
Returns:
316-
The same ToolCall
317-
"""
318-
if tool_call.context_token is None or tool_call.span is None:
319-
# TODO: Provide feedback that this invocation was not started
320-
return tool_call
321-
322-
span = tool_call.span
323-
324-
# Finalize span with result (capture_content=True allows result if mode permits)
325-
_finish_tool_call_span(span, tool_call, capture_content=True)
326-
327-
# Detach context and end span
328-
otel_context.detach(tool_call.context_token)
329-
span.end()
330-
331-
return tool_call
332-
333-
def fail_tool_call( # pylint: disable=no-self-use
334-
self, tool_call: ToolCall, error: Error
335-
) -> ToolCall:
336-
"""Fail a tool call execution with error.
337-
338-
Sets error attributes, ERROR status, and ends span.
339-
340-
Args:
341-
tool_call: ToolCall instance with span to fail
342-
error: Error details
343-
344-
Returns:
345-
The same ToolCall
346-
"""
347-
if tool_call.context_token is None or tool_call.span is None:
348-
# TODO: Provide feedback that this invocation was not started
349-
return tool_call
350-
351-
span = tool_call.span
352-
353-
# Set error_type on tool_call so it's included in attributes
354-
tool_call.error_type = error.type.__qualname__
355-
356-
# Finalize span with error
357-
_finish_tool_call_span(span, tool_call, capture_content=True)
358-
359-
# Apply additional error status with message
360-
span.set_status(Status(StatusCode.ERROR, error.message))
361-
362-
# Detach context and end span
363-
otel_context.detach(tool_call.context_token)
364-
span.end()
365-
366-
return tool_call
367-
368283
@contextmanager
369284
def tool_call(
370285
self, tool_call: ToolCall | None = None
@@ -388,15 +303,13 @@ def tool_call(
388303
arguments={},
389304
id=None,
390305
)
391-
self.start_tool_call(tool_call)
306+
self.start(tool_call)
392307
try:
393308
yield tool_call
394309
except Exception as exc:
395-
self.fail_tool_call(
396-
tool_call, Error(message=str(exc), type=type(exc))
397-
)
310+
self.fail(tool_call, Error(message=str(exc), type=type(exc)))
398311
raise
399-
self.stop_tool_call(tool_call)
312+
self.stop(tool_call)
400313

401314
@contextmanager
402315
def llm(

util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -69,45 +69,6 @@ class ToolCallRequest:
6969
type: Literal["tool_call"] = "tool_call"
7070

7171

72-
@dataclass()
73-
class ToolCall(ToolCallRequest):
74-
"""Represents a tool call for execution tracking with spans and metrics.
75-
76-
This type extends ToolCallRequest with additional fields for tracking tool execution
77-
per the execute_tool span semantic conventions.
78-
79-
Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md#execute-tool-span
80-
81-
For simple message parts (tool calls requested by the model), consider using
82-
ToolCallRequest instead to avoid unnecessary execution-tracking fields.
83-
84-
Semantic convention attributes for execute_tool spans:
85-
- gen_ai.operation.name: "execute_tool" (Required)
86-
- gen_ai.tool.name: Name of the tool (Recommended)
87-
- gen_ai.tool.call.id: Tool call identifier (Recommended if available)
88-
- gen_ai.tool.type: Type classification - "function", "extension", or "datastore" (Recommended if available)
89-
- gen_ai.tool.description: Tool description (Recommended if available)
90-
- gen_ai.tool.call.arguments: Parameters passed to tool (Opt-In, may contain sensitive data)
91-
- gen_ai.tool.call.result: Result returned by tool (Opt-In, may contain sensitive data)
92-
- error.type: Error type if operation failed (Conditionally Required)
93-
"""
94-
95-
# Execution-only fields (used for execute_tool spans):
96-
# gen_ai.tool.type - Tool type: "function", "extension", or "datastore"
97-
tool_type: str | None = None
98-
# gen_ai.tool.description - Description of what the tool does
99-
tool_description: str | None = None
100-
# gen_ai.tool.call.result - Result returned by the tool (Opt-In, may contain sensitive data)
101-
tool_result: Any = None
102-
# error.type - Error type if the tool call failed
103-
error_type: str | None = None
104-
105-
# Lifecycle tracking fields (used by TelemetryHandler):
106-
context_token: ContextToken | None = None
107-
span: Span | None = None
108-
monotonic_start_s: float | None = None
109-
110-
11172
@dataclass()
11273
class ToolCallResponse:
11374
"""Represents a tool call result sent to the model or a built-in tool call outcome and details

util/opentelemetry-util-genai/tests/test_handler_metrics.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -213,13 +213,13 @@ def setUp(self) -> None:
213213
self.handler = TelemetryHandler(tracer_provider=self.tracer_provider)
214214

215215
def test_start_tool_call_creates_span(self):
216-
"""Test start_tool_call creates span with correct name and kind"""
216+
"""Test start creates span with correct name and kind for ToolCall"""
217217
tool = ToolCall(
218218
name="get_weather",
219219
arguments={"location": "Paris"},
220220
id="call_123",
221221
)
222-
self.handler.start_tool_call(tool)
222+
self.handler.start(tool)
223223

224224
spans = self.span_exporter.get_finished_spans()
225225
self.assertEqual(len(spans), 0) # Span not finished yet
@@ -231,17 +231,17 @@ def test_start_tool_call_creates_span(self):
231231
self.assertEqual(tool.span.kind, SpanKind.INTERNAL)
232232

233233
def test_stop_tool_call_ends_span(self):
234-
"""Test stop_tool_call ends span successfully"""
234+
"""Test stop ends span successfully for ToolCall"""
235235
tool = ToolCall(
236236
name="get_weather",
237237
arguments={"location": "Paris"},
238238
id="call_123",
239239
tool_type="function",
240240
tool_description="Get current weather",
241241
)
242-
self.handler.start_tool_call(tool)
242+
self.handler.start(tool)
243243
tool.tool_result = {"temp": 20, "condition": "sunny"}
244-
self.handler.stop_tool_call(tool)
244+
self.handler.stop(tool)
245245

246246
spans = self.span_exporter.get_finished_spans()
247247
self.assertEqual(len(spans), 1)
@@ -272,21 +272,21 @@ def test_stop_tool_call_ends_span(self):
272272
self.assertEqual(span.status.status_code, StatusCode.OK)
273273

274274
def test_stop_tool_call_without_start(self):
275-
"""Test stop_tool_call without prior start is a no-op"""
275+
"""Test stop without prior start is a no-op for ToolCall"""
276276
tool = ToolCall(name="test", arguments={}, id=None)
277-
# Don't call start_tool_call
278-
self.handler.stop_tool_call(tool)
277+
# Don't call start
278+
self.handler.stop(tool)
279279

280280
spans = self.span_exporter.get_finished_spans()
281281
self.assertEqual(len(spans), 0)
282282

283283
def test_fail_tool_call_sets_error_status(self):
284-
"""Test fail_tool_call sets error status and attributes"""
284+
"""Test fail sets error status and attributes for ToolCall"""
285285
tool = ToolCall(name="failing_tool", arguments={}, id="call_456")
286-
self.handler.start_tool_call(tool)
286+
self.handler.start(tool)
287287

288288
error = Error(message="Tool execution failed", type=ValueError)
289-
self.handler.fail_tool_call(tool, error)
289+
self.handler.fail(tool, error)
290290

291291
spans = self.span_exporter.get_finished_spans()
292292
self.assertEqual(len(spans), 1)
@@ -316,9 +316,9 @@ def test_tool_call_with_content_capture(self):
316316
arguments={"location": "Paris"},
317317
id="call_123",
318318
)
319-
self.handler.start_tool_call(tool)
319+
self.handler.start(tool)
320320
tool.tool_result = {"temp": 20, "condition": "sunny"}
321-
self.handler.stop_tool_call(tool)
321+
self.handler.stop(tool)
322322

323323
spans = self.span_exporter.get_finished_spans()
324324
span = spans[0]

0 commit comments

Comments
 (0)