Skip to content

Commit eeac763

Browse files
123liuzimingclaude
andcommitted
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 <ip>-<pid> (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 <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6c50471 commit eeac763

5 files changed

Lines changed: 266 additions & 0 deletions

File tree

loongsuite-distro/src/loongsuite/distro/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,23 @@
2424
from opentelemetry.sdk._configuration import _OTelSDKConfigurator
2525
from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_PROTOCOL
2626

27+
from loongsuite.distro.resource import LoongSuiteResourceDetector
28+
2729

2830
class LoongSuiteConfigurator(_OTelSDKConfigurator):
2931
"""
3032
LoongSuite configurator, inherits from OpenTelemetry SDK configurator.
33+
34+
Augments the resource with LoongSuite specific attributes (``host.ip`` and
35+
``gen_ai.instrumentation.sdk.name``) before delegating to the OpenTelemetry
36+
SDK configurator.
3137
"""
3238

39+
def _configure(self, **kwargs: Any) -> None:
40+
resource_attributes = dict(kwargs.pop("resource_attributes", None) or {})
41+
resource_attributes.update(LoongSuiteResourceDetector().detect().attributes)
42+
super()._configure(resource_attributes=resource_attributes, **kwargs)
43+
3344

3445
class LoongSuiteDistro(BaseDistro):
3546
"""
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
import socket
17+
from logging import getLogger
18+
19+
from opentelemetry.sdk.resources import Resource, ResourceDetector
20+
21+
logger = getLogger(__name__)
22+
23+
# Resource attribute keys contributed by LoongSuite.
24+
HOST_IP = "host.ip"
25+
GEN_AI_INSTRUMENTATION_SDK_NAME = "gen_ai.instrumentation.sdk.name"
26+
27+
# Fixed value identifying the GenAI instrumentation SDK shipped with LoongSuite.
28+
_GEN_AI_INSTRUMENTATION_SDK_NAME_VALUE = "loongsuite-genai-utils"
29+
30+
_FALLBACK_HOST_IP = "127.0.0.1"
31+
32+
33+
def _get_host_ip() -> str:
34+
"""Best-effort detection of the local host IP address.
35+
36+
Opens a UDP socket towards a public address to discover which local
37+
interface would be used for outbound traffic. No packet is actually sent.
38+
Falls back to ``127.0.0.1`` when detection fails.
39+
"""
40+
sock = None
41+
try:
42+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
43+
sock.connect(("8.8.8.8", 80))
44+
return sock.getsockname()[0]
45+
except OSError as exception:
46+
logger.debug(
47+
"Failed to detect host ip, falling back to %s. Exception: %s",
48+
_FALLBACK_HOST_IP,
49+
exception,
50+
)
51+
return _FALLBACK_HOST_IP
52+
finally:
53+
if sock is not None:
54+
sock.close()
55+
56+
57+
def _get_host_ip_with_pid() -> str:
58+
"""Returns the host IP combined with the current process id.
59+
60+
The format is ``<ip>-<pid>``, for example ``127.0.0.1-1``.
61+
"""
62+
return f"{_get_host_ip()}-{os.getpid()}"
63+
64+
65+
class LoongSuiteResourceDetector(ResourceDetector):
66+
"""Detects LoongSuite specific resource attributes.
67+
68+
Contributes the following attributes to the resource:
69+
70+
* ``host.ip`` formatted as ``<ip>-<pid>`` (e.g. ``127.0.0.1-1``).
71+
* ``gen_ai.instrumentation.sdk.name`` set to ``loongsuite-genai-utils``.
72+
"""
73+
74+
def detect(self) -> Resource:
75+
return Resource(
76+
{
77+
HOST_IP: _get_host_ip_with_pid(),
78+
GEN_AI_INSTRUMENTATION_SDK_NAME: _GEN_AI_INSTRUMENTATION_SDK_NAME_VALUE,
79+
}
80+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import unittest
16+
from unittest import mock
17+
18+
from loongsuite.distro import LoongSuiteConfigurator
19+
from loongsuite.distro.resource import (
20+
GEN_AI_INSTRUMENTATION_SDK_NAME,
21+
HOST_IP,
22+
)
23+
24+
25+
class TestLoongSuiteConfigurator(unittest.TestCase):
26+
@mock.patch("opentelemetry.sdk._configuration._initialize_components")
27+
def test_configure_injects_loongsuite_attributes(self, mock_init):
28+
LoongSuiteConfigurator().configure()
29+
30+
mock_init.assert_called_once()
31+
resource_attributes = mock_init.call_args.kwargs[
32+
"resource_attributes"
33+
]
34+
self.assertIn(HOST_IP, resource_attributes)
35+
self.assertEqual(
36+
resource_attributes[GEN_AI_INSTRUMENTATION_SDK_NAME],
37+
"loongsuite-genai-utils",
38+
)
39+
40+
@mock.patch("opentelemetry.sdk._configuration._initialize_components")
41+
def test_configure_preserves_existing_attributes(self, mock_init):
42+
LoongSuiteConfigurator().configure(
43+
resource_attributes={"service.name": "my-service"}
44+
)
45+
46+
resource_attributes = mock_init.call_args.kwargs[
47+
"resource_attributes"
48+
]
49+
self.assertEqual(
50+
resource_attributes["service.name"], "my-service"
51+
)
52+
self.assertIn(HOST_IP, resource_attributes)
53+
self.assertIn(
54+
GEN_AI_INSTRUMENTATION_SDK_NAME, resource_attributes
55+
)
56+
57+
@mock.patch("opentelemetry.sdk._configuration._initialize_components")
58+
def test_configure_forwards_other_kwargs(self, mock_init):
59+
LoongSuiteConfigurator().configure(
60+
auto_instrumentation_version="1.2.3"
61+
)
62+
63+
self.assertEqual(
64+
mock_init.call_args.kwargs["auto_instrumentation_version"],
65+
"1.2.3",
66+
)
67+
68+
69+
if __name__ == "__main__":
70+
unittest.main()
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import unittest
16+
from unittest import mock
17+
18+
from opentelemetry.sdk.resources import Resource, ResourceDetector
19+
20+
from loongsuite.distro.resource import (
21+
GEN_AI_INSTRUMENTATION_SDK_NAME,
22+
HOST_IP,
23+
LoongSuiteResourceDetector,
24+
_get_host_ip,
25+
_get_host_ip_with_pid,
26+
)
27+
28+
29+
class TestLoongSuiteResourceDetector(unittest.TestCase):
30+
def test_is_resource_detector(self):
31+
self.assertIsInstance(
32+
LoongSuiteResourceDetector(), ResourceDetector
33+
)
34+
35+
def test_detect_returns_resource(self):
36+
resource = LoongSuiteResourceDetector().detect()
37+
self.assertIsInstance(resource, Resource)
38+
39+
def test_detect_contains_expected_keys(self):
40+
attributes = LoongSuiteResourceDetector().detect().attributes
41+
self.assertIn(HOST_IP, attributes)
42+
self.assertIn(GEN_AI_INSTRUMENTATION_SDK_NAME, attributes)
43+
44+
def test_gen_ai_instrumentation_sdk_name_value(self):
45+
attributes = LoongSuiteResourceDetector().detect().attributes
46+
self.assertEqual(
47+
attributes[GEN_AI_INSTRUMENTATION_SDK_NAME],
48+
"loongsuite-genai-utils",
49+
)
50+
51+
@mock.patch("loongsuite.distro.resource.os.getpid", return_value=1)
52+
@mock.patch(
53+
"loongsuite.distro.resource._get_host_ip", return_value="127.0.0.1"
54+
)
55+
def test_host_ip_format_is_ip_dash_pid(self, _mock_ip, _mock_pid):
56+
attributes = LoongSuiteResourceDetector().detect().attributes
57+
self.assertEqual(attributes[HOST_IP], "127.0.0.1-1")
58+
59+
@mock.patch("loongsuite.distro.resource.os.getpid", return_value=42)
60+
@mock.patch(
61+
"loongsuite.distro.resource._get_host_ip", return_value="10.0.0.5"
62+
)
63+
def test_get_host_ip_with_pid(self, _mock_ip, _mock_pid):
64+
self.assertEqual(_get_host_ip_with_pid(), "10.0.0.5-42")
65+
66+
67+
class TestGetHostIp(unittest.TestCase):
68+
def test_returns_detected_ip(self):
69+
mock_sock = mock.MagicMock()
70+
mock_sock.getsockname.return_value = ("192.168.1.100", 12345)
71+
with mock.patch(
72+
"loongsuite.distro.resource.socket.socket",
73+
return_value=mock_sock,
74+
):
75+
self.assertEqual(_get_host_ip(), "192.168.1.100")
76+
mock_sock.connect.assert_called_once()
77+
mock_sock.close.assert_called_once()
78+
79+
def test_falls_back_to_loopback_on_error(self):
80+
mock_sock = mock.MagicMock()
81+
mock_sock.connect.side_effect = OSError("no network")
82+
with mock.patch(
83+
"loongsuite.distro.resource.socket.socket",
84+
return_value=mock_sock,
85+
):
86+
self.assertEqual(_get_host_ip(), "127.0.0.1")
87+
# The socket must still be closed even when detection fails.
88+
mock_sock.close.assert_called_once()
89+
90+
91+
if __name__ == "__main__":
92+
unittest.main()

0 commit comments

Comments
 (0)