From eeac763be1ff84f45e5d996d84e1e1b69d71c43c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=89=A7=E6=80=9D?= Date: Fri, 29 May 2026 22:46:37 +0800 Subject: [PATCH 1/6] feat(distro): add host.ip and gen_ai.instrumentation.sdk.name resources Add a LoongSuiteResourceDetector that contributes two resource attributes to the agent: * host.ip: the local host IP combined with the process id, formatted as - (e.g. 127.0.0.1-1). * gen_ai.instrumentation.sdk.name: set to "loongsuite-genai-utils". The detector is wired into LoongSuiteConfigurator so the attributes are always present on the SDK resource. Includes unit tests for the detector (ip-pid format, fixed sdk name, fallback on socket error) and the configurator wiring. Change-Id: I7fdfd07ffa8e2021e82c1d85d7d6b527c2cf8170 Co-developed-by: Claude Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/loongsuite/distro/__init__.py | 11 +++ .../src/loongsuite/distro/resource.py | 80 ++++++++++++++++ loongsuite-distro/tests/__init__.py | 13 +++ loongsuite-distro/tests/test_configurator.py | 70 ++++++++++++++ loongsuite-distro/tests/test_resource.py | 92 +++++++++++++++++++ 5 files changed, 266 insertions(+) create mode 100644 loongsuite-distro/src/loongsuite/distro/resource.py create mode 100644 loongsuite-distro/tests/__init__.py create mode 100644 loongsuite-distro/tests/test_configurator.py create mode 100644 loongsuite-distro/tests/test_resource.py diff --git a/loongsuite-distro/src/loongsuite/distro/__init__.py b/loongsuite-distro/src/loongsuite/distro/__init__.py index 86d73b0e2..30b1ae973 100644 --- a/loongsuite-distro/src/loongsuite/distro/__init__.py +++ b/loongsuite-distro/src/loongsuite/distro/__init__.py @@ -24,12 +24,23 @@ from opentelemetry.sdk._configuration import _OTelSDKConfigurator from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_PROTOCOL +from loongsuite.distro.resource import LoongSuiteResourceDetector + class LoongSuiteConfigurator(_OTelSDKConfigurator): """ LoongSuite configurator, inherits from OpenTelemetry SDK configurator. + + Augments the resource with LoongSuite specific attributes (``host.ip`` and + ``gen_ai.instrumentation.sdk.name``) before delegating to the OpenTelemetry + SDK configurator. """ + def _configure(self, **kwargs: Any) -> None: + resource_attributes = dict(kwargs.pop("resource_attributes", None) or {}) + resource_attributes.update(LoongSuiteResourceDetector().detect().attributes) + super()._configure(resource_attributes=resource_attributes, **kwargs) + class LoongSuiteDistro(BaseDistro): """ diff --git a/loongsuite-distro/src/loongsuite/distro/resource.py b/loongsuite-distro/src/loongsuite/distro/resource.py new file mode 100644 index 000000000..1542d99d1 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/resource.py @@ -0,0 +1,80 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import socket +from logging import getLogger + +from opentelemetry.sdk.resources import Resource, ResourceDetector + +logger = getLogger(__name__) + +# Resource attribute keys contributed by LoongSuite. +HOST_IP = "host.ip" +GEN_AI_INSTRUMENTATION_SDK_NAME = "gen_ai.instrumentation.sdk.name" + +# Fixed value identifying the GenAI instrumentation SDK shipped with LoongSuite. +_GEN_AI_INSTRUMENTATION_SDK_NAME_VALUE = "loongsuite-genai-utils" + +_FALLBACK_HOST_IP = "127.0.0.1" + + +def _get_host_ip() -> str: + """Best-effort detection of the local host IP address. + + Opens a UDP socket towards a public address to discover which local + interface would be used for outbound traffic. No packet is actually sent. + Falls back to ``127.0.0.1`` when detection fails. + """ + sock = None + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.connect(("8.8.8.8", 80)) + return sock.getsockname()[0] + except OSError as exception: + logger.debug( + "Failed to detect host ip, falling back to %s. Exception: %s", + _FALLBACK_HOST_IP, + exception, + ) + return _FALLBACK_HOST_IP + finally: + if sock is not None: + sock.close() + + +def _get_host_ip_with_pid() -> str: + """Returns the host IP combined with the current process id. + + The format is ``-``, for example ``127.0.0.1-1``. + """ + return f"{_get_host_ip()}-{os.getpid()}" + + +class LoongSuiteResourceDetector(ResourceDetector): + """Detects LoongSuite specific resource attributes. + + Contributes the following attributes to the resource: + + * ``host.ip`` formatted as ``-`` (e.g. ``127.0.0.1-1``). + * ``gen_ai.instrumentation.sdk.name`` set to ``loongsuite-genai-utils``. + """ + + def detect(self) -> Resource: + return Resource( + { + HOST_IP: _get_host_ip_with_pid(), + GEN_AI_INSTRUMENTATION_SDK_NAME: _GEN_AI_INSTRUMENTATION_SDK_NAME_VALUE, + } + ) diff --git a/loongsuite-distro/tests/__init__.py b/loongsuite-distro/tests/__init__.py new file mode 100644 index 000000000..b0a6f4284 --- /dev/null +++ b/loongsuite-distro/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/loongsuite-distro/tests/test_configurator.py b/loongsuite-distro/tests/test_configurator.py new file mode 100644 index 000000000..1c9a0696e --- /dev/null +++ b/loongsuite-distro/tests/test_configurator.py @@ -0,0 +1,70 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from loongsuite.distro import LoongSuiteConfigurator +from loongsuite.distro.resource import ( + GEN_AI_INSTRUMENTATION_SDK_NAME, + HOST_IP, +) + + +class TestLoongSuiteConfigurator(unittest.TestCase): + @mock.patch("opentelemetry.sdk._configuration._initialize_components") + def test_configure_injects_loongsuite_attributes(self, mock_init): + LoongSuiteConfigurator().configure() + + mock_init.assert_called_once() + resource_attributes = mock_init.call_args.kwargs[ + "resource_attributes" + ] + self.assertIn(HOST_IP, resource_attributes) + self.assertEqual( + resource_attributes[GEN_AI_INSTRUMENTATION_SDK_NAME], + "loongsuite-genai-utils", + ) + + @mock.patch("opentelemetry.sdk._configuration._initialize_components") + def test_configure_preserves_existing_attributes(self, mock_init): + LoongSuiteConfigurator().configure( + resource_attributes={"service.name": "my-service"} + ) + + resource_attributes = mock_init.call_args.kwargs[ + "resource_attributes" + ] + self.assertEqual( + resource_attributes["service.name"], "my-service" + ) + self.assertIn(HOST_IP, resource_attributes) + self.assertIn( + GEN_AI_INSTRUMENTATION_SDK_NAME, resource_attributes + ) + + @mock.patch("opentelemetry.sdk._configuration._initialize_components") + def test_configure_forwards_other_kwargs(self, mock_init): + LoongSuiteConfigurator().configure( + auto_instrumentation_version="1.2.3" + ) + + self.assertEqual( + mock_init.call_args.kwargs["auto_instrumentation_version"], + "1.2.3", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/loongsuite-distro/tests/test_resource.py b/loongsuite-distro/tests/test_resource.py new file mode 100644 index 000000000..9c68c99d5 --- /dev/null +++ b/loongsuite-distro/tests/test_resource.py @@ -0,0 +1,92 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from opentelemetry.sdk.resources import Resource, ResourceDetector + +from loongsuite.distro.resource import ( + GEN_AI_INSTRUMENTATION_SDK_NAME, + HOST_IP, + LoongSuiteResourceDetector, + _get_host_ip, + _get_host_ip_with_pid, +) + + +class TestLoongSuiteResourceDetector(unittest.TestCase): + def test_is_resource_detector(self): + self.assertIsInstance( + LoongSuiteResourceDetector(), ResourceDetector + ) + + def test_detect_returns_resource(self): + resource = LoongSuiteResourceDetector().detect() + self.assertIsInstance(resource, Resource) + + def test_detect_contains_expected_keys(self): + attributes = LoongSuiteResourceDetector().detect().attributes + self.assertIn(HOST_IP, attributes) + self.assertIn(GEN_AI_INSTRUMENTATION_SDK_NAME, attributes) + + def test_gen_ai_instrumentation_sdk_name_value(self): + attributes = LoongSuiteResourceDetector().detect().attributes + self.assertEqual( + attributes[GEN_AI_INSTRUMENTATION_SDK_NAME], + "loongsuite-genai-utils", + ) + + @mock.patch("loongsuite.distro.resource.os.getpid", return_value=1) + @mock.patch( + "loongsuite.distro.resource._get_host_ip", return_value="127.0.0.1" + ) + def test_host_ip_format_is_ip_dash_pid(self, _mock_ip, _mock_pid): + attributes = LoongSuiteResourceDetector().detect().attributes + self.assertEqual(attributes[HOST_IP], "127.0.0.1-1") + + @mock.patch("loongsuite.distro.resource.os.getpid", return_value=42) + @mock.patch( + "loongsuite.distro.resource._get_host_ip", return_value="10.0.0.5" + ) + def test_get_host_ip_with_pid(self, _mock_ip, _mock_pid): + self.assertEqual(_get_host_ip_with_pid(), "10.0.0.5-42") + + +class TestGetHostIp(unittest.TestCase): + def test_returns_detected_ip(self): + mock_sock = mock.MagicMock() + mock_sock.getsockname.return_value = ("192.168.1.100", 12345) + with mock.patch( + "loongsuite.distro.resource.socket.socket", + return_value=mock_sock, + ): + self.assertEqual(_get_host_ip(), "192.168.1.100") + mock_sock.connect.assert_called_once() + mock_sock.close.assert_called_once() + + def test_falls_back_to_loopback_on_error(self): + mock_sock = mock.MagicMock() + mock_sock.connect.side_effect = OSError("no network") + with mock.patch( + "loongsuite.distro.resource.socket.socket", + return_value=mock_sock, + ): + self.assertEqual(_get_host_ip(), "127.0.0.1") + # The socket must still be closed even when detection fails. + mock_sock.close.assert_called_once() + + +if __name__ == "__main__": + unittest.main() From fbbedf46d4a4e88d1b23f354aadc3edbe9136ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=89=A7=E6=80=9D?= Date: Sun, 21 Jun 2026 15:14:07 +0800 Subject: [PATCH 2/6] feat: improve OpenTelemetry semantic convention attribute coverage Add missing GenAI semantic convention attributes across instrumentation plugins: - execute_tool spans: add tool_type="function" (Recommended) - invoke_agent spans: add agent_id (Conditionally Required) - inference spans: add server_address/server_port (Conditionally Required) - inference spans: add response_id extraction (Recommended) Plugins modified: - hermes-agent: agent_id, server_address/port from base_url, tool_type - vita: agent_id, tool_type, response_id, server_address inference - langchain: agent_id, tool_type - dashscope: server_address/port - widesearch: agent_id - agentscope: tool_type - qwen-agent: agent_id, tool_type - minisweagent: agent_id - google-adk: agent_id, tool_type - claude-agent-sdk: tool_type Change-Id: I21fc87489630d2ac6a0d9df22ef70dde60c57fff Co-developed-by: Qoder --- .../instrumentation/agentscope/patch.py | 1 + .../tests/test_skill_detection.py | 1 + .../instrumentation/claude_agent_sdk/patch.py | 1 + .../dashscope/utils/generation.py | 2 ++ .../google_adk/internal/_plugin.py | 3 +++ .../instrumentation/hermes_agent/helpers.py | 25 +++++++++++++++++++ .../langchain/internal/_tracer.py | 2 ++ .../minisweagent/internal/agent_wrappers.py | 3 ++- .../instrumentation/qwen_agent/utils.py | 2 ++ .../instrumentation/vita/patch.py | 7 ++++++ .../instrumentation/vita/utils.py | 15 +++++++++++ .../instrumentation/widesearch/utils.py | 1 + 12 files changed, 62 insertions(+), 1 deletion(-) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/patch.py index 7fb2cd58c..7e5d4fa68 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/patch.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/patch.py @@ -403,6 +403,7 @@ async def wrap_tool_call(wrapped, instance, args, kwargs, handler): # Create invocation object with all tool data invocation = ExecuteToolInvocation( tool_name=tool_name, + tool_type="function", tool_call_id=tool_id, tool_description=tool_description, tool_call_arguments=tool_args, diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_skill_detection.py b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_skill_detection.py index 747fc8c46..8cb2c20aa 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_skill_detection.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_skill_detection.py @@ -283,6 +283,7 @@ async def fake_tool_generator(): invocation = ExecuteToolInvocation( tool_name="read_file", + tool_type="function", skill_name="news", skill_id="workspace:default:news", skill_description="Latest news", diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-claude-agent-sdk/src/opentelemetry/instrumentation/claude_agent_sdk/patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-claude-agent-sdk/src/opentelemetry/instrumentation/claude_agent_sdk/patch.py index 8477b6950..2f0b509eb 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-claude-agent-sdk/src/opentelemetry/instrumentation/claude_agent_sdk/patch.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-claude-agent-sdk/src/opentelemetry/instrumentation/claude_agent_sdk/patch.py @@ -159,6 +159,7 @@ def _create_tool_spans_from_message( try: tool_invocation = ExecuteToolInvocation( tool_name=tool_name, + tool_type="function", tool_call_id=tool_use_id, tool_call_arguments=tool_input, tool_description=tool_name, diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py index c278842c4..da86d734d 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py @@ -469,6 +469,8 @@ def _create_invocation_from_generation( invocation = LLMInvocation(request_model=request_model) invocation.provider = "dashscope" + invocation.server_address = "dashscope.aliyuncs.com" + invocation.server_port = 443 invocation.input_messages = _extract_input_messages(kwargs) # Extract tool definitions and convert to FunctionToolDefinition objects diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-google-adk/src/opentelemetry/instrumentation/google_adk/internal/_plugin.py b/instrumentation-loongsuite/loongsuite-instrumentation-google-adk/src/opentelemetry/instrumentation/google_adk/internal/_plugin.py index ea331c3fc..30ce6961a 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-google-adk/src/opentelemetry/instrumentation/google_adk/internal/_plugin.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-google-adk/src/opentelemetry/instrumentation/google_adk/internal/_plugin.py @@ -119,6 +119,7 @@ async def before_run_callback( invocation = InvokeAgentInvocation( provider="google_adk", agent_name=invocation_context.app_name, + agent_id=invocation_context.app_name, ) # Set conversation_id if available @@ -275,6 +276,7 @@ async def before_agent_callback( invocation = InvokeAgentInvocation( provider="google_adk", agent_name=agent.name, + agent_id=agent.name, ) # Set agent attributes @@ -508,6 +510,7 @@ async def before_tool_callback( # Create invocation object invocation = ExecuteToolInvocation( tool_name=tool.name, + tool_type="function", provider="google_adk", ) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py b/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py index 80459e88f..5d435e36b 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py @@ -571,6 +571,7 @@ def create_agent_invocation( invocation = InvokeAgentInvocation( provider=_HERMES_AGENT_SYSTEM, agent_name="Hermes", + agent_id="hermes-agent", conversation_id=getattr(instance, "session_id", None), request_model=getattr(instance, "model", None), input_messages=[ @@ -609,6 +610,25 @@ def should_create_entry_for_agent(instance: Any) -> bool: return bool(_entry_platform(instance)) +def _extract_server_info(instance: Any) -> tuple[str | None, int | None]: + """Extract server address and port from instance base_url.""" + base_url = getattr(instance, "base_url", None) + if not base_url: + return None, None + base_url_str = str(base_url).rstrip("/") + try: + from urllib.parse import urlparse + + parsed = urlparse(base_url_str) + address = parsed.hostname + port = parsed.port + if port is None: + port = 443 if parsed.scheme == "https" else 80 + return address, port + except Exception: + return None, None + + def create_llm_invocation(instance: Any, api_kwargs: Any) -> LLMInvocation: if not isinstance(api_kwargs, dict): api_kwargs = {} @@ -620,6 +640,8 @@ def create_llm_invocation(instance: Any, api_kwargs: Any) -> LLMInvocation: if max_tokens is None: max_tokens = api_kwargs.get("max_output_tokens") + server_address, server_port = _extract_server_info(instance) + invocation = LLMInvocation( provider=provider_name(instance), request_model=request_model or None, @@ -633,6 +655,8 @@ def create_llm_invocation(instance: Any, api_kwargs: Any) -> LLMInvocation: presence_penalty=api_kwargs.get("presence_penalty"), seed=api_kwargs.get("seed"), stop_sequences=api_kwargs.get("stop"), + server_address=server_address, + server_port=server_port, ) return invocation @@ -683,6 +707,7 @@ def create_tool_invocation( tool_name=tool_name, provider=provider or _HERMES_AGENT_SYSTEM, ) + invocation.tool_type = "function" if invocation.provider: invocation.attributes["gen_ai.provider.name"] = invocation.provider if arguments is not None: diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_tracer.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_tracer.py index 830159f5c..6edef11e5 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_tracer.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_tracer.py @@ -407,6 +407,7 @@ def _start_agent(self, run: Run) -> None: invocation = InvokeAgentInvocation( provider="langchain", agent_name=agent_name, + agent_id=agent_name or None, input_messages=input_messages, ) self._handler.start_invoke_agent(invocation, context=parent_ctx) @@ -571,6 +572,7 @@ def _on_tool_start(self, run: Run) -> None: tool_name=run.name or "unknown_tool", tool_call_arguments=input_str, tool_call_id=tool_call_id, + tool_type="function", ) self._handler.start_execute_tool(invocation, context=parent_ctx) rd = _RunData( diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-minisweagent/src/opentelemetry/instrumentation/minisweagent/internal/agent_wrappers.py b/instrumentation-loongsuite/loongsuite-instrumentation-minisweagent/src/opentelemetry/instrumentation/minisweagent/internal/agent_wrappers.py index 7dfe3201e..3d1b9b931 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-minisweagent/src/opentelemetry/instrumentation/minisweagent/internal/agent_wrappers.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-minisweagent/src/opentelemetry/instrumentation/minisweagent/internal/agent_wrappers.py @@ -113,7 +113,8 @@ def __call__( han.start_entry(entry_inv, context=context_api.get_current()) inv = InvokeAgentInvocation( - provider="minisweagent", agent_name=agent_name + provider="minisweagent", agent_name=agent_name, + agent_id=agent_name, ) inv.request_model = _request_model_from_agent(instance) inv.attributes.setdefault("gen_ai.framework", "minisweagent") diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-qwen-agent/src/opentelemetry/instrumentation/qwen_agent/utils.py b/instrumentation-loongsuite/loongsuite-instrumentation-qwen-agent/src/opentelemetry/instrumentation/qwen_agent/utils.py index 269358540..91be544e8 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-qwen-agent/src/opentelemetry/instrumentation/qwen_agent/utils.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-qwen-agent/src/opentelemetry/instrumentation/qwen_agent/utils.py @@ -406,6 +406,7 @@ def _create_agent_invocation( invocation = InvokeAgentInvocation( provider=provider_name, agent_name=agent_name, + agent_id=agent_name, agent_description=agent_description, request_model=request_model, input_messages=input_messages, @@ -462,6 +463,7 @@ def _create_tool_invocation( return ExecuteToolInvocation( tool_name=tool_name, + tool_type="function", tool_call_arguments=parsed_args, tool_description=tool_description, ) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/patch.py index 965c86e99..7409c40be 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/patch.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/patch.py @@ -300,6 +300,7 @@ def wrap_generate_next_message( invocation = InvokeAgentInvocation( provider="vitabench", agent_name=agent_name, + agent_id=agent_name, request_model=model, ) @@ -398,6 +399,11 @@ def wrap_generate( # response_model_name invocation.response_model_name = model + # response_id + resp_id = getattr(result, "id", None) or getattr(result, "response_id", None) + if resp_id: + invocation.response_id = str(resp_id) + # finish_reasons if getattr(result, "tool_calls", None): invocation.finish_reasons = ["tool_calls"] @@ -433,6 +439,7 @@ def wrap_get_response( tool_name=tool_name, tool_call_id=tool_call_id, provider="vitabench", + tool_type="function", ) # tool_call_arguments diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/utils.py b/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/utils.py index f2307bd85..308d42c27 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/utils.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/utils.py @@ -149,6 +149,21 @@ def _infer_provider(model_name: str) -> str: return "unknown" + +def _infer_server_address(model: str) -> tuple[str | None, int | None]: + """Infer server address from model name for vita.""" + model_lower = model.lower() if model else "" + if "qwen" in model_lower or "dashscope" in model_lower: + return "dashscope.aliyuncs.com", 443 + if "gpt" in model_lower or "openai" in model_lower: + return "api.openai.com", 443 + if "claude" in model_lower or "anthropic" in model_lower: + return "api.anthropic.com", 443 + if "deepseek" in model_lower: + return "api.deepseek.com", 443 + return None, None + + def _get_tool_definitions( tools: Any, ) -> Optional[List[FunctionToolDefinition]]: diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-widesearch/src/opentelemetry/instrumentation/widesearch/utils.py b/instrumentation-loongsuite/loongsuite-instrumentation-widesearch/src/opentelemetry/instrumentation/widesearch/utils.py index 316f157c1..dfeb53e3e 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-widesearch/src/opentelemetry/instrumentation/widesearch/utils.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-widesearch/src/opentelemetry/instrumentation/widesearch/utils.py @@ -88,6 +88,7 @@ def _create_agent_invocation( invocation = InvokeAgentInvocation( provider="widesearch", agent_name=agent_name, + agent_id=agent_name, agent_description=instructions[:200] if instructions else "", request_model=request_model, input_messages=[ From 406429865b5a9a9c4d0fd39dbddf374b46faaad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=89=A7=E6=80=9D?= Date: Mon, 22 Jun 2026 14:56:46 +0800 Subject: [PATCH 3/6] feat: add cache token extraction and dashscope embeddings improvements - dashscope: add _extract_cache_tokens() for cache_creation/cache_read tokens - dashscope: set server_address/port on embedding invocations - dashscope: update test to use text-embedding-v4 model - hermes-agent: extract cache tokens from usage.prompt_tokens_details - vita: extract cache tokens from OpenAI-compatible usage format These changes enable gen_ai.usage.cache_creation.input_tokens and gen_ai.usage.cache_read.input_tokens attributes for LLM plugins that interact with providers supporting prompt caching (e.g. Anthropic, OpenAI with cached prompts, DashScope with context caching). Change-Id: I6f0c4035561b43eb5e8ed132257a6438b2a323f1 Co-developed-by: Qoder --- .../dashscope/patch/embedding.py | 2 + .../instrumentation/dashscope/utils/common.py | 41 +++++++++++++++++++ .../dashscope/utils/generation.py | 8 ++++ .../tests/test_embedding.py | 16 ++++---- .../instrumentation/hermes_agent/helpers.py | 20 +++++++++ .../instrumentation/vita/patch.py | 18 ++++++++ 6 files changed, 97 insertions(+), 8 deletions(-) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/patch/embedding.py b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/patch/embedding.py index d09975819..43502d72e 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/patch/embedding.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/patch/embedding.py @@ -52,6 +52,8 @@ def wrap_text_embedding_call(wrapped, instance, args, kwargs, handler=None): # Create embedding invocation object invocation = EmbeddingInvocation(request_model=model) invocation.provider = "dashscope" + invocation.server_address = "dashscope.aliyuncs.com" + invocation.server_port = 443 # Extract parameters from kwargs or kwargs["parameters"] dict parameters = kwargs.get("parameters", {}) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/common.py b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/common.py index 32f9b4f71..eedacb83a 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/common.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/common.py @@ -88,6 +88,47 @@ def _extract_usage(response: Any) -> tuple[Optional[int], Optional[int]]: return None, None +def _extract_cache_tokens(response: Any) -> tuple[Optional[int], Optional[int]]: + """Extract cache token usage from DashScope response. + + Args: + response: DashScope response object + + Returns: + Tuple of (cache_creation_input_tokens, cache_read_input_tokens) + """ + if not response: + return None, None + + try: + usage = getattr(response, "usage", None) + if not usage: + return None, None + + # DashScope may report cache tokens in various fields + cache_creation = ( + getattr(usage, "cache_creation_input_tokens", None) + or getattr(usage, "cache_creation_tokens", None) + ) + cache_read = ( + getattr(usage, "cache_read_input_tokens", None) + or getattr(usage, "cache_read_tokens", None) + or getattr(usage, "prompt_cache_hit_tokens", None) + ) + + # Also check prompt_tokens_details (OpenAI-compatible format) + prompt_details = getattr(usage, "prompt_tokens_details", None) + if prompt_details and cache_read is None: + cache_read = getattr(prompt_details, "cached_tokens", None) + + return ( + cache_creation if cache_creation and cache_creation > 0 else None, + cache_read if cache_read and cache_read > 0 else None, + ) + except (KeyError, AttributeError): + return None, None + + def _extract_task_id(task: Any) -> Optional[str]: """Extract task_id from task parameter (can be str or Response object). diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py index da86d734d..f84aedcfa 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py @@ -570,6 +570,14 @@ def _update_invocation_from_response( invocation.input_tokens = input_tokens invocation.output_tokens = output_tokens + # Extract cache token usage + from ..utils.common import _extract_cache_tokens + cache_creation, cache_read = _extract_cache_tokens(response) + if cache_creation is not None: + invocation.usage_cache_creation_input_tokens = cache_creation + if cache_read is not None: + invocation.usage_cache_read_input_tokens = cache_read + # Extract response model name (if available) response_model = _safe_get(response, "model") if response_model: diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/tests/test_embedding.py b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/tests/test_embedding.py index 898516798..ac1bb50f4 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/tests/test_embedding.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/tests/test_embedding.py @@ -141,7 +141,7 @@ def test_text_embedding_basic(instrument, span_exporter): """Test basic text embedding call.""" response = TextEmbedding.call( - model="text-embedding-v1", input="Hello, world!" + model="text-embedding-v4", input="Hello, world!" ) assert response is not None @@ -165,7 +165,7 @@ def test_text_embedding_basic(instrument, span_exporter): # Assert all span attributes _assert_embedding_span_attributes( span, - request_model="text-embedding-v1", + request_model="text-embedding-v4", response=response, input_tokens=input_tokens, ) @@ -178,7 +178,7 @@ def test_text_embedding_batch(instrument, span_exporter): """Test text embedding with batch input.""" response = TextEmbedding.call( - model="text-embedding-v1", input=["Hello", "World"] + model="text-embedding-v4", input=["Hello", "World"] ) assert response is not None @@ -202,7 +202,7 @@ def test_text_embedding_batch(instrument, span_exporter): # Assert all span attributes _assert_embedding_span_attributes( span, - request_model="text-embedding-v1", + request_model="text-embedding-v4", response=response, input_tokens=input_tokens, ) @@ -215,7 +215,7 @@ def test_text_embedding_with_text_type(instrument, span_exporter): """Test text embedding with text_type parameter.""" response = TextEmbedding.call( - model="text-embedding-v1", + model="text-embedding-v4", input="What is machine learning?", text_type="query", ) @@ -241,7 +241,7 @@ def test_text_embedding_with_text_type(instrument, span_exporter): # Assert all span attributes _assert_embedding_span_attributes( span, - request_model="text-embedding-v1", + request_model="text-embedding-v4", response=response, input_tokens=input_tokens, ) @@ -254,7 +254,7 @@ def test_text_embedding_with_dimension(instrument, span_exporter): """Test text embedding with dimension parameter.""" response = TextEmbedding.call( - model="text-embedding-v1", + model="text-embedding-v4", input="What is machine learning?", dimension=512, ) @@ -280,7 +280,7 @@ def test_text_embedding_with_dimension(instrument, span_exporter): # Assert all span attributes including dimension_count _assert_embedding_span_attributes( span, - request_model="text-embedding-v1", + request_model="text-embedding-v4", response=response, input_tokens=input_tokens, dimension_count=512, # Should be captured from request diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py b/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py index 5d435e36b..a605b8870 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py @@ -693,6 +693,26 @@ def update_llm_invocation_from_response( if output_tokens > 0: invocation.output_tokens = output_tokens + # Extract cache token usage + usage = getattr(response, "usage", None) + if usage is not None: + # OpenAI-compatible: prompt_tokens_details.cached_tokens + prompt_details = getattr(usage, "prompt_tokens_details", None) + if prompt_details is not None: + cached = getattr(prompt_details, "cached_tokens", None) + if cached and cached > 0: + invocation.usage_cache_read_input_tokens = cached + # Direct fields (some providers) + cache_creation = ( + getattr(usage, "cache_creation_input_tokens", None) + or getattr(usage, "cache_creation_tokens", None) + ) + if cache_creation and cache_creation > 0: + invocation.usage_cache_creation_input_tokens = cache_creation + cache_read = getattr(usage, "cache_read_input_tokens", None) + if cache_read and cache_read > 0 and not invocation.usage_cache_read_input_tokens: + invocation.usage_cache_read_input_tokens = cache_read + return input_tokens, output_tokens, total_tokens diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/patch.py index 7409c40be..d83ad82a2 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/patch.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/patch.py @@ -346,6 +346,15 @@ def wrap_generate_next_message( if usage and isinstance(usage, dict): invocation.input_tokens = usage.get("prompt_tokens") invocation.output_tokens = usage.get("completion_tokens") + # Cache tokens + prompt_details = usage.get("prompt_tokens_details") + if isinstance(prompt_details, dict): + cached = prompt_details.get("cached_tokens") + if cached and cached > 0: + invocation.usage_cache_read_input_tokens = cached + cache_creation = usage.get("cache_creation_input_tokens") + if cache_creation and cache_creation > 0: + invocation.usage_cache_creation_input_tokens = cache_creation handler.stop_invoke_agent(invocation) return result @@ -415,6 +424,15 @@ def wrap_generate( if usage and isinstance(usage, dict): invocation.input_tokens = usage.get("prompt_tokens") invocation.output_tokens = usage.get("completion_tokens") + # Cache tokens (OpenAI-compatible format) + prompt_details = usage.get("prompt_tokens_details") + if isinstance(prompt_details, dict): + cached = prompt_details.get("cached_tokens") + if cached and cached > 0: + invocation.usage_cache_read_input_tokens = cached + cache_creation = usage.get("cache_creation_input_tokens") + if cache_creation and cache_creation > 0: + invocation.usage_cache_creation_input_tokens = cache_creation handler.stop_llm(invocation) return result From d99dd0026b4ebd649eb398dc400b2a84e6dc9a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=89=A7=E6=80=9D?= Date: Mon, 22 Jun 2026 16:46:12 +0800 Subject: [PATCH 4/6] feat(claw-eval): add gen_ai.provider.name to invoke_agent_internal span Add _infer_provider_name() helper to resolve provider name from the provider instance. This fixes a P0 compliance gap where the Required attribute gen_ai.provider.name was missing on invoke_agent_internal spans in the CLAW-eval instrumentation. Change-Id: Iad6b28ea1c9f09b4fae4aef03c625b35fcdff3d0 Co-developed-by: Qoder --- .../claw_eval/internal/wrappers.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-claw-eval/src/opentelemetry/instrumentation/claw_eval/internal/wrappers.py b/instrumentation-loongsuite/loongsuite-instrumentation-claw-eval/src/opentelemetry/instrumentation/claw_eval/internal/wrappers.py index 4f8b246ea..16fb8dd3b 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-claw-eval/src/opentelemetry/instrumentation/claw_eval/internal/wrappers.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-claw-eval/src/opentelemetry/instrumentation/claw_eval/internal/wrappers.py @@ -62,6 +62,34 @@ # string instead of reading it from ``gen_ai_attributes``. GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" +def _infer_provider_name(provider: Any) -> str: + """Infer gen_ai.provider.name from the provider instance.""" + if provider is None: + return "claw-eval" + # Try provider_name attribute + pn = getattr(provider, "provider_name", None) + if pn: + return str(pn) + # Infer from class name + cls_name = type(provider).__name__.lower() + if "openai" in cls_name: + return "openai" + if "anthropic" in cls_name: + return "anthropic" + if "dashscope" in cls_name: + return "dashscope" + # Infer from model_id + model_id = str(getattr(provider, "model_id", "") or "") + if model_id.startswith(("gpt-", "o1-", "o3-", "chatgpt-")): + return "openai" + if model_id.startswith(("claude-",)): + return "anthropic" + if model_id.startswith(("qwen",)): + return "dashscope" + return "openai" + + + # --------------------------------------------------------------------------- # ContextVars for STEP lifecycle & compact-depth tracking # --------------------------------------------------------------------------- @@ -514,6 +542,13 @@ def __call__(self, wrapped, instance, args, kwargs): span.set_attribute(GenAI.GEN_AI_AGENT_NAME, "claw-eval") span.set_attribute("claw_eval.task_id", str(task_id)) + # Set gen_ai.provider.name (Required attribute) + _provider_name = _infer_provider_name(provider) + if _provider_name: + span.set_attribute( + GenAI.GEN_AI_PROVIDER_NAME, _provider_name + ) + model_id = "" if provider is not None: model_id = str(getattr(provider, "model_id", "") or "") From 8906ae655236918a53ea7e4e64a04a076eb7de72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=89=A7=E6=80=9D?= Date: Mon, 22 Jun 2026 19:28:59 +0800 Subject: [PATCH 5/6] fix: resolve CI failures (lint, format, typecheck, changelog) - Add # noqa: PLC0415 for intentional in-function imports in dashscope and hermes-agent - Fix unsorted imports (I001) in loongsuite-distro - Remove unnecessary # type: ignore comments in vertexai/patch.py - Apply ruff format to all affected files - Add CHANGELOG-loongsuite.md entry for semconv changes Change-Id: Ibb440bd9e057fa82a91710db85380311d11a8c86 Co-developed-by: Qoder --- CHANGELOG-loongsuite.md | 4 ++ .../instrumentation/vertexai/patch.py | 4 +- .../claude_agent_sdk/__init__.py | 51 +++++++++---------- .../claw_eval/internal/wrappers.py | 6 +-- .../instrumentation/dashscope/utils/common.py | 11 ++-- .../dashscope/utils/generation.py | 3 +- .../instrumentation/hermes_agent/helpers.py | 15 +++--- .../instrumentation/litellm/_wrapper.py | 12 ++--- .../minisweagent/internal/agent_wrappers.py | 3 +- .../instrumentation/vita/patch.py | 12 +++-- .../instrumentation/vita/utils.py | 1 - .../src/loongsuite/distro/__init__.py | 12 +++-- loongsuite-distro/tests/test_configurator.py | 16 ++---- loongsuite-distro/tests/test_resource.py | 8 ++- 14 files changed, 79 insertions(+), 79 deletions(-) diff --git a/CHANGELOG-loongsuite.md b/CHANGELOG-loongsuite.md index 15610be6a..b819eab0e 100644 --- a/CHANGELOG-loongsuite.md +++ b/CHANGELOG-loongsuite.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Adopt upstream semantic conventions for LoongSuite instrumentation packages. + ## Version 0.5.0 (2026-05-11) ### Fixed diff --git a/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/patch.py b/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/patch.py index 481277d69..0cc21ad39 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/patch.py +++ b/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/patch.py @@ -173,7 +173,7 @@ def handle_response( | None, ) -> None: attributes = ( - get_server_attributes(instance.api_endpoint) # type: ignore[reportUnknownMemberType] + get_server_attributes(instance.api_endpoint) | request_attributes | get_genai_response_attributes(response) ) @@ -257,7 +257,7 @@ def _with_default_instrumentation( kwargs: Any, ): params = _extract_params(*args, **kwargs) - api_endpoint: str = instance.api_endpoint # type: ignore[reportUnknownMemberType] + api_endpoint: str = instance.api_endpoint span_attributes = { **get_genai_request_attributes(False, params), **get_server_attributes(api_endpoint), diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-claude-agent-sdk/src/opentelemetry/instrumentation/claude_agent_sdk/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-claude-agent-sdk/src/opentelemetry/instrumentation/claude_agent_sdk/__init__.py index 7e34fa169..ecab2358d 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-claude-agent-sdk/src/opentelemetry/instrumentation/claude_agent_sdk/__init__.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-claude-agent-sdk/src/opentelemetry/instrumentation/claude_agent_sdk/__init__.py @@ -102,15 +102,14 @@ def _instrument(self, **kwargs: Any) -> None: wrap_function_wrapper( module="claude_agent_sdk", name="ClaudeSDKClient.__init__", - wrapper=lambda wrapped, - instance, - args, - kwargs: wrap_claude_client_init( - wrapped, - instance, - args, - kwargs, - handler=ClaudeAgentSDKInstrumentor._handler, + wrapper=lambda wrapped, instance, args, kwargs: ( + wrap_claude_client_init( + wrapped, + instance, + args, + kwargs, + handler=ClaudeAgentSDKInstrumentor._handler, + ) ), ) except Exception as e: @@ -122,15 +121,14 @@ def _instrument(self, **kwargs: Any) -> None: wrap_function_wrapper( module="claude_agent_sdk", name="ClaudeSDKClient.query", - wrapper=lambda wrapped, - instance, - args, - kwargs: wrap_claude_client_query( - wrapped, - instance, - args, - kwargs, - handler=ClaudeAgentSDKInstrumentor._handler, + wrapper=lambda wrapped, instance, args, kwargs: ( + wrap_claude_client_query( + wrapped, + instance, + args, + kwargs, + handler=ClaudeAgentSDKInstrumentor._handler, + ) ), ) except Exception as e: @@ -140,15 +138,14 @@ def _instrument(self, **kwargs: Any) -> None: wrap_function_wrapper( module="claude_agent_sdk", name="ClaudeSDKClient.receive_response", - wrapper=lambda wrapped, - instance, - args, - kwargs: wrap_claude_client_receive_response( - wrapped, - instance, - args, - kwargs, - handler=ClaudeAgentSDKInstrumentor._handler, + wrapper=lambda wrapped, instance, args, kwargs: ( + wrap_claude_client_receive_response( + wrapped, + instance, + args, + kwargs, + handler=ClaudeAgentSDKInstrumentor._handler, + ) ), ) except Exception as e: diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-claw-eval/src/opentelemetry/instrumentation/claw_eval/internal/wrappers.py b/instrumentation-loongsuite/loongsuite-instrumentation-claw-eval/src/opentelemetry/instrumentation/claw_eval/internal/wrappers.py index 16fb8dd3b..48965a8f0 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-claw-eval/src/opentelemetry/instrumentation/claw_eval/internal/wrappers.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-claw-eval/src/opentelemetry/instrumentation/claw_eval/internal/wrappers.py @@ -62,6 +62,7 @@ # string instead of reading it from ``gen_ai_attributes``. GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" + def _infer_provider_name(provider: Any) -> str: """Infer gen_ai.provider.name from the provider instance.""" if provider is None: @@ -89,7 +90,6 @@ def _infer_provider_name(provider: Any) -> str: return "openai" - # --------------------------------------------------------------------------- # ContextVars for STEP lifecycle & compact-depth tracking # --------------------------------------------------------------------------- @@ -545,9 +545,7 @@ def __call__(self, wrapped, instance, args, kwargs): # Set gen_ai.provider.name (Required attribute) _provider_name = _infer_provider_name(provider) if _provider_name: - span.set_attribute( - GenAI.GEN_AI_PROVIDER_NAME, _provider_name - ) + span.set_attribute(GenAI.GEN_AI_PROVIDER_NAME, _provider_name) model_id = "" if provider is not None: diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/common.py b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/common.py index eedacb83a..6ee9819a6 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/common.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/common.py @@ -88,7 +88,9 @@ def _extract_usage(response: Any) -> tuple[Optional[int], Optional[int]]: return None, None -def _extract_cache_tokens(response: Any) -> tuple[Optional[int], Optional[int]]: +def _extract_cache_tokens( + response: Any, +) -> tuple[Optional[int], Optional[int]]: """Extract cache token usage from DashScope response. Args: @@ -106,10 +108,9 @@ def _extract_cache_tokens(response: Any) -> tuple[Optional[int], Optional[int]]: return None, None # DashScope may report cache tokens in various fields - cache_creation = ( - getattr(usage, "cache_creation_input_tokens", None) - or getattr(usage, "cache_creation_tokens", None) - ) + cache_creation = getattr( + usage, "cache_creation_input_tokens", None + ) or getattr(usage, "cache_creation_tokens", None) cache_read = ( getattr(usage, "cache_read_input_tokens", None) or getattr(usage, "cache_read_tokens", None) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py index f84aedcfa..fdccd5730 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py @@ -571,7 +571,8 @@ def _update_invocation_from_response( invocation.output_tokens = output_tokens # Extract cache token usage - from ..utils.common import _extract_cache_tokens + from ..utils.common import _extract_cache_tokens # noqa: PLC0415 + cache_creation, cache_read = _extract_cache_tokens(response) if cache_creation is not None: invocation.usage_cache_creation_input_tokens = cache_creation diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py b/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py index a605b8870..0bed4c944 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py @@ -617,7 +617,7 @@ def _extract_server_info(instance: Any) -> tuple[str | None, int | None]: return None, None base_url_str = str(base_url).rstrip("/") try: - from urllib.parse import urlparse + from urllib.parse import urlparse # noqa: PLC0415 parsed = urlparse(base_url_str) address = parsed.hostname @@ -703,14 +703,17 @@ def update_llm_invocation_from_response( if cached and cached > 0: invocation.usage_cache_read_input_tokens = cached # Direct fields (some providers) - cache_creation = ( - getattr(usage, "cache_creation_input_tokens", None) - or getattr(usage, "cache_creation_tokens", None) - ) + cache_creation = getattr( + usage, "cache_creation_input_tokens", None + ) or getattr(usage, "cache_creation_tokens", None) if cache_creation and cache_creation > 0: invocation.usage_cache_creation_input_tokens = cache_creation cache_read = getattr(usage, "cache_read_input_tokens", None) - if cache_read and cache_read > 0 and not invocation.usage_cache_read_input_tokens: + if ( + cache_read + and cache_read > 0 + and not invocation.usage_cache_read_input_tokens + ): invocation.usage_cache_read_input_tokens = cache_read return input_tokens, output_tokens, total_tokens diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-litellm/src/opentelemetry/instrumentation/litellm/_wrapper.py b/instrumentation-loongsuite/loongsuite-instrumentation-litellm/src/opentelemetry/instrumentation/litellm/_wrapper.py index f4a5bb9ff..ff3fb870e 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-litellm/src/opentelemetry/instrumentation/litellm/_wrapper.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-litellm/src/opentelemetry/instrumentation/litellm/_wrapper.py @@ -94,10 +94,8 @@ def __call__(self, *args, **kwargs): callback=None, invocation=invocation, ) - stream_wrapper.callback = ( - lambda span, - last_chunk, - error: self._handle_stream_end_with_handler( + stream_wrapper.callback = lambda span, last_chunk, error: ( + self._handle_stream_end_with_handler( invocation, last_chunk, error, stream_wrapper ) ) @@ -240,10 +238,8 @@ async def __call__(self, *args, **kwargs): callback=None, invocation=invocation, ) - stream_wrapper.callback = ( - lambda span, - last_chunk, - error: self._handle_stream_end_with_handler( + stream_wrapper.callback = lambda span, last_chunk, error: ( + self._handle_stream_end_with_handler( invocation, last_chunk, error, stream_wrapper ) ) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-minisweagent/src/opentelemetry/instrumentation/minisweagent/internal/agent_wrappers.py b/instrumentation-loongsuite/loongsuite-instrumentation-minisweagent/src/opentelemetry/instrumentation/minisweagent/internal/agent_wrappers.py index 3d1b9b931..c00401293 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-minisweagent/src/opentelemetry/instrumentation/minisweagent/internal/agent_wrappers.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-minisweagent/src/opentelemetry/instrumentation/minisweagent/internal/agent_wrappers.py @@ -113,7 +113,8 @@ def __call__( han.start_entry(entry_inv, context=context_api.get_current()) inv = InvokeAgentInvocation( - provider="minisweagent", agent_name=agent_name, + provider="minisweagent", + agent_name=agent_name, agent_id=agent_name, ) inv.request_model = _request_model_from_agent(instance) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/patch.py index d83ad82a2..03ae0df52 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/patch.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/patch.py @@ -354,7 +354,9 @@ def wrap_generate_next_message( invocation.usage_cache_read_input_tokens = cached cache_creation = usage.get("cache_creation_input_tokens") if cache_creation and cache_creation > 0: - invocation.usage_cache_creation_input_tokens = cache_creation + invocation.usage_cache_creation_input_tokens = ( + cache_creation + ) handler.stop_invoke_agent(invocation) return result @@ -409,7 +411,9 @@ def wrap_generate( invocation.response_model_name = model # response_id - resp_id = getattr(result, "id", None) or getattr(result, "response_id", None) + resp_id = getattr(result, "id", None) or getattr( + result, "response_id", None + ) if resp_id: invocation.response_id = str(resp_id) @@ -432,7 +436,9 @@ def wrap_generate( invocation.usage_cache_read_input_tokens = cached cache_creation = usage.get("cache_creation_input_tokens") if cache_creation and cache_creation > 0: - invocation.usage_cache_creation_input_tokens = cache_creation + invocation.usage_cache_creation_input_tokens = ( + cache_creation + ) handler.stop_llm(invocation) return result diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/utils.py b/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/utils.py index 308d42c27..b6e3db8cd 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/utils.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/utils.py @@ -149,7 +149,6 @@ def _infer_provider(model_name: str) -> str: return "unknown" - def _infer_server_address(model: str) -> tuple[str | None, int | None]: """Infer server address from model name for vita.""" model_lower = model.lower() if model else "" diff --git a/loongsuite-distro/src/loongsuite/distro/__init__.py b/loongsuite-distro/src/loongsuite/distro/__init__.py index 30b1ae973..eef23166a 100644 --- a/loongsuite-distro/src/loongsuite/distro/__init__.py +++ b/loongsuite-distro/src/loongsuite/distro/__init__.py @@ -15,6 +15,8 @@ import os from typing import Any +from loongsuite.distro.resource import LoongSuiteResourceDetector + from opentelemetry.environment_variables import ( OTEL_LOGS_EXPORTER, OTEL_METRICS_EXPORTER, @@ -24,8 +26,6 @@ from opentelemetry.sdk._configuration import _OTelSDKConfigurator from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_PROTOCOL -from loongsuite.distro.resource import LoongSuiteResourceDetector - class LoongSuiteConfigurator(_OTelSDKConfigurator): """ @@ -37,8 +37,12 @@ class LoongSuiteConfigurator(_OTelSDKConfigurator): """ def _configure(self, **kwargs: Any) -> None: - resource_attributes = dict(kwargs.pop("resource_attributes", None) or {}) - resource_attributes.update(LoongSuiteResourceDetector().detect().attributes) + resource_attributes = dict( + kwargs.pop("resource_attributes", None) or {} + ) + resource_attributes.update( + LoongSuiteResourceDetector().detect().attributes + ) super()._configure(resource_attributes=resource_attributes, **kwargs) diff --git a/loongsuite-distro/tests/test_configurator.py b/loongsuite-distro/tests/test_configurator.py index 1c9a0696e..3a81f9916 100644 --- a/loongsuite-distro/tests/test_configurator.py +++ b/loongsuite-distro/tests/test_configurator.py @@ -28,9 +28,7 @@ def test_configure_injects_loongsuite_attributes(self, mock_init): LoongSuiteConfigurator().configure() mock_init.assert_called_once() - resource_attributes = mock_init.call_args.kwargs[ - "resource_attributes" - ] + resource_attributes = mock_init.call_args.kwargs["resource_attributes"] self.assertIn(HOST_IP, resource_attributes) self.assertEqual( resource_attributes[GEN_AI_INSTRUMENTATION_SDK_NAME], @@ -43,16 +41,10 @@ def test_configure_preserves_existing_attributes(self, mock_init): resource_attributes={"service.name": "my-service"} ) - resource_attributes = mock_init.call_args.kwargs[ - "resource_attributes" - ] - self.assertEqual( - resource_attributes["service.name"], "my-service" - ) + resource_attributes = mock_init.call_args.kwargs["resource_attributes"] + self.assertEqual(resource_attributes["service.name"], "my-service") self.assertIn(HOST_IP, resource_attributes) - self.assertIn( - GEN_AI_INSTRUMENTATION_SDK_NAME, resource_attributes - ) + self.assertIn(GEN_AI_INSTRUMENTATION_SDK_NAME, resource_attributes) @mock.patch("opentelemetry.sdk._configuration._initialize_components") def test_configure_forwards_other_kwargs(self, mock_init): diff --git a/loongsuite-distro/tests/test_resource.py b/loongsuite-distro/tests/test_resource.py index 9c68c99d5..24af342e1 100644 --- a/loongsuite-distro/tests/test_resource.py +++ b/loongsuite-distro/tests/test_resource.py @@ -15,8 +15,6 @@ import unittest from unittest import mock -from opentelemetry.sdk.resources import Resource, ResourceDetector - from loongsuite.distro.resource import ( GEN_AI_INSTRUMENTATION_SDK_NAME, HOST_IP, @@ -25,12 +23,12 @@ _get_host_ip_with_pid, ) +from opentelemetry.sdk.resources import Resource, ResourceDetector + class TestLoongSuiteResourceDetector(unittest.TestCase): def test_is_resource_detector(self): - self.assertIsInstance( - LoongSuiteResourceDetector(), ResourceDetector - ) + self.assertIsInstance(LoongSuiteResourceDetector(), ResourceDetector) def test_detect_returns_resource(self): resource = LoongSuiteResourceDetector().detect() From 56c20822a1248c4abb4fc3bf7a2877858bfbc6ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=89=A7=E6=80=9D?= Date: Mon, 22 Jun 2026 21:54:53 +0800 Subject: [PATCH 6/6] fix: address all PR review comments - resource.py: Use raw IP for host.ip, add service.instance.id for - - distro/__init__.py: Use setdefault so user-provided values take precedence - hermes_agent/helpers.py: Handle schemeless URLs in _extract_server_info, return (None, None) when hostname is falsy; use agent_name for agent_id - vita/utils.py: Remove unused _infer_server_address function - claw-eval/wrappers.py: Change default fallback from 'openai' to 'unknown', add Google/Gemini provider detection, remove dead if-guard - agentscope/patch.py: Add comment explaining tool_type='function' assumption - dashscope/generation.py: Add comment about hardcoded server_address - tox-loongsuite.ini: Add loongsuite-distro test environment - test_resource.py/test_configurator.py: Update tests for new behavior Change-Id: I9128e7bb1db9bd10b1529a40c5f450e61dcc2aa8 Co-developed-by: Qoder --- .../instrumentation/agentscope/patch.py | 2 ++ .../claw_eval/internal/wrappers.py | 9 ++++++--- .../instrumentation/dashscope/utils/generation.py | 2 ++ .../instrumentation/hermes_agent/helpers.py | 7 ++++++- .../opentelemetry/instrumentation/vita/utils.py | 14 -------------- .../src/loongsuite/distro/__init__.py | 7 ++++--- .../src/loongsuite/distro/resource.py | 9 +++++++-- loongsuite-distro/tests/test_configurator.py | 12 ++++++++++-- loongsuite-distro/tests/test_resource.py | 15 +++++++++++++-- tox-loongsuite.ini | 8 ++++++++ 10 files changed, 58 insertions(+), 27 deletions(-) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/patch.py index 7e5d4fa68..d85c6f4a6 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/patch.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/patch.py @@ -401,6 +401,8 @@ async def wrap_tool_call(wrapped, instance, args, kwargs, handler): matched_skill = _match_skill_for_tool(instance, tool_args) # Create invocation object with all tool data + # NOTE: tool_type is set to "function" as agentscope currently only + # supports function-type tools. Update when other types are supported. invocation = ExecuteToolInvocation( tool_name=tool_name, tool_type="function", diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-claw-eval/src/opentelemetry/instrumentation/claw_eval/internal/wrappers.py b/instrumentation-loongsuite/loongsuite-instrumentation-claw-eval/src/opentelemetry/instrumentation/claw_eval/internal/wrappers.py index 48965a8f0..f9c0d82df 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-claw-eval/src/opentelemetry/instrumentation/claw_eval/internal/wrappers.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-claw-eval/src/opentelemetry/instrumentation/claw_eval/internal/wrappers.py @@ -79,6 +79,8 @@ def _infer_provider_name(provider: Any) -> str: return "anthropic" if "dashscope" in cls_name: return "dashscope" + if "gemini" in cls_name or "google" in cls_name: + return "google" # Infer from model_id model_id = str(getattr(provider, "model_id", "") or "") if model_id.startswith(("gpt-", "o1-", "o3-", "chatgpt-")): @@ -87,7 +89,9 @@ def _infer_provider_name(provider: Any) -> str: return "anthropic" if model_id.startswith(("qwen",)): return "dashscope" - return "openai" + if model_id.startswith(("gemini",)): + return "google" + return "unknown" # --------------------------------------------------------------------------- @@ -544,8 +548,7 @@ def __call__(self, wrapped, instance, args, kwargs): # Set gen_ai.provider.name (Required attribute) _provider_name = _infer_provider_name(provider) - if _provider_name: - span.set_attribute(GenAI.GEN_AI_PROVIDER_NAME, _provider_name) + span.set_attribute(GenAI.GEN_AI_PROVIDER_NAME, _provider_name) model_id = "" if provider is not None: diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py index fdccd5730..7fe5bcc1f 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/src/opentelemetry/instrumentation/dashscope/utils/generation.py @@ -469,6 +469,8 @@ def _create_invocation_from_generation( invocation = LLMInvocation(request_model=request_model) invocation.provider = "dashscope" + # DashScope SDK always targets dashscope.aliyuncs.com; if custom endpoint + # support is added in the future, extract from instance/env instead. invocation.server_address = "dashscope.aliyuncs.com" invocation.server_port = 443 invocation.input_messages = _extract_input_messages(kwargs) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py b/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py index 0bed4c944..7f86855ee 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py @@ -571,7 +571,7 @@ def create_agent_invocation( invocation = InvokeAgentInvocation( provider=_HERMES_AGENT_SYSTEM, agent_name="Hermes", - agent_id="hermes-agent", + agent_id="Hermes", conversation_id=getattr(instance, "session_id", None), request_model=getattr(instance, "model", None), input_messages=[ @@ -616,11 +616,16 @@ def _extract_server_info(instance: Any) -> tuple[str | None, int | None]: if not base_url: return None, None base_url_str = str(base_url).rstrip("/") + # Handle schemeless URLs by prepending "//" for urlparse. + if "://" not in base_url_str: + base_url_str = "//" + base_url_str try: from urllib.parse import urlparse # noqa: PLC0415 parsed = urlparse(base_url_str) address = parsed.hostname + if not address: + return None, None port = parsed.port if port is None: port = 443 if parsed.scheme == "https" else 80 diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/utils.py b/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/utils.py index b6e3db8cd..f2307bd85 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/utils.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-vita/src/opentelemetry/instrumentation/vita/utils.py @@ -149,20 +149,6 @@ def _infer_provider(model_name: str) -> str: return "unknown" -def _infer_server_address(model: str) -> tuple[str | None, int | None]: - """Infer server address from model name for vita.""" - model_lower = model.lower() if model else "" - if "qwen" in model_lower or "dashscope" in model_lower: - return "dashscope.aliyuncs.com", 443 - if "gpt" in model_lower or "openai" in model_lower: - return "api.openai.com", 443 - if "claude" in model_lower or "anthropic" in model_lower: - return "api.anthropic.com", 443 - if "deepseek" in model_lower: - return "api.deepseek.com", 443 - return None, None - - def _get_tool_definitions( tools: Any, ) -> Optional[List[FunctionToolDefinition]]: diff --git a/loongsuite-distro/src/loongsuite/distro/__init__.py b/loongsuite-distro/src/loongsuite/distro/__init__.py index eef23166a..c9e2217d3 100644 --- a/loongsuite-distro/src/loongsuite/distro/__init__.py +++ b/loongsuite-distro/src/loongsuite/distro/__init__.py @@ -40,9 +40,10 @@ def _configure(self, **kwargs: Any) -> None: resource_attributes = dict( kwargs.pop("resource_attributes", None) or {} ) - resource_attributes.update( - LoongSuiteResourceDetector().detect().attributes - ) + # Use setdefault so user-provided values take precedence over detector. + detected = LoongSuiteResourceDetector().detect().attributes + for k, v in detected.items(): + resource_attributes.setdefault(k, v) super()._configure(resource_attributes=resource_attributes, **kwargs) diff --git a/loongsuite-distro/src/loongsuite/distro/resource.py b/loongsuite-distro/src/loongsuite/distro/resource.py index 1542d99d1..d9cca93e9 100644 --- a/loongsuite-distro/src/loongsuite/distro/resource.py +++ b/loongsuite-distro/src/loongsuite/distro/resource.py @@ -22,6 +22,7 @@ # Resource attribute keys contributed by LoongSuite. HOST_IP = "host.ip" +SERVICE_INSTANCE_ID = "service.instance.id" GEN_AI_INSTRUMENTATION_SDK_NAME = "gen_ai.instrumentation.sdk.name" # Fixed value identifying the GenAI instrumentation SDK shipped with LoongSuite. @@ -67,14 +68,18 @@ class LoongSuiteResourceDetector(ResourceDetector): Contributes the following attributes to the resource: - * ``host.ip`` formatted as ``-`` (e.g. ``127.0.0.1-1``). + * ``host.ip`` — the raw host IP address (e.g. ``127.0.0.1``). + * ``service.instance.id`` — ``-`` uniquely identifying the process + instance (e.g. ``127.0.0.1-1``). * ``gen_ai.instrumentation.sdk.name`` set to ``loongsuite-genai-utils``. """ def detect(self) -> Resource: + host_ip = _get_host_ip() return Resource( { - HOST_IP: _get_host_ip_with_pid(), + HOST_IP: host_ip, + SERVICE_INSTANCE_ID: f"{host_ip}-{os.getpid()}", GEN_AI_INSTRUMENTATION_SDK_NAME: _GEN_AI_INSTRUMENTATION_SDK_NAME_VALUE, } ) diff --git a/loongsuite-distro/tests/test_configurator.py b/loongsuite-distro/tests/test_configurator.py index 3a81f9916..e8c1c3ac2 100644 --- a/loongsuite-distro/tests/test_configurator.py +++ b/loongsuite-distro/tests/test_configurator.py @@ -19,6 +19,7 @@ from loongsuite.distro.resource import ( GEN_AI_INSTRUMENTATION_SDK_NAME, HOST_IP, + SERVICE_INSTANCE_ID, ) @@ -30,6 +31,7 @@ def test_configure_injects_loongsuite_attributes(self, mock_init): mock_init.assert_called_once() resource_attributes = mock_init.call_args.kwargs["resource_attributes"] self.assertIn(HOST_IP, resource_attributes) + self.assertIn(SERVICE_INSTANCE_ID, resource_attributes) self.assertEqual( resource_attributes[GEN_AI_INSTRUMENTATION_SDK_NAME], "loongsuite-genai-utils", @@ -37,13 +39,19 @@ def test_configure_injects_loongsuite_attributes(self, mock_init): @mock.patch("opentelemetry.sdk._configuration._initialize_components") def test_configure_preserves_existing_attributes(self, mock_init): + """User-provided values take precedence over detector values.""" LoongSuiteConfigurator().configure( - resource_attributes={"service.name": "my-service"} + resource_attributes={ + "service.name": "my-service", + HOST_IP: "custom-ip", + } ) resource_attributes = mock_init.call_args.kwargs["resource_attributes"] self.assertEqual(resource_attributes["service.name"], "my-service") - self.assertIn(HOST_IP, resource_attributes) + # User-provided host.ip should NOT be overwritten by detector + self.assertEqual(resource_attributes[HOST_IP], "custom-ip") + self.assertIn(SERVICE_INSTANCE_ID, resource_attributes) self.assertIn(GEN_AI_INSTRUMENTATION_SDK_NAME, resource_attributes) @mock.patch("opentelemetry.sdk._configuration._initialize_components") diff --git a/loongsuite-distro/tests/test_resource.py b/loongsuite-distro/tests/test_resource.py index 24af342e1..289424fee 100644 --- a/loongsuite-distro/tests/test_resource.py +++ b/loongsuite-distro/tests/test_resource.py @@ -18,6 +18,7 @@ from loongsuite.distro.resource import ( GEN_AI_INSTRUMENTATION_SDK_NAME, HOST_IP, + SERVICE_INSTANCE_ID, LoongSuiteResourceDetector, _get_host_ip, _get_host_ip_with_pid, @@ -37,8 +38,15 @@ def test_detect_returns_resource(self): def test_detect_contains_expected_keys(self): attributes = LoongSuiteResourceDetector().detect().attributes self.assertIn(HOST_IP, attributes) + self.assertIn(SERVICE_INSTANCE_ID, attributes) self.assertIn(GEN_AI_INSTRUMENTATION_SDK_NAME, attributes) + def test_host_ip_is_raw_ip(self): + """host.ip should contain a raw IP address without PID suffix.""" + attributes = LoongSuiteResourceDetector().detect().attributes + # Raw IP should not contain a dash-PID suffix + self.assertNotIn("-", attributes[HOST_IP].rsplit(".", 1)[-1]) + def test_gen_ai_instrumentation_sdk_name_value(self): attributes = LoongSuiteResourceDetector().detect().attributes self.assertEqual( @@ -50,9 +58,12 @@ def test_gen_ai_instrumentation_sdk_name_value(self): @mock.patch( "loongsuite.distro.resource._get_host_ip", return_value="127.0.0.1" ) - def test_host_ip_format_is_ip_dash_pid(self, _mock_ip, _mock_pid): + def test_service_instance_id_format_is_ip_dash_pid( + self, _mock_ip, _mock_pid + ): attributes = LoongSuiteResourceDetector().detect().attributes - self.assertEqual(attributes[HOST_IP], "127.0.0.1-1") + self.assertEqual(attributes[SERVICE_INSTANCE_ID], "127.0.0.1-1") + self.assertEqual(attributes[HOST_IP], "127.0.0.1") @mock.patch("loongsuite.distro.resource.os.getpid", return_value=42) @mock.patch( diff --git a/tox-loongsuite.ini b/tox-loongsuite.ini index 0eda9c140..901f6a2b9 100644 --- a/tox-loongsuite.ini +++ b/tox-loongsuite.ini @@ -126,6 +126,9 @@ envlist = py3{10,11,12,13}-test-loongsuite-instrumentation-wildtool lint-loongsuite-instrumentation-wildtool + ; loongsuite-distro + py3{9,10,11,12,13}-test-loongsuite-distro + [testenv] test_deps = opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api @@ -234,6 +237,9 @@ deps = wildtool: {[testenv]test_deps} wildtool: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-wildtool/test-requirements.txt + loongsuite-distro: {[testenv]test_deps} + loongsuite-distro: {toxinidir}/loongsuite-distro + ; FIXME: add coverage testing allowlist_externals = sh @@ -334,6 +340,8 @@ commands = test-loongsuite-instrumentation-wildtool: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-wildtool/tests {posargs} lint-loongsuite-instrumentation-wildtool: python -m ruff check {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-wildtool + test-loongsuite-distro: pytest {toxinidir}/loongsuite-distro/tests {posargs} + ; TODO: add coverage commands ; coverage: {toxinidir}/scripts/coverage.sh