diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/132.removed b/instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/132.removed new file mode 100644 index 00000000..4edee7d8 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/132.removed @@ -0,0 +1 @@ +Stopped setting `gen_ai.provider.name` on internal agent spans. diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py index 8779b3d4..6cbba597 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py @@ -92,9 +92,6 @@ def on_chain_start( ) if suggested_agent_name_lower != agent_invocation_name_lower: agent = self._telemetry_handler.invoke_local_agent( - provider=metadata.get("ls_provider", "unknown") - if metadata - else "unknown", agent_name=suggested_agent_name, ) agent.input_messages = make_input_message(inputs) diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py index be5476e4..c4cdd564 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py @@ -165,7 +165,6 @@ def test_new_agent_span_created(self): ) telemetry.invoke_local_agent.assert_called_once_with( - provider="openai", agent_name="math_agent", ) assert agent_inv.agent_name == "math_agent" diff --git a/policies/genai_span_validation.rego b/policies/genai_span_validation.rego index a55fb8bd..9866b354 100644 --- a/policies/genai_span_validation.rego +++ b/policies/genai_span_validation.rego @@ -3,7 +3,7 @@ # (name, type, presence) for spans matching its definitions; this file adds # cross-cutting span-level invariants the registry can't easily express. # -# Two classes of rules, both keyed on `gen_ai.operation.name`: +# Three classes of rules, all keyed on `gen_ai.operation.name`: # # 1. Span name format → `violation` # (`{operation_name} {request_model}` for inference / embeddings, @@ -17,6 +17,9 @@ # semantic-conventions/docs/gen-ai/gen-ai-spans.md and # gen-ai-agent-spans.md (the MD flattens the YAML inheritance chain # via `extends:`, so it's the right place to source from). +# `invoke_agent` is the one operation whose manifest also depends on +# span kind: semconv defines separate internal (same-process) and +# client (remote) spans, and only the client span carries server.*. # # The "set when known" Recommended subset (sampling parameters like # `frequency_penalty`, `max_tokens`; provider-side caches; conditionally- @@ -99,21 +102,26 @@ deny contains _span_finding( # ─── Per-operation expected attributes (violation) ────────────────────────── -_expected_for_op["chat"] := _inference_expected +# `_expected_for_op(op, kind)` returns the expected-attribute manifest for a +# span given its `gen_ai.operation.name` and span kind. Most operations ignore +# kind (second arg is a wildcard); invoke_agent dispatches on it because +# semconv splits it into separate internal and client spans. Undefined (→ no +# violations) for an unmapped op or an unexpected agent span kind. +_expected_for_op("chat", _) := _inference_expected -_expected_for_op["generate_content"] := _inference_expected +_expected_for_op("generate_content", _) := _inference_expected -_expected_for_op["text_completion"] := _inference_expected +_expected_for_op("text_completion", _) := _inference_expected -_expected_for_op["embeddings"] := _embeddings_expected +_expected_for_op("embeddings", _) := _embeddings_expected -_expected_for_op["execute_tool"] := _execute_tool_expected +_expected_for_op("execute_tool", _) := _execute_tool_expected -_expected_for_op["invoke_agent"] := _invoke_agent_expected +_expected_for_op("invoke_agent", kind) := _invoke_agent_expected[kind] -_expected_for_op["create_agent"] := _create_agent_expected +_expected_for_op("create_agent", _) := _create_agent_expected -_expected_for_op["retrieval"] := _retrieval_expected +_expected_for_op("retrieval", _) := _retrieval_expected # Inference (chat / generate_content / text_completion). # Required: gen_ai.operation.name, gen_ai.provider.name. @@ -156,11 +164,17 @@ _execute_tool_expected := { "gen_ai.tool.type", } -# Invoke agent. -# Required: gen_ai.operation.name, gen_ai.provider.name. -_invoke_agent_expected := { +# Invoke agent (internal) +# Required: gen_ai.operation.name +_invoke_agent_expected["internal"] := { "gen_ai.operation.name", - "gen_ai.provider.name", +} + +# Invoke agent (client) +# Required: gen_ai.operation.name, server.address +_invoke_agent_expected["client"] := { + "gen_ai.operation.name", + "server.address", } # Create agent. After creation completes the provider returns an agent.id; @@ -193,7 +207,7 @@ deny contains _span_finding( ) if { input.sample.span op := _attr_value(input.sample.span, "gen_ai.operation.name") - expected := _expected_for_op[op] + expected := _expected_for_op(op, input.sample.span.kind) some attr_name in expected not _has_attr(input.sample.span, attr_name) } diff --git a/util/opentelemetry-util-genai/.changelog/132.removed b/util/opentelemetry-util-genai/.changelog/132.removed new file mode 100644 index 00000000..c8fc51cc --- /dev/null +++ b/util/opentelemetry-util-genai/.changelog/132.removed @@ -0,0 +1 @@ +Removed the `provider` parameter from the internal agent invocation APIs and stopped emitting `gen_ai.provider.name` on internal agent spans and metrics. diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_agent_invocation.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_agent_invocation.py index eebb05c5..70fad50d 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_agent_invocation.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_agent_invocation.py @@ -43,8 +43,8 @@ def __init__( metrics_recorder: InvocationMetricsRecorder, logger: Logger, completion_hook: CompletionHook, - provider: str, *, + provider: str | None = None, span_kind: SpanKind = SpanKind.INTERNAL, request_model: str | None = None, server_address: str | None = None, @@ -64,10 +64,10 @@ def __init__( else _operation_name, span_kind=span_kind, ) - self.provider = provider self.request_model = request_model self.server_address = server_address self.server_port = server_port + self.provider = provider self.agent_name: str | None = agent_name self.agent_id: str | None = None @@ -104,6 +104,7 @@ def __init__( def _get_base_attributes(self) -> dict[str, Any]: """Return sampling-relevant attributes available at span creation time.""" optional_attrs = ( + (GenAI.GEN_AI_PROVIDER_NAME, self.provider), (GenAI.GEN_AI_REQUEST_MODEL, self.request_model), (GenAI.GEN_AI_AGENT_NAME, self.agent_name), (server_attributes.SERVER_ADDRESS, self.server_address), @@ -111,12 +112,12 @@ def _get_base_attributes(self) -> dict[str, Any]: ) return { GenAI.GEN_AI_OPERATION_NAME: self._operation_name, - GenAI.GEN_AI_PROVIDER_NAME: self.provider, **{k: v for k, v in optional_attrs if v is not None}, } def _get_common_attributes(self) -> dict[str, Any]: optional_attrs = ( + (GenAI.GEN_AI_PROVIDER_NAME, self.provider), (GenAI.GEN_AI_REQUEST_MODEL, self.request_model), (server_attributes.SERVER_ADDRESS, self.server_address), (server_attributes.SERVER_PORT, self.server_port), @@ -127,7 +128,6 @@ def _get_common_attributes(self) -> dict[str, Any]: ) return { GenAI.GEN_AI_OPERATION_NAME: self._operation_name, - GenAI.GEN_AI_PROVIDER_NAME: self.provider, **{k: v for k, v in optional_attrs if v is not None}, } 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 ff3ebbba..e28d0e8d 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/handler.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/handler.py @@ -389,7 +389,6 @@ def tool( def start_invoke_local_agent( self, - provider: str, *, request_model: str | None = None, agent_name: str | None = None, @@ -409,7 +408,6 @@ def start_invoke_local_agent( self._metrics_recorder, self._logger, self._completion_hook, - provider, span_kind=SpanKind.INTERNAL, request_model=request_model, agent_name=agent_name, @@ -439,7 +437,7 @@ def start_invoke_remote_agent( self._metrics_recorder, self._logger, self._completion_hook, - provider, + provider=provider, span_kind=SpanKind.CLIENT, request_model=request_model, agent_name=agent_name, @@ -449,7 +447,6 @@ def start_invoke_remote_agent( def invoke_local_agent( self, - provider: str, *, request_model: str | None = None, agent_name: str | None = None, @@ -469,7 +466,6 @@ def invoke_local_agent( self._metrics_recorder, self._logger, self._completion_hook, - provider, span_kind=SpanKind.INTERNAL, request_model=request_model, agent_name=agent_name, @@ -499,7 +495,7 @@ def invoke_remote_agent( self._metrics_recorder, self._logger, self._completion_hook, - provider, + provider=provider, span_kind=SpanKind.CLIENT, request_model=request_model, agent_name=agent_name, diff --git a/util/opentelemetry-util-genai/tests/test_handler_agent.py b/util/opentelemetry-util-genai/tests/test_handler_agent.py index 9075b291..338786d1 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_agent.py +++ b/util/opentelemetry-util-genai/tests/test_handler_agent.py @@ -40,7 +40,6 @@ def setUp(self): def test_start_stop_creates_span(self): invocation = self.handler.invoke_local_agent( - "openai", request_model="gpt-4", agent_name="Math Tutor", ) @@ -52,11 +51,10 @@ def test_start_stop_creates_span(self): assert span.name == "invoke_agent Math Tutor" assert span.attributes[GenAI.GEN_AI_OPERATION_NAME] == "invoke_agent" assert span.attributes[GenAI.GEN_AI_AGENT_NAME] == "Math Tutor" - assert span.attributes[GenAI.GEN_AI_PROVIDER_NAME] == "openai" assert span.attributes[GenAI.GEN_AI_REQUEST_MODEL] == "gpt-4" def test_span_kind_internal(self): - invocation = self.handler.invoke_local_agent("openai") + invocation = self.handler.invoke_local_agent() invocation.stop() assert ( self.span_exporter.get_finished_spans()[0].kind @@ -64,7 +62,7 @@ def test_span_kind_internal(self): ) def test_no_server_attributes(self): - invocation = self.handler.invoke_local_agent("openai") + invocation = self.handler.invoke_local_agent() invocation.stop() attrs = self.span_exporter.get_finished_spans()[0].attributes assert server_attributes.SERVER_ADDRESS not in attrs @@ -72,7 +70,6 @@ def test_no_server_attributes(self): def test_all_attributes(self): invocation = self.handler.invoke_local_agent( - "openai", request_model="gpt-4", ) invocation.agent_name = "Full Agent" @@ -116,7 +113,7 @@ def test_all_attributes(self): assert attrs[GenAI.GEN_AI_RESPONSE_FINISH_REASONS] == ("stop",) def test_finish_reasons_multiple(self): - invocation = self.handler.invoke_local_agent("openai") + invocation = self.handler.invoke_local_agent() invocation.finish_reasons = ["stop", "length"] invocation.stop() attrs = self.span_exporter.get_finished_spans()[0].attributes @@ -126,7 +123,7 @@ def test_finish_reasons_multiple(self): ) def test_finish_reasons_empty_list_omitted(self): - invocation = self.handler.invoke_local_agent("openai") + invocation = self.handler.invoke_local_agent() invocation.finish_reasons = [] invocation.stop() attrs = self.span_exporter.get_finished_spans()[0].attributes @@ -134,7 +131,7 @@ def test_finish_reasons_empty_list_omitted(self): assert GenAI.GEN_AI_RESPONSE_FINISH_REASONS not in attrs def test_cache_token_attributes(self): - invocation = self.handler.invoke_local_agent("openai") + invocation = self.handler.invoke_local_agent() invocation.input_tokens = 100 invocation.cache_creation_input_tokens = 25 invocation.cache_read_input_tokens = 50 @@ -146,7 +143,7 @@ def test_cache_token_attributes(self): assert attrs[GenAI.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] == 50 def test_fail_sets_error_status(self): - invocation = self.handler.invoke_local_agent("openai") + invocation = self.handler.invoke_local_agent() invocation.fail(RuntimeError("agent crashed")) span = self.span_exporter.get_finished_spans()[0] @@ -155,7 +152,7 @@ def test_fail_sets_error_status(self): def test_context_manager_success(self): with self.handler.invoke_local_agent( - "openai", request_model="gpt-4", agent_name="CM Agent" + request_model="gpt-4", agent_name="CM Agent" ) as inv: inv.input_tokens = 10 inv.output_tokens = 20 @@ -167,7 +164,7 @@ def test_context_manager_success(self): def test_context_manager_error(self): with self.assertRaises(ValueError): - with self.handler.invoke_local_agent("openai"): + with self.handler.invoke_local_agent(): raise ValueError("test error") assert ( @@ -178,16 +175,15 @@ def test_context_manager_error(self): ) def test_context_manager_default_invocation(self): - with self.handler.invoke_local_agent("openai") as inv: + with self.handler.invoke_local_agent() as inv: inv.agent_name = "Dynamic Agent" assert len(self.span_exporter.get_finished_spans()) == 1 def test_default_values(self): - invocation = self.handler.invoke_local_agent("openai") + invocation = self.handler.invoke_local_agent() invocation.stop() assert invocation._operation_name == "invoke_agent" assert invocation.agent_name is None - assert invocation.provider == "openai" assert invocation.request_model is None assert not invocation.input_messages assert not invocation.output_messages @@ -198,7 +194,7 @@ def test_default_values(self): assert not invocation.attributes def test_with_messages(self): - invocation = self.handler.invoke_local_agent("openai") + invocation = self.handler.invoke_local_agent() invocation.input_messages = [ InputMessage(role="user", parts=[Text(content="Hello")]) ] @@ -214,7 +210,7 @@ def test_with_messages(self): assert invocation.input_messages[0].role == "user" def test_custom_attributes(self): - invocation = self.handler.invoke_local_agent("openai") + invocation = self.handler.invoke_local_agent() invocation.attributes["custom.key"] = "custom_value" invocation.stop() spans = self.span_exporter.get_finished_spans() @@ -226,7 +222,7 @@ def test_tool_definitions_type(self): description="Get the weather", parameters={"type": "object", "properties": {}}, ) - invocation = self.handler.invoke_local_agent("openai") + invocation = self.handler.invoke_local_agent() invocation.tool_definitions = [tool] invocation.stop() assert len(invocation.tool_definitions) == 1 @@ -234,23 +230,23 @@ def test_tool_definitions_type(self): assert invocation.tool_definitions[0].type == "function" def test_default_lists_are_independent(self): - inv1 = self.handler.invoke_local_agent("openai") - inv2 = self.handler.invoke_local_agent("openai") + inv1 = self.handler.invoke_local_agent() + inv2 = self.handler.invoke_local_agent() inv1.input_messages.append(InputMessage(role="user", parts=[])) assert len(inv2.input_messages) == 0 inv2.stop() inv1.stop() def test_default_attributes_are_independent(self): - inv1 = self.handler.invoke_local_agent("openai") - inv2 = self.handler.invoke_local_agent("openai") + inv1 = self.handler.invoke_local_agent() + inv2 = self.handler.invoke_local_agent() inv1.attributes["foo"] = "bar" assert "foo" not in inv2.attributes inv2.stop() inv1.stop() def test_agent_name_set_after_construction(self): - invocation = self.handler.invoke_local_agent("openai") + invocation = self.handler.invoke_local_agent() invocation.agent_name = "Named Agent" invocation.stop() span = self.span_exporter.get_finished_spans()[0] @@ -261,7 +257,7 @@ def test_agent_name_set_after_construction(self): def test_agent_name_passed_at_construction(self): invocation = self.handler.invoke_local_agent( - "openai", agent_name="Constructor Agent" + agent_name="Constructor Agent" ) invocation.stop() span = self.span_exporter.get_finished_spans()[0] @@ -293,9 +289,7 @@ def get_description(self): ) handler = TelemetryHandler(tracer_provider=sampler_provider) - invocation = handler.invoke_local_agent( - "openai", agent_name="Sampler Agent" - ) + invocation = handler.invoke_local_agent(agent_name="Sampler Agent") invocation.stop() assert captured_attributes[GenAI.GEN_AI_AGENT_NAME] == "Sampler Agent" @@ -325,7 +319,7 @@ def get_description(self): ) handler = TelemetryHandler(tracer_provider=sampler_provider) - invocation = handler.invoke_local_agent("openai") + invocation = handler.invoke_local_agent() invocation.stop() assert GenAI.GEN_AI_AGENT_NAME not in captured_attributes @@ -345,7 +339,7 @@ def setUp(self): return_value=ContentCapturingMode.SPAN_AND_EVENT, ) def test_system_instruction_on_span(self, _mock_cap): - invocation = self.handler.invoke_local_agent("openai") + invocation = self.handler.invoke_local_agent() invocation.system_instruction = [ Text(content="You are a helpful assistant."), ] @@ -364,7 +358,7 @@ def test_tool_definitions_on_span(self, _mock_cap): description="Get the weather", parameters={"type": "object", "properties": {}}, ) - invocation = self.handler.invoke_local_agent("openai") + invocation = self.handler.invoke_local_agent() invocation.tool_definitions = [tool] invocation.stop() @@ -376,7 +370,7 @@ def test_tool_definitions_on_span(self, _mock_cap): return_value=ContentCapturingMode.SPAN_AND_EVENT, ) def test_messages_on_span(self, _mock_cap): - invocation = self.handler.invoke_local_agent("openai") + invocation = self.handler.invoke_local_agent() invocation.input_messages = [ InputMessage(role="user", parts=[Text(content="Hello")]) ] @@ -394,7 +388,7 @@ def test_messages_on_span(self, _mock_cap): assert GenAI.GEN_AI_OUTPUT_MESSAGES in attrs def test_content_not_on_span_by_default(self): - invocation = self.handler.invoke_local_agent("openai") + invocation = self.handler.invoke_local_agent() invocation.system_instruction = [ Text(content="You are a helpful assistant."), ] @@ -551,9 +545,7 @@ def test_local_agent_records_duration_and_tokens(self) -> None: meter_provider=self.meter_provider, ) with patch("timeit.default_timer", return_value=1000.0): - invocation = handler.invoke_local_agent( - "prov", request_model="model" - ) + invocation = handler.invoke_local_agent(request_model="model") invocation.input_tokens = 5 invocation.output_tokens = 7 @@ -572,9 +564,6 @@ def test_local_agent_records_duration_and_tokens(self) -> None: self.assertEqual( duration_point.attributes[GenAI.GEN_AI_REQUEST_MODEL], "model" ) - self.assertEqual( - duration_point.attributes[GenAI.GEN_AI_PROVIDER_NAME], "prov" - ) self.assertAlmostEqual(duration_point.sum, 2.0, places=3) self.assertIn("gen_ai.client.token.usage", metrics) @@ -623,9 +612,7 @@ def test_fail_agent_records_error_metric(self) -> None: meter_provider=self.meter_provider, ) with patch("timeit.default_timer", return_value=2000.0): - invocation = handler.invoke_local_agent( - "", request_model="err-model" - ) + invocation = handler.invoke_local_agent(request_model="err-model") invocation.input_tokens = 11 error = Error(message="boom", type=ValueError) diff --git a/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py b/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py index adb8197a..300597b4 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py +++ b/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py @@ -231,9 +231,7 @@ def test_local_agent_hook_called_on_stop_with_messages(self): ) ] - invocation = handler.invoke_local_agent( - "openai", request_model="gpt-4" - ) + invocation = handler.invoke_local_agent(request_model="gpt-4") invocation.agent_name = "Math Tutor" invocation.input_messages = input_messages invocation.output_messages = output_messages @@ -254,9 +252,7 @@ def test_local_agent_hook_called_on_fail(self): hook = MagicMock() handler = self._make_handler(hook) - invocation = handler.invoke_local_agent( - "openai", request_model="gpt-4" - ) + invocation = handler.invoke_local_agent(request_model="gpt-4") invocation.input_messages = [ InputMessage(role="user", parts=[Text(content="hello")]) ] @@ -323,7 +319,7 @@ def test_agent_hook_called_with_empty_messages_when_none_set(self): hook = MagicMock() handler = self._make_handler(hook) - handler.invoke_local_agent("openai").stop() + handler.invoke_local_agent().stop() handler.invoke_remote_agent("openai").stop() for call in hook.on_completion.call_args_list: @@ -335,7 +331,7 @@ def test_agent_hook_called_with_empty_messages_when_none_set(self): def test_agent_hook_not_called_when_not_set(self): # No hook — stop should not raise handler = self._make_handler() - handler.invoke_local_agent("openai").stop() + handler.invoke_local_agent().stop() handler.invoke_remote_agent("openai").stop() def test_should_capture_content_false_by_default(self): diff --git a/versions.env b/versions.env index d08850cd..5caa3a2e 100644 --- a/versions.env +++ b/versions.env @@ -6,4 +6,4 @@ WEAVER_VERSION=v0.23.0 # The genai semconv registry has no tagged releases yet, so we pin a SHA on `main`. # renovate: datasource=git-refs depName=open-telemetry/semantic-conventions-genai packageName=https://github.com/open-telemetry/semantic-conventions-genai.git versioning=git -SEMCONV_GENAI_REF=8508fbfa5189ae50c7e95aa2fcd90c5c4998cbc7 +SEMCONV_GENAI_REF=528c45308c35c4d0cc31d386238908b4a1e7fd8f