Skip to content

Commit 52325af

Browse files
authored
fix(telemetry): merge SDK resource attrs into pre-installed MeterProvider (#320)
1 parent 6e863ae commit 52325af

4 files changed

Lines changed: 121 additions & 14 deletions

File tree

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "sap-cloud-sdk"
3-
version = "0.52.0"
3+
version = "0.52.1"
44
description = "SAP Cloud SDK for Python"
55
readme = "README.md"
66
license = "Apache-2.0"

‎src/sap_cloud_sdk/core/telemetry/_provider.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
)
3737
from opentelemetry.sdk.resources import Resource
3838

39+
# Stable reference for isinstance checks — not overwritten when tests patch MeterProvider
40+
_SDKMeterProvider = MeterProvider
41+
3942
from sap_cloud_sdk.core.telemetry.config import (
4043
get_config,
4144
create_resource_attributes_from_env,
@@ -48,6 +51,22 @@
4851
logger = logging.getLogger(__name__)
4952

5053

54+
def _merge_sdk_resource_into_meter_provider(
55+
provider: MeterProvider, sdk_resource: Resource
56+
) -> None:
57+
"""Merge SDK resource attrs into an already-installed MeterProvider.
58+
59+
No lock needed (unlike the log provider): meters don't cache a resource, so
60+
there's no per-instance collection to iterate. collect() reads
61+
_sdk_config.resource live at export time via the same shared reference we
62+
mutate here, so this single reassignment propagates.
63+
"""
64+
provider._sdk_config.resource = provider._sdk_config.resource.merge(sdk_resource)
65+
logger.info(
66+
"Merged sap-cloud-sdk resource attrs onto wrapper-installed MeterProvider"
67+
)
68+
69+
5170
def _merge_sdk_resource_into_log_provider(
5271
provider: LoggerProvider, sdk_resource: Resource
5372
) -> None:
@@ -214,6 +233,21 @@ def _setup_meter_provider() -> Optional[MeterProvider]:
214233

215234
try:
216235
resource = Resource.create(create_resource_attributes_from_env())
236+
existing = cast(MeterProvider, metrics.get_meter_provider())
237+
238+
if isinstance(existing, _SDKMeterProvider):
239+
logger.warning(
240+
"Global MeterProvider was already set by another library. "
241+
"Merging sap.cloud_sdk.* resource attributes into the existing provider."
242+
)
243+
_merge_sdk_resource_into_meter_provider(existing, resource)
244+
logger.info(
245+
f"OpenTelemetry meter provider merged. "
246+
f"Service: {config.service_name}, "
247+
f"Endpoint: {config.otlp_endpoint}"
248+
)
249+
return existing
250+
217251
exporter = _create_metric_exporter()
218252
reader = PeriodicExportingMetricReader(exporter=exporter)
219253
provider = MeterProvider(resource=resource, metric_readers=[reader])

‎tests/core/unit/telemetry/test_provider.py‎

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
shutdown,
2222
_setup_meter_provider,
2323
_create_metric_exporter,
24+
_merge_sdk_resource_into_meter_provider,
2425
setup_log_provider,
2526
_create_log_exporter,
2627
_merge_sdk_resource_into_log_provider,
@@ -173,32 +174,66 @@ def test_delegates_to_create_metric_exporter(self):
173174
mock_exporter = MagicMock()
174175
with patch("sap_cloud_sdk.core.telemetry._provider.get_config", return_value=_ENABLED_CONFIG):
175176
with patch("sap_cloud_sdk.core.telemetry._provider.Resource"):
176-
with patch("sap_cloud_sdk.core.telemetry._provider._create_metric_exporter", return_value=mock_exporter) as mock_create:
177-
with patch("sap_cloud_sdk.core.telemetry._provider.PeriodicExportingMetricReader") as mock_reader:
178-
with patch("sap_cloud_sdk.core.telemetry._provider.MeterProvider"):
179-
with patch("opentelemetry.metrics.set_meter_provider"):
177+
with patch("sap_cloud_sdk.core.telemetry._provider.metrics") as mock_metrics:
178+
mock_metrics.get_meter_provider.return_value = MagicMock()
179+
with patch("sap_cloud_sdk.core.telemetry._provider._create_metric_exporter", return_value=mock_exporter) as mock_create:
180+
with patch("sap_cloud_sdk.core.telemetry._provider.PeriodicExportingMetricReader") as mock_reader:
181+
with patch("sap_cloud_sdk.core.telemetry._provider.MeterProvider"):
180182
_setup_meter_provider()
181183

182-
mock_create.assert_called_once_with()
183-
mock_reader.assert_called_once_with(exporter=mock_exporter)
184+
mock_create.assert_called_once_with()
185+
mock_reader.assert_called_once_with(exporter=mock_exporter)
184186

185187
def test_unsupported_protocol_returns_none(self):
186188
with patch("sap_cloud_sdk.core.telemetry._provider.get_config", return_value=_ENABLED_CONFIG):
187189
with patch("sap_cloud_sdk.core.telemetry._provider.Resource"):
188-
with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_PROTOCOL": "http/json"}):
189-
assert _setup_meter_provider() is None
190+
with patch("sap_cloud_sdk.core.telemetry._provider.metrics") as mock_metrics:
191+
mock_metrics.get_meter_provider.return_value = MagicMock()
192+
with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_PROTOCOL": "http/json"}):
193+
assert _setup_meter_provider() is None
190194

191195
def test_returns_configured_provider(self):
192196
mock_provider = MagicMock()
193197
with patch("sap_cloud_sdk.core.telemetry._provider.get_config", return_value=_ENABLED_CONFIG):
194198
with patch("sap_cloud_sdk.core.telemetry._provider.Resource"):
195-
with patch("sap_cloud_sdk.core.telemetry._provider._create_metric_exporter"):
196-
with patch("sap_cloud_sdk.core.telemetry._provider.PeriodicExportingMetricReader"):
197-
with patch("sap_cloud_sdk.core.telemetry._provider.MeterProvider", return_value=mock_provider):
198-
with patch("opentelemetry.metrics.set_meter_provider"):
199+
with patch("sap_cloud_sdk.core.telemetry._provider.metrics") as mock_metrics:
200+
mock_metrics.get_meter_provider.return_value = MagicMock()
201+
with patch("sap_cloud_sdk.core.telemetry._provider._create_metric_exporter"):
202+
with patch("sap_cloud_sdk.core.telemetry._provider.PeriodicExportingMetricReader"):
203+
with patch("sap_cloud_sdk.core.telemetry._provider.MeterProvider", return_value=mock_provider):
199204
assert _setup_meter_provider() is mock_provider
200205

201206

207+
def test_reuses_existing_sdk_meter_provider(self):
208+
"""When a MeterProvider is already set, merge into it instead of creating a new one."""
209+
from opentelemetry.sdk.metrics import MeterProvider as _MP
210+
existing = MagicMock(spec=_MP)
211+
with patch("sap_cloud_sdk.core.telemetry._provider.get_config", return_value=_ENABLED_CONFIG):
212+
with patch("sap_cloud_sdk.core.telemetry._provider.Resource"):
213+
with patch("sap_cloud_sdk.core.telemetry._provider.metrics") as mock_metrics:
214+
mock_metrics.get_meter_provider.return_value = existing
215+
with patch("sap_cloud_sdk.core.telemetry._provider._merge_sdk_resource_into_meter_provider") as mock_merge:
216+
with patch("sap_cloud_sdk.core.telemetry._provider._SDKMeterProvider", _MP):
217+
result = _setup_meter_provider()
218+
assert result is existing
219+
mock_merge.assert_called_once()
220+
mock_metrics.set_meter_provider.assert_not_called()
221+
222+
def test_existing_provider_no_new_reader(self):
223+
"""Reuse path must not create a new PeriodicExportingMetricReader."""
224+
from opentelemetry.sdk.metrics import MeterProvider as _MP
225+
existing = MagicMock(spec=_MP)
226+
with patch("sap_cloud_sdk.core.telemetry._provider.get_config", return_value=_ENABLED_CONFIG):
227+
with patch("sap_cloud_sdk.core.telemetry._provider.Resource"):
228+
with patch("sap_cloud_sdk.core.telemetry._provider.metrics") as mock_metrics:
229+
mock_metrics.get_meter_provider.return_value = existing
230+
with patch("sap_cloud_sdk.core.telemetry._provider._merge_sdk_resource_into_meter_provider"):
231+
with patch("sap_cloud_sdk.core.telemetry._provider._SDKMeterProvider", _MP):
232+
with patch("sap_cloud_sdk.core.telemetry._provider.PeriodicExportingMetricReader") as mock_reader:
233+
_setup_meter_provider()
234+
mock_reader.assert_not_called()
235+
236+
202237
_LOGGING_HANDLER = "sap_cloud_sdk.core.telemetry._provider.LoggingHandler"
203238
_GRPC_LOG_EXPORTER = "sap_cloud_sdk.core.telemetry._provider.GRPCLogExporter"
204239
_HTTP_LOG_EXPORTER = "sap_cloud_sdk.core.telemetry._provider.HTTPLogExporter"
@@ -330,6 +365,44 @@ def test_platform_path_adds_handler_when_none_present(self):
330365
mock_handler_cls.assert_called_once_with(logger_provider=external)
331366

332367

368+
class TestMergeSdkResourceIntoMeterProvider:
369+
def test_updates_sdk_config_resource(self):
370+
from opentelemetry.sdk.metrics import MeterProvider as _MP
371+
from opentelemetry.sdk.resources import Resource as _R
372+
373+
sdk_resource = _R({"sap.cloud_sdk.language": "python"})
374+
existing_resource = _R({"service.name": "svc"})
375+
provider = _MP(resource=existing_resource)
376+
377+
_merge_sdk_resource_into_meter_provider(provider, sdk_resource)
378+
379+
assert provider._sdk_config.resource.attributes["sap.cloud_sdk.language"] == "python"
380+
assert provider._sdk_config.resource.attributes["service.name"] == "svc"
381+
382+
def test_measurement_consumer_sees_update(self):
383+
"""_measurement_consumer shares the same SdkConfiguration object."""
384+
from opentelemetry.sdk.metrics import MeterProvider as _MP
385+
from opentelemetry.sdk.resources import Resource as _R
386+
387+
sdk_resource = _R({"sap.cloud_sdk.language": "python"})
388+
provider = _MP(resource=_R({"service.name": "svc"}))
389+
390+
_merge_sdk_resource_into_meter_provider(provider, sdk_resource)
391+
392+
assert provider._measurement_consumer._sdk_config.resource is provider._sdk_config.resource
393+
394+
def test_sdk_attrs_win_on_collision(self):
395+
from opentelemetry.sdk.metrics import MeterProvider as _MP
396+
from opentelemetry.sdk.resources import Resource as _R
397+
398+
sdk_resource = _R({"service.name": "sdk-name"})
399+
provider = _MP(resource=_R({"service.name": "platform-name"}))
400+
401+
_merge_sdk_resource_into_meter_provider(provider, sdk_resource)
402+
403+
assert provider._sdk_config.resource.attributes["service.name"] == "sdk-name"
404+
405+
333406
class TestMergeSdkResourceIntoLogProvider:
334407
def test_updates_provider_resource(self):
335408
from opentelemetry.sdk._logs import LoggerProvider as _LP

‎uv.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)