diff --git a/.github/instructions/util-genai.instructions.md b/.github/instructions/util-genai.instructions.md index 5863310ff..9fa9f2d37 100644 --- a/.github/instructions/util-genai.instructions.md +++ b/.github/instructions/util-genai.instructions.md @@ -45,16 +45,16 @@ land the semconv change first. ## 4. Invocation shape -- `start_*()` factories must accept all sampling-relevant semconv attributes as parameters. +- Factory methods must accept all sampling-relevant semconv attributes as parameters. Attributes also marked required by semconv must be required parameters (no default value). -- `start_*()` factories must map 1:1 to distinct semconv operation types (inference, embeddings, +- Factory methods must map 1:1 to distinct semconv operation types (inference, embeddings, tool execution, agent invocation, workflow invocation). Names must match the operation - unambiguously — e.g., `create_agent` vs `invoke_agent` are distinct ops; `start_agent()` alone + unambiguously — e.g., `create_agent` vs `invoke_agent` are distinct ops; `agent()` alone is ambiguous. -- Each operation exposes both a factory (`start_inference(...)`) and a context-manager - (`inference(...)`) form. +- Each operation exposes a factory such as `inference(...)`; returned invocations may also be + used as context managers. - Never construct invocation types directly (`InferenceInvocation(...)`) — skips span creation, - silent no-ops. Always use `handler.start_*()` or the context manager. + silent no-ops. Always use the corresponding `handler` factory. ## 5. Exception handling diff --git a/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/test_async_wrappers.py b/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/test_async_wrappers.py index c5c9c86dc..548041c0d 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/test_async_wrappers.py +++ b/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/test_async_wrappers.py @@ -200,7 +200,7 @@ def test_stream_wrapper_finalization_records_thinking_tokens( "opentelemetry.instrumentation.genai.anthropic" ), ) - invocation = handler.start_inference( + invocation = handler.inference( provider="anthropic", request_model="claude-sonnet-4-20250514", ) diff --git a/util/opentelemetry-util-genai/.changelog/823.removed b/util/opentelemetry-util-genai/.changelog/823.removed new file mode 100644 index 000000000..1beb4b224 --- /dev/null +++ b/util/opentelemetry-util-genai/.changelog/823.removed @@ -0,0 +1 @@ +Remove the deprecated GenAI utility callables ``should_emit_event()``, ``should_capture_content_on_spans()``, ``TelemetryHandler.start_inference()``, ``TelemetryHandler.start_llm()``, ``TelemetryHandler.start_embedding()``, ``TelemetryHandler.start_tool()``, ``TelemetryHandler.start_workflow()``, ``TelemetryHandler.stop_llm()``, ``TelemetryHandler.fail_llm()``, ``TelemetryHandler.start_invoke_local_agent()``, ``TelemetryHandler.start_invoke_remote_agent()``, and ``ToolInvocation.should_capture_content_on_span 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 bcdcc3e25..380741f6f 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 @@ -3,6 +3,8 @@ from __future__ import annotations +import logging +import os from collections.abc import Mapping from dataclasses import dataclass, field from typing import Final @@ -13,7 +15,7 @@ gen_ai_attributes as GenAI, ) from opentelemetry.semconv.attributes import server_attributes -from opentelemetry.trace import INVALID_SPAN, Span, SpanKind, Tracer +from opentelemetry.trace import SpanKind, Tracer from opentelemetry.util.genai._instruments import _Instruments from opentelemetry.util.genai._invocation import ( Error, @@ -21,7 +23,11 @@ get_content_attributes, ) from opentelemetry.util.genai.completion_hook import CompletionHook +from opentelemetry.util.genai.environment_variables import ( + OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, +) from opentelemetry.util.genai.types import ( + ContentCapturingMode, ErrorTypeResolver, InputMessage, MessagePart, @@ -31,12 +37,10 @@ SystemInstructionPart, ToolDefinition, ) -from opentelemetry.util.genai.utils import ( - ContentCapturingMode, - _should_emit_event, -) from opentelemetry.util.types import AttributeValue +_logger = logging.getLogger(__name__) + _GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS: Final = ( "gen_ai.usage.cache_write.input_tokens" ) @@ -78,6 +82,30 @@ _GEN_AI_PROMPT_VERSION: Final = "gen_ai.prompt.version" +def _should_emit_event( + content_capturing_mode: ContentCapturingMode, +) -> bool: + """Check if event emission is enabled.""" + if ( + envvar := os.environ.get(OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, "") + .lower() + .strip() + ): + if envvar == "true": + return True + if envvar == "false": + return False + _logger.warning( + "%s is not a valid option for `%s` environment variable. Must be one of true or false (case-insensitive). Defaulting based on content capturing mode.", + envvar, + OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, + ) + return content_capturing_mode in ( + ContentCapturingMode.EVENT_ONLY, + ContentCapturingMode.SPAN_AND_EVENT, + ) + + class InferenceInvocation(GenAIInvocation): """Represents a single LLM chat/completion call. @@ -457,10 +485,9 @@ def _maybe_create_event(self) -> LogRecord | None: @dataclass class LLMInvocation: - """Deprecated. Use InferenceInvocation instead. + """Deprecated compatibility data container for an LLM invocation. - 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(). + Use ``handler.inference()`` to create an ``InferenceInvocation`` instead. """ request_model: str | None = None @@ -489,82 +516,3 @@ class LLMInvocation: seed: int | None = None server_address: str | None = None server_port: int | None = None - - _inference_invocation: InferenceInvocation | None = field( - default=None, init=False, repr=False - ) - - def _start_with_handler( - self, - tracer: Tracer, - instruments: _Instruments, - logger: Logger, - completion_hook: CompletionHook, - *, - content_capturing_mode: ContentCapturingMode | None = None, - ) -> None: - """Create and start an InferenceInvocation from this data container. Called by handler.start_llm().""" - inv = InferenceInvocation( - tracer, - instruments, - logger, - completion_hook, - self.provider or "", - request_model=self.request_model, - server_address=self.server_address, - server_port=self.server_port, - content_capturing_mode=content_capturing_mode, - ) - inv.input_messages = self.input_messages - inv.output_messages = self.output_messages - inv.system_instruction = self.system_instruction - inv.response_model_name = self.response_model_name - inv.response_id = self.response_id - inv.finish_reasons = self.finish_reasons - inv.input_tokens = self.input_tokens - inv.output_tokens = self.output_tokens - - inv.temperature = self.temperature - inv.top_p = self.top_p - inv.frequency_penalty = self.frequency_penalty - inv.presence_penalty = self.presence_penalty - inv.max_tokens = self.max_tokens - inv.stop_sequences = self.stop_sequences - inv.seed = self.seed - inv.attributes.update(self.attributes) - inv.metric_attributes.update(self.metric_attributes) - self._inference_invocation = inv - - def _sync_to_invocation(self) -> None: - inv = self._inference_invocation - if inv is None: - return - # Start attributes (provider, request_model, server_address, server_port) - # are fixed at construction in _start_with_handler and cannot be reassigned. - inv.input_messages = self.input_messages - inv.output_messages = self.output_messages - inv.system_instruction = self.system_instruction - inv.response_model_name = self.response_model_name - inv.response_id = self.response_id - inv.finish_reasons = self.finish_reasons - inv.input_tokens = self.input_tokens - inv.output_tokens = self.output_tokens - - inv.temperature = self.temperature - inv.top_p = self.top_p - inv.frequency_penalty = self.frequency_penalty - inv.presence_penalty = self.presence_penalty - inv.max_tokens = self.max_tokens - inv.stop_sequences = self.stop_sequences - inv.seed = self.seed - inv.attributes = self.attributes - inv.metric_attributes = self.metric_attributes - - @property - def span(self) -> Span: - """The underlying span, for back-compat with code that checks span.is_recording().""" - return ( - self._inference_invocation.span - if self._inference_invocation is not None - else INVALID_SPAN - ) 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 58988aec7..a903d0143 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 @@ -112,15 +112,6 @@ def __init__( self._tool_type: str | None = tool_type self._agent_name: str | None = agent_name - @property - def should_capture_content_on_span(self) -> bool: - """Returns whether content capture is enabled on spans. - - .. deprecated:: 1.2b0 - Use :attr:`should_capture_content` instead. - """ - return self._should_capture_content_on_span - def _get_metric_attributes(self) -> dict[str, AttributeValue]: attrs: dict[str, AttributeValue] = { GenAI.GEN_AI_TOOL_NAME: self._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 0c6f84de9..08b6271f4 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/handler.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/handler.py @@ -50,16 +50,13 @@ TracerProvider, get_tracer, ) -from opentelemetry.util.genai._inference_invocation import LLMInvocation from opentelemetry.util.genai._instruments import _Instruments -from opentelemetry.util.genai._invocation import Error from opentelemetry.util.genai.completion_hook import ( CompletionHook, _NoOpCompletionHook, _SafeCompletionHook, ) from opentelemetry.util.genai.invocation import ( - AgentInvocation, EmbeddingInvocation, FetchResponseInvocation, InferenceInvocation, @@ -166,94 +163,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, - *, - request_model: str | None = None, - server_address: str | None = None, - server_port: int | None = None, - operation_name: str | None = None, - context: Context | None = None, - _attach_to_context: bool = True, - ) -> InferenceInvocation: - """Create and start an LLM inference invocation. - - .. deprecated:: 1.0b0 - Use ``handler.inference()`` instead. - - Args: - context: An optional OpenTelemetry Context to parent the span. - - Set remaining attributes (input_messages, temperature, etc.) on the - returned invocation, then call invocation.stop() or invocation.fail(). - """ - return InferenceInvocation( - self._tracer, - self._instruments, - self._logger, - self._completion_hook, - provider, - request_model=request_model, - server_address=server_address, - server_port=server_port, - operation_name=operation_name, - content_capturing_mode=self._content_capturing_mode, - context=context, - _attach_to_context=_attach_to_context, - ) - - def start_llm(self, invocation: LLMInvocation) -> LLMInvocation: - """Start an LLM invocation. - - .. deprecated:: - Use ``handler.inference()`` instead. - """ - invocation._start_with_handler( - self._tracer, - self._instruments, - self._logger, - self._completion_hook, - content_capturing_mode=self._content_capturing_mode, - ) - return invocation - - def start_embedding( - self, - provider: str, - *, - request_model: str | None = None, - server_address: str | None = None, - server_port: int | None = None, - context: Context | None = None, - _attach_to_context: bool = True, - ) -> EmbeddingInvocation: - """Create and start an Embedding invocation. - - .. deprecated:: 1.0b0 - Use ``handler.embedding()`` instead. - - Args: - context: An optional OpenTelemetry Context to parent the span. - - Set remaining attributes (encoding_formats, etc.) on the returned - invocation, then call invocation.stop() or invocation.fail(). - """ - return EmbeddingInvocation( - self._tracer, - self._instruments, - self._logger, - self._completion_hook, - provider, - request_model=request_model, - server_address=server_address, - server_port=server_port, - content_capturing_mode=self._content_capturing_mode, - context=context, - _attach_to_context=_attach_to_context, - ) - def retrieval( self, *, @@ -291,98 +200,6 @@ def retrieval( _attach_to_context=_attach_to_context, ) - def start_tool( - self, - name: str, - *, - tool_type: str | None = None, - tool_call_id: str | None = None, - tool_description: str | None = None, - context: Context | None = None, - _attach_to_context: bool = True, - ) -> ToolInvocation: - """Create and start a tool invocation. - - .. deprecated:: 1.0b0 - Use ``handler.tool()`` instead. - - Args: - context: An optional OpenTelemetry Context to parent the span. - - Set tool_result on the returned invocation when done, then call - invocation.stop() or invocation.fail(). - """ - return ToolInvocation( - self._tracer, - self._instruments, - self._logger, - self._completion_hook, - name, - tool_type=tool_type, - tool_call_id=tool_call_id, - tool_description=tool_description, - content_capturing_mode=self._content_capturing_mode, - context=context, - _attach_to_context=_attach_to_context, - ) - - def start_workflow( - self, - *, - name: str | None = None, - context: Context | None = None, - _attach_to_context: bool = True, - ) -> WorkflowInvocation: - """Create and start a workflow invocation. - - .. deprecated:: 1.0b0 - Use ``handler.workflow()`` instead. - - Args: - context: An optional OpenTelemetry Context to parent the span. - - Set remaining attributes on the returned invocation, then call - invocation.stop() or invocation.fail(). - """ - return WorkflowInvocation( - self._tracer, - self._instruments, - self._logger, - self._completion_hook, - name, - content_capturing_mode=self._content_capturing_mode, - context=context, - _attach_to_context=_attach_to_context, - ) - - def stop_llm(self, invocation: LLMInvocation) -> LLMInvocation: # pylint: disable=no-self-use - """Finalize an LLM invocation successfully and end its span. - - .. deprecated:: - Use ``handler.inference()`` and then ``inference.stop()`` instead. - """ - invocation._sync_to_invocation() - if invocation._inference_invocation is not None: - invocation._inference_invocation.stop() - return invocation - - def fail_llm( # pylint: disable=no-self-use - self, - invocation: LLMInvocation, - error: Error, - ) -> LLMInvocation: - """Fail an LLM invocation and end its span with error status. - - .. deprecated:: - Use ``handler.inference()`` and then ``inference.fail()`` instead. - """ - invocation._sync_to_invocation() - if invocation._inference_invocation is not None: - 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, @@ -549,78 +366,6 @@ def tool( _attach_to_context=_attach_to_context, ) - def start_invoke_local_agent( - self, - *, - request_model: str | None = None, - agent_name: str | None = None, - context: Context | None = None, - _attach_to_context: bool = True, - ) -> AgentInvocation: - """Create and start a local agent invocation (INTERNAL span kind). - - .. deprecated:: 1.0b0 - Use ``handler.invoke_local_agent()`` instead. - - Use for agents running within the same process (e.g. LangChain, CrewAI). - - Args: - context: An optional OpenTelemetry Context to parent the span. - - Set remaining attributes (agent_name, etc.) on the returned invocation, - then call invocation.stop() or invocation.fail(). - """ - return LocalAgentInvocation( - self._tracer, - self._instruments, - self._logger, - self._completion_hook, - request_model=request_model, - agent_name=agent_name, - content_capturing_mode=self._content_capturing_mode, - context=context, - _attach_to_context=_attach_to_context, - ) - - def start_invoke_remote_agent( - self, - provider: str, - *, - request_model: str | None = None, - server_address: str | None = None, - server_port: int | None = None, - agent_name: str | None = None, - context: Context | None = None, - _attach_to_context: bool = True, - ) -> AgentInvocation: - """Create and start a remote agent invocation (CLIENT span kind). - - .. deprecated:: 1.0b0 - Use ``handler.invoke_remote_agent()`` instead. - - Use for agents invoked over a remote service (e.g. OpenAI Assistants, AWS Bedrock). - - Args: - context: An optional OpenTelemetry Context to parent the span. - - Set remaining attributes (agent_name, etc.) on the returned invocation, - then call invocation.stop() or invocation.fail(). - """ - return RemoteAgentInvocation( - self._tracer, - self._instruments, - self._logger, - self._completion_hook, - provider=provider, - request_model=request_model, - agent_name=agent_name, - server_address=server_address, - server_port=server_port, - content_capturing_mode=self._content_capturing_mode, - context=context, - _attach_to_context=_attach_to_context, - ) - def invoke_local_agent( self, *, @@ -747,7 +492,7 @@ def get_telemetry_handler( """ Returns a singleton TelemetryHandler instance. - .. deprecated:: + .. deprecated::1.2b0 Construct a :class:`TelemetryHandler` directly instead. """ handler: TelemetryHandler | None = getattr( diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py index 9fa40adbc..e67112c06 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py @@ -16,7 +16,6 @@ from opentelemetry.util.genai.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, - OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, ) from opentelemetry.util.genai.types import ( BlobPart, @@ -104,71 +103,6 @@ def is_experimental_mode() -> bool: return True -def _should_emit_event( - content_capturing_mode: ContentCapturingMode, -) -> bool: - """Check if event emission is enabled. - - Returns True if event emission is enabled, False otherwise. - - If the environment variable OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT is explicitly set, - its value takes precedence. Otherwise, the default value is determined by - the provided ContentCapturingMode: - - NO_CONTENT or SPAN_ONLY: defaults to False - - EVENT_ONLY or SPAN_AND_EVENT: defaults to True - """ - # If explicitly set (and not empty), use the user's value (highest priority) - if ( - envvar := os.environ.get(OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, "") - .lower() - .strip() - ): - if envvar == "true": - return True - if envvar == "false": - return False - logger.warning( - "%s is not a valid option for `%s` environment variable. Must be one of true or false (case-insensitive). Defaulting based on content capturing mode.", - envvar, - OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, - ) - # EVENT_ONLY and SPAN_AND_EVENT require events, so default to True - return content_capturing_mode in ( - ContentCapturingMode.EVENT_ONLY, - ContentCapturingMode.SPAN_AND_EVENT, - ) - - -def should_emit_event() -> bool: - """Check if event emission is enabled. - - Returns True if event emission is enabled, False otherwise. - - .. deprecated:: 1.2b0 - This function reads environment variables on every call and should NOT - be called on the hot path. Event emission is managed internally by - telemetry handlers and invocations. - """ - return _should_emit_event(get_content_capturing_mode()) - - -def should_capture_content_on_spans() -> bool: - """Returns whether capture content is enabled on spans. - - This function reads environment variables on every call and should NOT - be called on the hot path. - - .. deprecated:: 1.2b0 - Use ``GenAIInvocation.should_capture_content`` - or ``TelemetryHandler.should_capture_content`` - instead. - """ - return get_content_capturing_mode() in ( - ContentCapturingMode.SPAN_ONLY, - ContentCapturingMode.SPAN_AND_EVENT, - ) - - def fq_exception_type(exception: BaseException) -> str: """Return the fully qualified name of an exception's type. diff --git a/util/opentelemetry-util-genai/tests/test_handler_metrics.py b/util/opentelemetry-util-genai/tests/test_handler_metrics.py index 939a662c4..6c69cb06b 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_metrics.py +++ b/util/opentelemetry-util-genai/tests/test_handler_metrics.py @@ -20,7 +20,7 @@ class TelemetryHandlerMetricsTest(TestBase): - def test_stop_llm_records_duration_and_tokens(self) -> None: + def test_inference_stop_records_duration_and_tokens(self) -> None: handler = TelemetryHandler( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, @@ -74,7 +74,7 @@ def test_stop_llm_records_duration_and_tokens(self) -> None: places=3, ) - def test_stop_llm_records_duration_and_tokens_with_additional_attributes( + def test_inference_stop_records_duration_and_tokens_with_additional_attributes( self, ) -> None: handler = TelemetryHandler( @@ -114,7 +114,7 @@ def test_stop_llm_records_duration_and_tokens_with_additional_attributes( ) self.assertIsNone(point.attributes.get("should not be on metrics")) - def test_fail_llm_records_error_and_available_tokens(self) -> None: + def test_inference_fail_records_error_and_available_tokens(self) -> None: handler = TelemetryHandler( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, @@ -156,7 +156,7 @@ def test_fail_llm_records_error_and_available_tokens(self) -> None: ) self.assertAlmostEqual(token_point.sum, 11.0, places=3) - def test_fail_llm_error_type_uses_supplied_resolver(self) -> None: + def test_inference_fail_error_type_uses_supplied_resolver(self) -> None: # An instrumentor-supplied error_type_resolver derives error.type from # the raw exception (e.g. surfacing a provider's canonical status). handler = TelemetryHandler( @@ -178,7 +178,7 @@ def test_fail_llm_error_type_uses_supplied_resolver(self) -> None: "429", ) - def test_fail_llm_error_type_falls_back_when_resolver_returns_none( + def test_inference_fail_error_type_falls_back_when_resolver_returns_none( self, ) -> None: # Resolver returning None falls back to the exception class name. diff --git a/util/opentelemetry-util-genai/tests/test_handler_workflow.py b/util/opentelemetry-util-genai/tests/test_handler_workflow.py index 7217658f7..914b7f8d3 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_workflow.py +++ b/util/opentelemetry-util-genai/tests/test_handler_workflow.py @@ -48,16 +48,12 @@ def _get_finished_spans(self): class TelemetryHandlerWorkflowTest(_WorkflowTestBase): - # ------------------------------------------------------------------ - # start_workflow - # ------------------------------------------------------------------ - - def test_start_workflow_creates_span(self) -> None: + def test_workflow_creates_span(self) -> None: invocation = self.handler.workflow(name="my_workflow") self.assertIsNot(invocation.span, INVALID_SPAN) invocation.stop() - def test_start_workflow_span_name(self) -> None: + def test_workflow_span_name(self) -> None: invocation = self.handler.workflow(name="my_pipeline") invocation.stop() @@ -65,7 +61,7 @@ def test_start_workflow_span_name(self) -> None: self.assertEqual(len(spans), 1) self.assertEqual(spans[0].name, "invoke_workflow my_pipeline") - def test_start_workflow_span_name_without_name(self) -> None: + def test_workflow_span_name_without_name(self) -> None: invocation = self.handler.workflow(name=None) invocation.stop() @@ -90,7 +86,7 @@ def test_workflow_without_conversation_id(self) -> None: spans = self._get_finished_spans() self.assertNotIn(GenAI.GEN_AI_CONVERSATION_ID, spans[0].attributes) - def test_start_workflow_span_kind_is_internal(self) -> None: + def test_workflow_span_kind_is_internal(self) -> None: invocation = self.handler.workflow(name="wf") invocation.stop() @@ -98,13 +94,13 @@ def test_start_workflow_span_kind_is_internal(self) -> None: self.assertEqual(len(spans), 1) self.assertEqual(spans[0].kind, SpanKind.INTERNAL) - def test_start_workflow_records_monotonic_start(self) -> None: + def test_workflow_records_monotonic_start(self) -> None: with patch("timeit.default_timer", return_value=500.0): invocation = self.handler.workflow(name="wf") self.assertEqual(invocation._monotonic_start_s, 500.0) invocation.stop() - def test_start_workflow_sets_workflow_name_attribute(self) -> None: + def test_workflow_sets_workflow_name_attribute(self) -> None: invocation = self.handler.workflow(name="my_pipeline") invocation.stop() @@ -113,7 +109,7 @@ def test_start_workflow_sets_workflow_name_attribute(self) -> None: self.assertEqual(value, "my_pipeline") self.assertIsInstance(value, str) - def test_start_workflow_without_name_omits_workflow_name_attribute( + def test_workflow_without_name_omits_workflow_name_attribute( self, ) -> None: invocation = self.handler.workflow(name=None) @@ -280,7 +276,7 @@ def test_workflow_context_manager_default_invocation(self) -> None: class TelemetryHandlerWorkflowSamplingTest(_WorkflowTestBase): - def test_start_workflow_passes_sampling_attributes_at_span_creation( + def test_workflow_passes_sampling_attributes_at_span_creation( self, ) -> None: """Verify that sampling-relevant attributes are available at start_span() time for workflows.""" diff --git a/util/opentelemetry-util-genai/tests/test_toolcall.py b/util/opentelemetry-util-genai/tests/test_toolcall.py index 860a1f0ef..ffb59e696 100644 --- a/util/opentelemetry-util-genai/tests/test_toolcall.py +++ b/util/opentelemetry-util-genai/tests/test_toolcall.py @@ -205,7 +205,7 @@ def test_tool_span_is_internal_kind(): assert span_exporter.get_finished_spans()[0].kind == SpanKind.INTERNAL -def test_start_tool_passes_sampling_attributes_at_span_creation(): +def test_tool_passes_sampling_attributes_at_span_creation(): """Verify that only sampling-relevant attributes are available at start_span() time for tools.""" captured_attributes = {} @@ -412,7 +412,6 @@ def test_tool_should_capture_content_defaults_to_false(): handler = _make_handler() invocation = handler.tool("get_weather") assert invocation.should_capture_content is False - assert invocation.should_capture_content_on_span is False invocation.stop() @@ -424,7 +423,6 @@ def test_tool_should_capture_content_enabled(): handler = _make_handler() invocation = handler.tool("get_weather") assert invocation.should_capture_content is True - assert invocation.should_capture_content_on_span is True invocation.stop() @@ -439,7 +437,6 @@ def test_tool_content_not_on_span_in_event_only_mode(): handler = TelemetryHandler(tracer_provider=tracer_provider) invocation = handler.tool("get_weather") assert invocation.should_capture_content is True - assert invocation.should_capture_content_on_span is False invocation.arguments = {"city": "Paris"} invocation.tool_result = "sunny" invocation.stop() @@ -461,7 +458,6 @@ def test_tool_content_not_on_span_with_completion_hook(): ) invocation = handler.tool("get_weather") assert invocation.should_capture_content is True - assert invocation.should_capture_content_on_span is False invocation.arguments = {"city": "Paris"} invocation.tool_result = "sunny" invocation.stop() @@ -495,7 +491,6 @@ def test_direct_invocation_instantiation_falls_back_to_env(): name="direct_tool", ) assert invocation.should_capture_content is True - assert invocation.should_capture_content_on_span is True invocation.arguments = {"arg": "val"} invocation.stop() @@ -613,8 +608,7 @@ def test_tool_invocation_context_property(): invocation.stop() -@pytest.mark.parametrize("method_name", ["tool", "start_tool"]) -def test_tool_invocation_explicit_context(method_name: str): +def test_tool_invocation_explicit_context(): span_exporter = InMemorySpanExporter() tracer_provider = TracerProvider() tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter)) @@ -625,9 +619,7 @@ def test_tool_invocation_explicit_context(method_name: str): parent_context = set_span_in_context(parent_span) with tracer.start_as_current_span("ambient_span"): - tool_invocation = getattr(handler, method_name)( - "child_tool", context=parent_context - ) + tool_invocation = handler.tool("child_tool", context=parent_context) tool_invocation.stop() spans = span_exporter.get_finished_spans() @@ -639,8 +631,7 @@ def test_tool_invocation_explicit_context(method_name: str): assert tool_span.parent.span_id == parent_span.get_span_context().span_id -@pytest.mark.parametrize("method_name", ["tool", "start_tool"]) -def test_tool_invocation_attach_to_context_false(method_name: str): +def test_tool_invocation_attach_to_context_false(): span_exporter = InMemorySpanExporter() tracer_provider = TracerProvider() tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter)) @@ -648,7 +639,7 @@ def test_tool_invocation_attach_to_context_false(method_name: str): tracer = tracer_provider.get_tracer("test") with tracer.start_as_current_span("ambient_span") as ambient_span: - tool_invocation = getattr(handler, method_name)( + tool_invocation = handler.tool( "detached_tool", _attach_to_context=False ) assert get_current_span() == ambient_span diff --git a/util/opentelemetry-util-genai/tests/test_utils.py b/util/opentelemetry-util-genai/tests/test_utils.py index e4dc48307..5532ebe0a 100644 --- a/util/opentelemetry-util-genai/tests/test_utils.py +++ b/util/opentelemetry-util-genai/tests/test_utils.py @@ -32,7 +32,7 @@ ) from opentelemetry.semconv.schemas import Schemas from opentelemetry.trace.status import StatusCode -from opentelemetry.util.genai._inference_invocation import LLMInvocation +from opentelemetry.util.genai._inference_invocation import _should_emit_event from opentelemetry.util.genai.handler import TelemetryHandler from opentelemetry.util.genai.types import ( Blob, @@ -55,7 +55,6 @@ UriPart, ) from opentelemetry.util.genai.utils import ( - _should_emit_event, bind_arguments, decode_base64, gen_ai_json_dumps, @@ -63,8 +62,6 @@ get_content_capturing_mode, get_signature, image_from_url, - should_capture_content_on_spans, - should_emit_event, ) @@ -269,37 +266,8 @@ def test_should_emit_event_with_explicit_mode(self): # pylint: disable=no-self- is False ) - def test_deprecated_should_emit_event(self): # pylint: disable=no-self-use - with patch.dict( - os.environ, - { - "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "EVENT_ONLY", - "OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT": "", - }, - ): - assert should_emit_event() is True - class TestShouldCaptureContent(unittest.TestCase): - def test_should_capture_content_on_spans_against_various_env_var_combinations( - self, - ): # pylint: disable=no-self-use - for content_capture, span_content_enabled in [ - ("NO_CONTENT", False), - ("EVENT_ONLY", False), - ("SPAN_ONLY", True), - ("SPAN_AND_EVENT", True), - ]: - with patch.dict( - os.environ, - { - "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": content_capture, - }, - ): - assert ( - should_capture_content_on_spans() is span_content_enabled - ) - def test_get_content_capturing_mode(self): # pylint: disable=no-self-use for content_capture, expected_content_capturing in [ ("NO_CONTENT", ContentCapturingMode.NO_CONTENT), @@ -514,26 +482,6 @@ def test_llm_manual_start_and_stop_creates_span(self): }, ) - @patch.dict( - os.environ, - { - "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "SPAN_ONLY", - "OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT": "false", - }, - ) - def test_start_llm_captures_content_on_span(self): - handler = TelemetryHandler(tracer_provider=self.tracer_provider) - inv = LLMInvocation(request_model="legacy-model") - handler.start_llm(inv) - inv.input_messages = [_create_input_message("hi")] - inv.output_messages = [_create_output_message("hello")] - handler.stop_llm(inv) - - span = _get_single_span(self.span_exporter) - attrs = _get_span_attributes(span) - assert GenAI.GEN_AI_INPUT_MESSAGES in attrs - assert GenAI.GEN_AI_OUTPUT_MESSAGES in attrs - @patch.dict( os.environ, { @@ -554,7 +502,7 @@ def test_inference_messages_omitted_from_span_in_event_only_mode(self): assert GenAI.GEN_AI_INPUT_MESSAGES not in attrs assert GenAI.GEN_AI_OUTPUT_MESSAGES not in attrs - def test_start_inference_passes_sampling_attributes_at_span_creation(self): + def test_inference_passes_sampling_attributes_at_span_creation(self): """Verify that sampling-relevant attributes are available at start_span() time.""" captured_attributes = {} @@ -634,7 +582,7 @@ def test_inference_omits_conversation_id_when_not_set(self): attrs = self.span_exporter.get_finished_spans()[0].attributes assert GenAI.GEN_AI_CONVERSATION_ID not in attrs - def test_start_inference_sampler_can_drop_span_based_on_attributes(self): + def test_inference_sampler_can_drop_span_based_on_attributes(self): """Verify that a sampler can reject spans based on attributes passed at creation time.""" class ModelRejectingSampler: # pylint: disable=no-self-use @@ -685,7 +633,7 @@ def get_description(self): assert len(spans) == 1 assert spans[0].name == "chat accepted-model" - def test_start_embedding_passes_sampling_attributes_at_span_creation(self): + def test_embedding_passes_sampling_attributes_at_span_creation(self): """Verify that sampling-relevant attributes are available at start_span() time for embeddings.""" captured_attributes = {}