Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
11f21f0
Migrate google GenAi instrumentation to use the GenAi utils package
DylanRussell May 13, 2026
e82a658
Apply fixes
DylanRussell May 13, 2026
b051cd9
Upade changelog and add assertion to test..
DylanRussell May 13, 2026
db1261d
Fix tests, tool argument / result serialization
DylanRussell May 14, 2026
e881322
Remove some code moved to utils
DylanRussell May 15, 2026
497a318
Add comments explaining serialization
DylanRussell May 15, 2026
e3545cc
Merge branch 'main' into google_to_utils
DylanRussell May 19, 2026
1b63130
Fix tests and merge main
DylanRussell May 19, 2026
d2dd2c1
update changelog
DylanRussell May 19, 2026
87615c7
Fix broken CI
DylanRussell May 19, 2026
0630f6a
Remove all deprecated functions in genai utils. Remove last usages of…
DylanRussell May 19, 2026
763ca1c
Update some docstring comments
DylanRussell May 19, 2026
aef4b90
Merge branch 'main' into remove_old_funcs
DylanRussell May 19, 2026
3b8cbcc
Clean up google genai code a bit..
DylanRussell May 20, 2026
d82e359
Merge branch 'main' into remove_old_funcs
DylanRussell May 20, 2026
262e379
Add changelog
DylanRussell May 20, 2026
3b520cc
Merge branch 'main' into remove_old_funcs
DylanRussell May 21, 2026
2c53174
Remove test deps..
DylanRussell May 21, 2026
3d87f0e
Add deleted functions back
DylanRussell May 21, 2026
4cca88c
Merge branch 'main' into remove_old_funcs
DylanRussell May 21, 2026
b7fb963
Respond to comments
DylanRussell May 21, 2026
c6c1ba7
Fix broken test..
DylanRussell May 21, 2026
e8ed154
Merge branch 'main' into remove_old_funcs
DylanRussell May 26, 2026
893d079
Delete duplicated funcs..
DylanRussell May 26, 2026
634aadf
Fix more docstrings
DylanRussell May 26, 2026
3e9e4da
Merge branch 'main' into remove_old_funcs
DylanRussell May 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def _create_invocation(
)

server_address, server_port = get_server_address_and_port(instance)
invocation = handler.start_inference(
invocation = handler.inference(
provider=ANTHROPIC,
request_model=request_model,
server_address=server_address,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def responses_create(handler: TelemetryHandler):

def traced_method(wrapped, instance, args, kwargs):
params = extract_params(**kwargs)
invocation = handler.start_inference(
invocation = handler.inference(
**get_inference_creation_kwargs(params, instance)
)
apply_request_attributes(invocation, params, capture_content)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ def create_chat_invocation(
# pylint: disable=too-many-branches

address, port = get_server_address_and_port(client_instance)
invocation = handler.start_inference(
invocation = handler.inference(
GenAIAttributes.GenAiProviderNameValues.OPENAI.value,
request_model=kwargs.get("model", ""),
server_address=address if address else None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1152,7 +1152,7 @@ async def instrumented_generate_content_stream(
)
)
if helper.experimental_sem_convs_enabled:
invocation = telemetry_handler.start_inference(
invocation = telemetry_handler.inference(
provider=helper._genai_system,
request_model=model,
operation_name="generate_content",
Expand All @@ -1161,13 +1161,14 @@ async def instrumented_generate_content_stream(
invocation.tool_definitions = (
await helper._maybe_get_tool_definitions_async(config)
)
invocation.input_messages = to_input_messages(
contents=transformers.t_contents(contents)
)
if system_content := _config_to_system_instruction(config):
invocation.system_instruction = to_system_instructions(
content=transformers.t_contents(system_content)[0]
if helper._content_recording_enabled:
invocation.input_messages = to_input_messages(
contents=transformers.t_contents(contents)
)
if system_content := _config_to_system_instruction(config):
invocation.system_instruction = to_system_instructions(
content=transformers.t_contents(system_content)[0]
)

async def _response_async_generator_wrapper():
candidates = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ def _to_otel_value(python_value):
return repr(python_value)


# There is non canonical way to serialize a Python object to a span attribute value.
# Span attribute values currently most be one of the primitive types, or a homogeneous list of primitive types.
# In the future the value will be expanded to include None, a heterogeneous lists of primitive types, and a Map of these types.
# There is no canonical way to serialize a Python object to a span attribute value.
# Span attribute values currently must be one of the primitive types, or a homogeneous list of primitive types.
# In the future the value will be expanded to include None, heterogeneous lists of primitive types, and a Map of these types.
# See https://github.com/open-telemetry/opentelemetry-specification/pull/4485
def _get_function_args(wrapped_function, function_args, function_kwargs):
"""Records the details about a function invocation as span attributes."""
Expand Down Expand Up @@ -75,34 +75,42 @@ def _wrap_tool_function(

@functools.wraps(tool_function)
async def wrapped_function(*args, **kwargs):
# Always json.dumps. First we convert args / result to something that we can serialize, then we serialize.
# The return value of _to_otel_value could be a dict, which currently cannot be a span attribute..
# In the future that could change (see https://github.com/open-telemetry/opentelemetry-specification/pull/4485), and we could possibly stop using json.dumps here.
with telemetry_handler.tool(
tool_function.__name__, tool_description=tool_function.__doc__
tool_function.__name__,
tool_description=tool_function.__doc__,
) as tool_invocation:
# Do this before calling the tool in case that crashes.
if tool_invocation.should_capture_content_on_span:
tool_invocation.arguments = json.dumps(
_get_function_args(tool_function, args, kwargs)
)
result = await tool_function(*args, **kwargs)
# Always json.dumps. First we convert args / result to something that we can serialize, then we serialize.
# The return value of _to_otel_value could be a dict, which currently cannot be a span attribute..
# In the future that could change (see https://github.com/open-telemetry/opentelemetry-specification/pull/4485), and we could possibly stop using json.dumps here.
tool_invocation.arguments = json.dumps(
_get_function_args(tool_function, args, kwargs)
)
tool_invocation.tool_result = json.dumps(
_to_otel_value(result)
)
if tool_invocation.should_capture_content_on_span:
tool_invocation.tool_result = json.dumps(
_to_otel_value(result)
)
return result
else:

@functools.wraps(tool_function)
def wrapped_function(*args, **kwargs):
with telemetry_handler.tool(
tool_function.__name__, tool_description=tool_function.__doc__
tool_function.__name__,
tool_description=tool_function.__doc__,
) as tool_invocation:
# Do this before calling the tool in case that crashes.
if tool_invocation.should_capture_content_on_span:
tool_invocation.arguments = json.dumps(
_get_function_args(tool_function, args, kwargs)
)
result = tool_function(*args, **kwargs)
tool_invocation.arguments = json.dumps(
_get_function_args(tool_function, args, kwargs)
)
tool_invocation.tool_result = json.dumps(
_to_otel_value(result)
)
if tool_invocation.should_capture_content_on_span:
tool_invocation.tool_result = json.dumps(
_to_otel_value(result)
)
return result

return wrapped_function
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def somefunction():
"An example tool call function.",
)

# Capture content must be enabled to get arguments
@patch.dict(
"os.environ",
{
Expand Down Expand Up @@ -165,6 +166,7 @@ def somefunction(
self.assertEqual(
arguments["code.function.parameters.primitive_int.type"], "int"
)
self.assertEqual(span.attributes["gen_ai.tool.name"], "somefunction")
self.assertEqual(
arguments["code.function.parameters.primitive_int.value"], 12345
)
Expand All @@ -190,54 +192,32 @@ def somefunction(
[123, "abc"],
)

def test_handle_with_different_capture_content_on_span_config(self):
@patch.dict(
"os.environ",
{
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "false",
"OTEL_SEMCONV_STABILITY_OPT_IN": "default",
},
)
def test_with_capture_content_disabled(self):
_OpenTelemetrySemanticConventionStability._initialized = False
_OpenTelemetrySemanticConventionStability._initialize()

def somefunction(arg=None):
return arg

for capture_content_on_span in ["SPAN_AND_EVENT", "NO_CONTENT"]:
patched_environ = patch.dict(
"os.environ",
{
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": capture_content_on_span,
"OTEL_SEMCONV_STABILITY_OPT_IN": "gen_ai_latest_experimental",
},
)
with patched_environ:
_OpenTelemetrySemanticConventionStability._initialized = False
_OpenTelemetrySemanticConventionStability._initialize()
self.setUp()
wrapped_somefunction = self.wrap(somefunction)
wrapped_somefunction("a string value")
span = self.otel.get_span_named("execute_tool somefunction")
self.assertEqual(
span.attributes["gen_ai.tool.name"], "somefunction"
)

if capture_content_on_span == "NO_CONTENT":
self.assertNotIn(
"gen_ai.tool.call.arguments",
span.attributes,
)
self.assertNotIn(
"gen_ai.tool.call.result",
span.attributes,
)
else:
self.assertEqual(
span.attributes["gen_ai.tool.call.result"],
'"a string value"',
)
arguments = json.loads(
span.attributes["gen_ai.tool.call.arguments"]
)
self.assertEqual(
arguments["code.function.parameters.arg.type"], "str"
)
self.assertEqual(
arguments["code.function.parameters.arg.value"],
"a string value",
)
self.tearDown()
wrapped_somefunction = self.wrap(somefunction)
wrapped_somefunction("a string value")
span = self.otel.get_span_named("execute_tool somefunction")

self.assertNotIn(
"gen_ai.tool.call.arguments",
span.attributes,
)
self.assertNotIn(
"gen_ai.tool.call.result",
span.attributes,
)

def test_function_that_throws_exception(self):
def somefunction(arg=None):
Expand Down
1 change: 1 addition & 0 deletions util/opentelemetry-util-genai/.changelog/47.removed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Remove remaining usages of and references to deprecated functions
Comment thread
DylanRussell marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def _maybe_create_event(self) -> LogRecord | None:
class LLMInvocation:
"""Deprecated. Use InferenceInvocation instead.

Data container for an LLM invocation. Pass to handler.start_llm() to start
Data container for an LLM invocation. Pass to handler.llm() to start
the span, then update fields and call handler.stop_llm() or handler.fail_llm().
"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ class GenAIInvocation(AbstractContextManager["GenAIInvocation"]):
Base class for all GenAI invocation types. Manages the lifecycle of a single
GenAI operation (LLM call, embedding, tool execution, workflow, etc.).

Use the factory methods on TelemetryHandler (start_inference, start_embedding,
start_workflow, start_tool) rather than constructing invocations directly.
Use the factory methods on TelemetryHandler (inference, embedding,
workflow, tool) rather than constructing invocations directly.
"""

def __init__(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class ToolInvocation(GenAIInvocation):

Not used as a message part — use ToolCallRequest for that purpose.

Use handler.start_tool(name) rather than constructing this directly.
Use handler.tool(name) rather than constructing this directly.

Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md#execute-tool-span

Expand All @@ -47,13 +47,11 @@ def __init__(
completion_hook: CompletionHook,
name: str,
*,
arguments: AttributeValue | None = None,
tool_call_id: str | None = None,
tool_type: str | None = None,
tool_description: str | None = None,
tool_result: AttributeValue | None = None,
) -> None:
"""Use handler.start_tool(name) or handler.tool(name) instead of calling this directly."""
"""Use handler.tool(name) instead of calling this directly."""
_operation_name = GenAI.GenAiOperationNameValues.EXECUTE_TOOL.value
super().__init__(
tracer,
Expand All @@ -65,8 +63,12 @@ def __init__(
)
self.should_capture_content_on_span = should_capture_content_on_spans()
self.name = name
self.tool_result = tool_result
self.arguments = arguments
self.tool_result: AttributeValue | None = None
# Since arguments and tool_result can be expensive to serialize,
# it's recommended to check the content capture flag in the
# instrumentation library before assigning these attributes
# to the invocation.
self.arguments: AttributeValue | None = None
self.tool_call_id = tool_call_id
self.tool_type = tool_type
self.tool_description = tool_description
Expand All @@ -79,18 +81,6 @@ def _get_base_attributes(self) -> dict[str, Any]:
(GenAI.GEN_AI_TOOL_CALL_ID, self.tool_call_id),
(GenAI.GEN_AI_TOOL_TYPE, self.tool_type),
(GenAI.GEN_AI_TOOL_DESCRIPTION, self.tool_description),
(
GenAI.GEN_AI_TOOL_CALL_ARGUMENTS,
self.arguments
if self.should_capture_content_on_span
else None,
),
(
GenAI.GEN_AI_TOOL_CALL_RESULT,
self.tool_result
if self.should_capture_content_on_span
else None,
),
)
return {
GenAI.GEN_AI_OPERATION_NAME: self._operation_name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
is_experimental_mode,
)
from opentelemetry.util.genai.version import __version__
from opentelemetry.util.types import AttributeValue


class TelemetryHandler:
Expand Down Expand Up @@ -133,7 +132,6 @@ def should_capture_content(self) -> bool:
return self._capture_content

# New-style factory methods: construct + start in one call, handler stored on invocation

def start_inference(
self,
provider: str,
Expand Down Expand Up @@ -208,7 +206,6 @@ def start_tool(
self,
name: str,
*,
arguments: AttributeValue | None = None,
Comment thread
DylanRussell marked this conversation as resolved.
tool_call_id: str | None = None,
tool_type: str | None = None,
tool_description: str | None = None,
Expand All @@ -227,7 +224,6 @@ def start_tool(
self._logger,
self._completion_hook,
name,
arguments=arguments,
tool_call_id=tool_call_id,
tool_type=tool_type,
tool_description=tool_description,
Expand Down Expand Up @@ -280,6 +276,8 @@ def fail_llm( # pylint: disable=no-self-use
invocation._inference_invocation.fail(error)
return invocation

# New-style factory methods: construct + start in one call, handler stored on invocation

def inference(
self,
provider: str,
Expand Down Expand Up @@ -340,7 +338,6 @@ def tool(
self,
name: str,
*,
arguments: AttributeValue | None = None,
tool_call_id: str | None = None,
tool_type: str | None = None,
tool_description: str | None = None,
Expand All @@ -352,14 +349,15 @@ def tool(
responsible for calling `stop` or `fail` to finalize the span.

Only set data attributes on the invocation object, do not modify the span or context.
Recommended to set ``invocation.arguments`` and ``invocation.tool_result`` on the
invocation object but only if `invocation.should_capture_content_on_span` is True.
"""
return ToolInvocation(
self._tracer,
self._metrics_recorder,
self._logger,
self._completion_hook,
name,
arguments=arguments,
tool_call_id=tool_call_id,
tool_type=tool_type,
tool_description=tool_description,
Expand Down
3 changes: 2 additions & 1 deletion util/opentelemetry-util-genai/tests/test_toolcall.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ def test_toolcallrequest_is_message_part():
def test_toolcall_inherits_from_genaiinvocation():
"""ToolInvocation inherits from GenAIInvocation for lifecycle management"""
handler = _make_handler()
tc = handler.tool("get_weather", arguments={"city": "Paris"})
tc = handler.tool("get_weather")
tc.arguments = {"city": "Paris"}
assert isinstance(tc, GenAIInvocation)
assert not isinstance(tc, ToolCallRequest)
tc.stop()
Expand Down