Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Info] tool_type is unconditionally set to "function" here and across all other instrumentations in this PR. Some agent frameworks support non-function tool types (e.g., code_interpreter, retrieval, builtin). If the framework exposes the actual tool type, consider deriving it rather than hardcoding. If "function" is the only supported type today, a comment documenting this assumption would help future maintainers.

tool_call_id=tool_id,
tool_description=tool_description,
tool_call_arguments=tool_args,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,8 @@ def _create_invocation_from_generation(

invocation = LLMInvocation(request_model=request_model)
invocation.provider = "dashscope"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Info] server_address/server_port are hardcoded to "dashscope.aliyuncs.com"/443. This is inconsistent with the hermes-agent instrumentation in this same PR, which dynamically extracts server info from base_url. Users pointing DashScope-compatible SDKs at custom or regional endpoints won't see their actual endpoint reflected. Consider extracting from instance.base_url (or os.environ for DASHSCOPE_* base URL overrides) if available.

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -508,6 +510,7 @@ async def before_tool_callback(
# Create invocation object
invocation = ExecuteToolInvocation(
tool_name=tool.name,
tool_type="function",
provider="google_adk",
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ def create_agent_invocation(
invocation = InvokeAgentInvocation(
provider=_HERMES_AGENT_SYSTEM,
agent_name="Hermes",
agent_id="hermes-agent",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Warning] agent_id is hardcoded to "hermes-agent" for every instance. Other instrumentations in this PR use agent_name (which at least varies per agent). The GenAI semconv agent.id is intended to uniquely identify an agent instance — a constant string here means all concurrent hermes-agent instances share the same ID, making the attribute non-discriminating. Consider deriving from a session/instance identifier, or at minimum use agent_name for consistency.

conversation_id=getattr(instance, "session_id", None),
request_model=getattr(instance, "model", None),
input_messages=[
Expand Down Expand Up @@ -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

Comment on lines +613 to +630

def create_llm_invocation(instance: Any, api_kwargs: Any) -> LLMInvocation:
if not isinstance(api_kwargs, dict):
api_kwargs = {}
Expand All @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ def wrap_generate_next_message(
invocation = InvokeAgentInvocation(
provider="vitabench",
agent_name=agent_name,
agent_id=agent_name,
request_model=model,
)

Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Comment on lines +153 to +165

def _get_tool_definitions(
tools: Any,
) -> Optional[List[FunctionToolDefinition]]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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=[
Expand Down
11 changes: 11 additions & 0 deletions loongsuite-distro/src/loongsuite/distro/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Warning] The dict.update() call gives the resource detector precedence over any user-provided resource_attributes. If a user explicitly configures host.ip or gen_ai.instrumentation.sdk.name, those values are silently overwritten by the detector. Consider merging the detector's attributes first (or using setdefault-style logic) so caller-provided values take precedence.

resource_attributes = dict(kwargs.pop("resource_attributes", None) or {})
# User values set first, detector only fills gaps:
detected = LoongSuiteResourceDetector().detect().attributes
for k, v in detected.items():
    resource_attributes.setdefault(k, v)

resource_attributes.update(LoongSuiteResourceDetector().detect().attributes)
super()._configure(resource_attributes=resource_attributes, **kwargs)


class LoongSuiteDistro(BaseDistro):
"""
Expand Down
80 changes: 80 additions & 0 deletions loongsuite-distro/src/loongsuite/distro/resource.py
Original file line number Diff line number Diff line change
@@ -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 ``<ip>-<pid>``, 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 ``<ip>-<pid>`` (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,
}
Comment on lines +74 to +79
)
13 changes: 13 additions & 0 deletions loongsuite-distro/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading