Skip to content

Commit 849cfac

Browse files
herin049xrmx
andauthored
feat: make resource detector ordering deterministic (#5120)
* feat: make resource detector ordering deterministic * fix lint error * update CHANGELOG.md * add more descriptive variable name * make entry point iteration deterministic by sorting by name --------- Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
1 parent 8c82502 commit 849cfac

4 files changed

Lines changed: 86 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2828
([#5076](https://github.com/open-telemetry/opentelemetry-python/pull/5076))
2929
- `opentelemetry-semantic-conventions`: use `X | Y` union annotation
3030
([#5096](https://github.com/open-telemetry/opentelemetry-python/pull/5096))
31+
- `opentelemetry-sdk`: make resource detector ordering deterministic
32+
([#5120](https://github.com/open-telemetry/opentelemetry-python/pull/5120))
3133
- Add WeaverLiveCheck test util
3234
([#5088](https://github.com/open-telemetry/opentelemetry-python/pull/5088))
3335

opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,13 @@ def channel_credential_provider() -> grpc.ChannelCredentials:
777777
entry points for the ```opentelemetry_resource_detector``` entry point. This is an
778778
experimental feature and the name of this variable and its behavior can change
779779
in a non-backwards compatible way.
780+
781+
Detectors are run in the order they are listed and their attributes are merged
782+
in that order, with later detectors taking precedence over earlier ones on
783+
conflicting keys. The ``otel`` detector (which reads
784+
:envvar:`OTEL_RESOURCE_ATTRIBUTES` and :envvar:`OTEL_SERVICE_NAME`) is always
785+
appended last unless explicitly placed elsewhere in the list, ensuring
786+
environment variable attributes take highest priority among detectors.
780787
"""
781788

782789
OTEL_EXPORTER_PROMETHEUS_HOST = "OTEL_EXPORTER_PROMETHEUS_HOST"

opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
from json import dumps
7171
from os import environ
7272
from types import ModuleType
73-
from typing import List, Optional, Set, cast
73+
from typing import List, Optional, cast
7474
from urllib import parse
7575

7676
from opentelemetry.attributes import BoundedAttributes
@@ -195,22 +195,27 @@ def create(
195195
if not attributes:
196196
attributes = {}
197197

198-
otel_experimental_resource_detectors: Set[str] = {"otel"}.union(
199-
{
200-
otel_experimental_resource_detector.strip()
201-
for otel_experimental_resource_detector in environ.get(
202-
OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, ""
203-
).split(",")
204-
if otel_experimental_resource_detector
205-
}
206-
)
198+
otel_experimental_resource_detectors: list[str] = [
199+
detector.strip()
200+
for detector in environ.get(
201+
OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, ""
202+
).split(",")
203+
if detector.strip()
204+
]
207205

208206
resource_detectors: List[ResourceDetector] = []
209207

210208
if "*" in otel_experimental_resource_detectors:
211-
otel_experimental_resource_detectors = entry_points(
212-
group="opentelemetry_resource_detector"
213-
).names
209+
otel_experimental_resource_detectors = [
210+
name
211+
for name in sorted(
212+
entry_points(group="opentelemetry_resource_detector").names
213+
)
214+
if name != "otel"
215+
]
216+
otel_experimental_resource_detectors.append("otel")
217+
elif "otel" not in otel_experimental_resource_detectors:
218+
otel_experimental_resource_detectors.append("otel")
214219

215220
for resource_detector in otel_experimental_resource_detectors:
216221
try:

opentelemetry-sdk/tests/resources/test_resources.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@
5757
_HostResourceDetector,
5858
get_aggregated_resources,
5959
)
60+
from opentelemetry.util._importlib_metadata import (
61+
entry_points as real_entry_points,
62+
)
6063

6164
try:
6265
import psutil
@@ -475,6 +478,16 @@ def test_service_name_env(self):
475478

476479

477480
# pylint: disable=too-many-public-methods
481+
def _make_detector_ep(resource):
482+
return Mock(
483+
**{
484+
"load.return_value": Mock(
485+
return_value=Mock(**{"detect.return_value": resource})
486+
)
487+
}
488+
)
489+
490+
478491
class TestOTELResourceDetector(unittest.TestCase):
479492
def setUp(self) -> None:
480493
environ[OTEL_RESOURCE_ATTRIBUTES] = ""
@@ -774,6 +787,52 @@ def test_resource_detector_entry_points_otel(self):
774787
self.assertIn(PROCESS_RUNTIME_VERSION, resource.attributes.keys())
775788
self.assertEqual(resource.schema_url, "")
776789

790+
@patch.dict(
791+
environ,
792+
{OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "mock_a,mock_b"},
793+
clear=True,
794+
)
795+
def test_resource_detector_ordering_last_wins(self):
796+
"""Last detector in OTEL_EXPERIMENTAL_RESOURCE_DETECTORS wins on conflict."""
797+
ep_a = _make_detector_ep(Resource({"conflict_key": "from_a"}))
798+
ep_b = _make_detector_ep(Resource({"conflict_key": "from_b"}))
799+
800+
def side_effect(*args, **kwargs):
801+
return {"mock_a": [ep_a], "mock_b": [ep_b]}.get(
802+
kwargs.get("name", ""), []
803+
)
804+
805+
with patch(
806+
"opentelemetry.sdk.resources.entry_points", side_effect=side_effect
807+
):
808+
resource = Resource({}).create()
809+
810+
self.assertEqual(resource.attributes["conflict_key"], "from_b")
811+
812+
@patch.dict(
813+
environ,
814+
{
815+
OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "mock",
816+
OTEL_RESOURCE_ATTRIBUTES: "conflict_key=otel_value",
817+
},
818+
clear=True,
819+
)
820+
def test_otel_detector_appended_last(self):
821+
"""'otel' detector is always appended last, so its attributes win over earlier detectors."""
822+
ep_mock = _make_detector_ep(Resource({"conflict_key": "mock_value"}))
823+
824+
def side_effect(*args, **kwargs):
825+
if kwargs.get("name") == "mock":
826+
return [ep_mock]
827+
return real_entry_points(*args, **kwargs)
828+
829+
with patch(
830+
"opentelemetry.sdk.resources.entry_points", side_effect=side_effect
831+
):
832+
resource = Resource({}).create()
833+
834+
self.assertEqual(resource.attributes["conflict_key"], "otel_value")
835+
777836
@patch("platform.system", lambda: "Linux")
778837
@patch("platform.release", lambda: "666.5.0-35-generic")
779838
def test_os_detector_linux(self):

0 commit comments

Comments
 (0)