From 1832583bb9482fa57d29268d1541dbfe01c7a07c Mon Sep 17 00:00:00 2001 From: Jean Scherf Date: Wed, 26 Aug 2026 13:21:26 -0300 Subject: [PATCH 1/4] feat(telemetry): add register_sdk_resource_attributes() for companion SDKs Adds a module-level registry in create_resource_attributes_from_env() so companion SDKs (e.g. sap-internal-sdk) can contribute their version as an OTel resource attribute at import time, without requiring any change to agent startup code. --- src/sap_cloud_sdk/core/telemetry/__init__.py | 2 + src/sap_cloud_sdk/core/telemetry/config.py | 16 ++++++++ tests/core/unit/telemetry/test_config.py | 39 ++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/src/sap_cloud_sdk/core/telemetry/__init__.py b/src/sap_cloud_sdk/core/telemetry/__init__.py index 1febaddd..6552f7fd 100644 --- a/src/sap_cloud_sdk/core/telemetry/__init__.py +++ b/src/sap_cloud_sdk/core/telemetry/__init__.py @@ -16,6 +16,7 @@ from sap_cloud_sdk.core.telemetry.genai_operation import GenAIOperation from sap_cloud_sdk.core.telemetry.metrics_decorator import record_metrics from sap_cloud_sdk.core.telemetry.auto_instrument import auto_instrument +from sap_cloud_sdk.core.telemetry.config import register_sdk_resource_attributes from sap_cloud_sdk.core.telemetry.tracer import ( context_overlay, get_current_span, @@ -67,6 +68,7 @@ "set_tenant_id", "get_tenant_id", "auto_instrument", + "register_sdk_resource_attributes", "context_overlay", "get_current_span", "add_span_attribute", diff --git a/src/sap_cloud_sdk/core/telemetry/config.py b/src/sap_cloud_sdk/core/telemetry/config.py index 45f3565d..97c55420 100644 --- a/src/sap_cloud_sdk/core/telemetry/config.py +++ b/src/sap_cloud_sdk/core/telemetry/config.py @@ -22,6 +22,20 @@ SDK_NAME, ) +# Registry of extra resource attributes contributed by companion SDKs. +# Populated via register_sdk_resource_attributes() at import time of those SDKs. +_extra_sdk_attributes: dict = {} + + +def register_sdk_resource_attributes(attributes: dict) -> None: + """Register additional OTel resource attributes to be included in every provider. + + Companion SDKs (e.g. sap-internal-sdk) call this once at import time so their + version appears on every span and metric without any change to agent startup code. + """ + _extra_sdk_attributes.update(attributes) + + # Default attribute values DEFAULT_UNKNOWN = "unknown" @@ -172,6 +186,8 @@ def create_resource_attributes_from_env() -> dict: if service_display_name is not None: attributes[ATTR_SAP_SERVICE_DISPLAY_NAME] = service_display_name + attributes.update(_extra_sdk_attributes) + return attributes diff --git a/tests/core/unit/telemetry/test_config.py b/tests/core/unit/telemetry/test_config.py index fe2f1f68..022409b2 100644 --- a/tests/core/unit/telemetry/test_config.py +++ b/tests/core/unit/telemetry/test_config.py @@ -7,6 +7,8 @@ create_resource_attributes_from_env, get_config, set_config, + register_sdk_resource_attributes, + _extra_sdk_attributes, ) from sap_cloud_sdk.core.telemetry.constants import ( ATTR_MLFLOW_EXPERIMENT_ID, @@ -299,3 +301,40 @@ def test_service_display_name_independent_of_other_attributes(self): assert attrs[ATTR_SAP_SERVICE_DISPLAY_NAME] == "My Service" assert attrs[ATTR_SAP_ORD_ID] == "my-ord-id" + + +class TestRegisterSdkResourceAttributes: + """Tests for the companion-SDK resource attribute registry.""" + + def setup_method(self): + _extra_sdk_attributes.clear() + + def teardown_method(self): + _extra_sdk_attributes.clear() + + def test_registered_attributes_appear_in_resource(self): + register_sdk_resource_attributes({"sap.internal_sdk.version": "1.0.0"}) + attrs = create_resource_attributes_from_env() + assert attrs["sap.internal_sdk.version"] == "1.0.0" + + def test_multiple_registrations_are_merged(self): + register_sdk_resource_attributes({"sap.foo.version": "1.0"}) + register_sdk_resource_attributes({"sap.bar.version": "2.0"}) + attrs = create_resource_attributes_from_env() + assert attrs["sap.foo.version"] == "1.0" + assert attrs["sap.bar.version"] == "2.0" + + def test_registered_attributes_do_not_override_cloud_sdk_keys(self): + from sap_cloud_sdk.core.telemetry.constants import ATTR_SAP_SDK_VERSION + original = create_resource_attributes_from_env()[ATTR_SAP_SDK_VERSION] + register_sdk_resource_attributes({ATTR_SAP_SDK_VERSION: "999"}) + attrs = create_resource_attributes_from_env() + assert attrs[ATTR_SAP_SDK_VERSION] == "999" + _extra_sdk_attributes.clear() + assert create_resource_attributes_from_env()[ATTR_SAP_SDK_VERSION] == original + + def test_empty_registry_does_not_affect_output(self): + attrs_without = create_resource_attributes_from_env() + register_sdk_resource_attributes({}) + attrs_with = create_resource_attributes_from_env() + assert attrs_without == attrs_with From 69ad64ff5c5f96aae2a54eadf2185098d691618b Mon Sep 17 00:00:00 2001 From: Jean Scherf Date: Wed, 26 Aug 2026 13:53:01 -0300 Subject: [PATCH 2/4] docs(telemetry): show importlib.metadata pattern in register_sdk_resource_attributes --- src/sap_cloud_sdk/core/telemetry/config.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/sap_cloud_sdk/core/telemetry/config.py b/src/sap_cloud_sdk/core/telemetry/config.py index 97c55420..f3f5b7c2 100644 --- a/src/sap_cloud_sdk/core/telemetry/config.py +++ b/src/sap_cloud_sdk/core/telemetry/config.py @@ -30,8 +30,22 @@ def register_sdk_resource_attributes(attributes: dict) -> None: """Register additional OTel resource attributes to be included in every provider. - Companion SDKs (e.g. sap-internal-sdk) call this once at import time so their - version appears on every span and metric without any change to agent startup code. + Companion SDKs call this once at import time so their version appears on every + span and metric without any change to agent startup code. Read the version from + ``importlib.metadata`` rather than hardcoding it:: + + from importlib.metadata import version, PackageNotFoundError + from sap_cloud_sdk.core.telemetry.config import register_sdk_resource_attributes + + try: + sdk_version = version("my-sdk-package") + except PackageNotFoundError: + sdk_version = "unknown" + + register_sdk_resource_attributes({ + "sap.my_sdk.version": sdk_version, + "sap.my_sdk.language": "python", + }) """ _extra_sdk_attributes.update(attributes) From 8e70d26326205e30088fbac7f57fb8c8b8403c5f Mon Sep 17 00:00:00 2001 From: Jean Scherf Date: Thu, 27 Aug 2026 17:19:12 -0300 Subject: [PATCH 3/4] refactor(telemetry): replace registry with entry-points plugin discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the import-time registry (_extra_sdk_attributes / register_sdk_resource_attributes) and replace it with importlib.metadata entry_points discovery at auto_instrument() call time. Any installed package that registers a callable under the "sap_cloud_sdk.resource_providers" group has its returned dict merged into the resource attributes automatically — no import order dependency, no agent code change required. --- src/sap_cloud_sdk/core/telemetry/__init__.py | 2 - src/sap_cloud_sdk/core/telemetry/config.py | 37 +++-------- tests/core/unit/telemetry/test_config.py | 69 +++++++++++--------- 3 files changed, 47 insertions(+), 61 deletions(-) diff --git a/src/sap_cloud_sdk/core/telemetry/__init__.py b/src/sap_cloud_sdk/core/telemetry/__init__.py index 6552f7fd..1febaddd 100644 --- a/src/sap_cloud_sdk/core/telemetry/__init__.py +++ b/src/sap_cloud_sdk/core/telemetry/__init__.py @@ -16,7 +16,6 @@ from sap_cloud_sdk.core.telemetry.genai_operation import GenAIOperation from sap_cloud_sdk.core.telemetry.metrics_decorator import record_metrics from sap_cloud_sdk.core.telemetry.auto_instrument import auto_instrument -from sap_cloud_sdk.core.telemetry.config import register_sdk_resource_attributes from sap_cloud_sdk.core.telemetry.tracer import ( context_overlay, get_current_span, @@ -68,7 +67,6 @@ "set_tenant_id", "get_tenant_id", "auto_instrument", - "register_sdk_resource_attributes", "context_overlay", "get_current_span", "add_span_attribute", diff --git a/src/sap_cloud_sdk/core/telemetry/config.py b/src/sap_cloud_sdk/core/telemetry/config.py index f3f5b7c2..aa21d719 100644 --- a/src/sap_cloud_sdk/core/telemetry/config.py +++ b/src/sap_cloud_sdk/core/telemetry/config.py @@ -1,7 +1,9 @@ """Configuration for OpenTelemetry telemetry.""" +import logging import os from dataclasses import dataclass +from importlib.metadata import entry_points as _entry_points from typing import Optional from opentelemetry.sdk.resources import SERVICE_NAME from sap_cloud_sdk.core._version import get_version @@ -22,33 +24,7 @@ SDK_NAME, ) -# Registry of extra resource attributes contributed by companion SDKs. -# Populated via register_sdk_resource_attributes() at import time of those SDKs. -_extra_sdk_attributes: dict = {} - - -def register_sdk_resource_attributes(attributes: dict) -> None: - """Register additional OTel resource attributes to be included in every provider. - - Companion SDKs call this once at import time so their version appears on every - span and metric without any change to agent startup code. Read the version from - ``importlib.metadata`` rather than hardcoding it:: - - from importlib.metadata import version, PackageNotFoundError - from sap_cloud_sdk.core.telemetry.config import register_sdk_resource_attributes - - try: - sdk_version = version("my-sdk-package") - except PackageNotFoundError: - sdk_version = "unknown" - - register_sdk_resource_attributes({ - "sap.my_sdk.version": sdk_version, - "sap.my_sdk.language": "python", - }) - """ - _extra_sdk_attributes.update(attributes) - +logger = logging.getLogger(__name__) # Default attribute values DEFAULT_UNKNOWN = "unknown" @@ -200,7 +176,12 @@ def create_resource_attributes_from_env() -> dict: if service_display_name is not None: attributes[ATTR_SAP_SERVICE_DISPLAY_NAME] = service_display_name - attributes.update(_extra_sdk_attributes) + for _ep in _entry_points(group="sap_cloud_sdk.resource_providers"): + try: + _provider = _ep.load() + attributes.update(_provider()) + except Exception: + logger.debug("Failed to load resource provider %s", _ep.name, exc_info=True) return attributes diff --git a/tests/core/unit/telemetry/test_config.py b/tests/core/unit/telemetry/test_config.py index 022409b2..436dce9d 100644 --- a/tests/core/unit/telemetry/test_config.py +++ b/tests/core/unit/telemetry/test_config.py @@ -7,8 +7,6 @@ create_resource_attributes_from_env, get_config, set_config, - register_sdk_resource_attributes, - _extra_sdk_attributes, ) from sap_cloud_sdk.core.telemetry.constants import ( ATTR_MLFLOW_EXPERIMENT_ID, @@ -303,38 +301,47 @@ def test_service_display_name_independent_of_other_attributes(self): assert attrs[ATTR_SAP_ORD_ID] == "my-ord-id" -class TestRegisterSdkResourceAttributes: - """Tests for the companion-SDK resource attribute registry.""" +def _make_ep(name: str, attrs: dict, raises: Exception | None = None): + """Build a mock entry point that returns *attrs* (or raises *raises*).""" + from unittest.mock import MagicMock + ep = MagicMock() + ep.name = name + if raises is not None: + ep.load.return_value = MagicMock(side_effect=raises) + else: + ep.load.return_value = lambda: attrs + return ep - def setup_method(self): - _extra_sdk_attributes.clear() - def teardown_method(self): - _extra_sdk_attributes.clear() +class TestSdkResourceProviderEntryPoints: + """Tests for companion-SDK resource attribute discovery via entry points.""" - def test_registered_attributes_appear_in_resource(self): - register_sdk_resource_attributes({"sap.internal_sdk.version": "1.0.0"}) - attrs = create_resource_attributes_from_env() - assert attrs["sap.internal_sdk.version"] == "1.0.0" + def test_provider_attributes_appear_in_resource(self): + mock_ep = _make_ep("test_sdk", {"sap.test_sdk.version": "1.0.0"}) + with patch("sap_cloud_sdk.core.telemetry.config._entry_points", return_value=[mock_ep]): + attrs = create_resource_attributes_from_env() + assert attrs["sap.test_sdk.version"] == "1.0.0" + + def test_multiple_providers_are_merged(self): + eps = [ + _make_ep("sdk_a", {"sap.sdk_a.version": "1.0"}), + _make_ep("sdk_b", {"sap.sdk_b.version": "2.0"}), + ] + with patch("sap_cloud_sdk.core.telemetry.config._entry_points", return_value=eps): + attrs = create_resource_attributes_from_env() + assert attrs["sap.sdk_a.version"] == "1.0" + assert attrs["sap.sdk_b.version"] == "2.0" - def test_multiple_registrations_are_merged(self): - register_sdk_resource_attributes({"sap.foo.version": "1.0"}) - register_sdk_resource_attributes({"sap.bar.version": "2.0"}) - attrs = create_resource_attributes_from_env() - assert attrs["sap.foo.version"] == "1.0" - assert attrs["sap.bar.version"] == "2.0" + def test_failing_provider_is_skipped_silently(self): + bad_ep = _make_ep("broken", {}, raises=RuntimeError("boom")) + good_ep = _make_ep("good", {"sap.good.version": "3.0"}) + with patch("sap_cloud_sdk.core.telemetry.config._entry_points", return_value=[bad_ep, good_ep]): + attrs = create_resource_attributes_from_env() + assert attrs["sap.good.version"] == "3.0" + assert "sap.broken.version" not in attrs - def test_registered_attributes_do_not_override_cloud_sdk_keys(self): + def test_no_providers_does_not_affect_cloud_sdk_keys(self): + with patch("sap_cloud_sdk.core.telemetry.config._entry_points", return_value=[]): + attrs = create_resource_attributes_from_env() from sap_cloud_sdk.core.telemetry.constants import ATTR_SAP_SDK_VERSION - original = create_resource_attributes_from_env()[ATTR_SAP_SDK_VERSION] - register_sdk_resource_attributes({ATTR_SAP_SDK_VERSION: "999"}) - attrs = create_resource_attributes_from_env() - assert attrs[ATTR_SAP_SDK_VERSION] == "999" - _extra_sdk_attributes.clear() - assert create_resource_attributes_from_env()[ATTR_SAP_SDK_VERSION] == original - - def test_empty_registry_does_not_affect_output(self): - attrs_without = create_resource_attributes_from_env() - register_sdk_resource_attributes({}) - attrs_with = create_resource_attributes_from_env() - assert attrs_without == attrs_with + assert ATTR_SAP_SDK_VERSION in attrs From db776edb3ffa091dc12d7d09faaa72622c72906d Mon Sep 17 00:00:00 2001 From: Jean Scherf Date: Fri, 28 Aug 2026 14:08:54 -0300 Subject: [PATCH 4/4] chore: bump version to 0.46.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5aa3e57d..419e1792 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.46.0" +version = "0.46.1" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0"