diff --git a/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/patch.py b/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/patch.py index 5dd0132e..b10d5de1 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/patch.py +++ b/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/patch.py @@ -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, diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/patch_responses.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/patch_responses.py index 9af7dd8a..e8ebbcbd 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/patch_responses.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/patch_responses.py @@ -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) diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py index 69b2a298..bead7e39 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py @@ -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, diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py index 260e1762..43ba56e0 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py @@ -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", @@ -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 = [] diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py index 668e3dd1..322f0f04 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py @@ -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.""" @@ -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 diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/tests/utils/test_tool_call_wrapper.py b/instrumentation/opentelemetry-instrumentation-google-genai/tests/utils/test_tool_call_wrapper.py index 71dcdd74..37f82153 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/tests/utils/test_tool_call_wrapper.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/tests/utils/test_tool_call_wrapper.py @@ -136,6 +136,7 @@ def somefunction(): "An example tool call function.", ) + # Capture content must be enabled to get arguments @patch.dict( "os.environ", { @@ -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 ) @@ -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): diff --git a/util/opentelemetry-util-genai/.changelog/47.removed b/util/opentelemetry-util-genai/.changelog/47.removed new file mode 100644 index 00000000..4e488a4d --- /dev/null +++ b/util/opentelemetry-util-genai/.changelog/47.removed @@ -0,0 +1 @@ +Remove remaining usages of and references to deprecated functions \ No newline at end of file diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_inference_invocation.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_inference_invocation.py index bcfb6692..e05b21ab 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_inference_invocation.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_inference_invocation.py @@ -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(). """ diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_invocation.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_invocation.py index 36cc3849..f58c3852 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_invocation.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_invocation.py @@ -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__( diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py index d35cad3a..8563685f 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py @@ -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 @@ -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, @@ -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 @@ -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, diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/handler.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/handler.py index 86d4af09..6567241b 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/handler.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/handler.py @@ -73,7 +73,6 @@ is_experimental_mode, ) from opentelemetry.util.genai.version import __version__ -from opentelemetry.util.types import AttributeValue class TelemetryHandler: @@ -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, @@ -208,7 +206,6 @@ def start_tool( self, name: str, *, - arguments: AttributeValue | None = None, tool_call_id: str | None = None, tool_type: str | None = None, tool_description: str | None = None, @@ -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, @@ -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, @@ -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, @@ -352,6 +349,8 @@ 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, @@ -359,7 +358,6 @@ def tool( self._logger, self._completion_hook, name, - arguments=arguments, tool_call_id=tool_call_id, tool_type=tool_type, tool_description=tool_description, diff --git a/util/opentelemetry-util-genai/tests/test_toolcall.py b/util/opentelemetry-util-genai/tests/test_toolcall.py index 0f65e441..2c9a3ac7 100644 --- a/util/opentelemetry-util-genai/tests/test_toolcall.py +++ b/util/opentelemetry-util-genai/tests/test_toolcall.py @@ -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()