From 4ea6b07c56b462c767c520a565d130cdd6e6b57a Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Wed, 13 May 2026 11:39:08 -0300 Subject: [PATCH 1/2] Autoisntrumentation backpatch --- .../core/telemetry/auto_instrument.py | 19 +++++++- .../unit/telemetry/test_auto_instrument.py | 43 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/sap_cloud_sdk/core/telemetry/auto_instrument.py b/src/sap_cloud_sdk/core/telemetry/auto_instrument.py index fef7842e..330da32e 100644 --- a/src/sap_cloud_sdk/core/telemetry/auto_instrument.py +++ b/src/sap_cloud_sdk/core/telemetry/auto_instrument.py @@ -159,6 +159,13 @@ def _merge_resource_attrs_into_active_provider_if_wrapper_installed( Mutates ``provider._resource`` because OTel SDK exposes no public API to swap a TracerProvider's Resource post-construction. + + Also back-patches all Tracer instances already cached inside the provider. + The OTel SDK caches Tracer objects by instrumentation scope; each cached + Tracer holds a ``resource`` snapshot taken at ``get_tracer()`` time. + Auto-instrumented libraries (requests, httpx, starlette, langchain) call + ``get_tracer()` BEFORE the app calls``auto_instrument()``, so their + cached Tracers hold the pre-merge resource. """ provider = trace.get_tracer_provider() if not isinstance(provider, TracerProvider): @@ -169,8 +176,16 @@ def _merge_resource_attrs_into_active_provider_if_wrapper_installed( if "telemetry.auto.version" not in existing_attrs: return - provider._resource = provider.resource.merge(Resource.create(sap_attrs)) + merged = provider.resource.merge(Resource.create(sap_attrs)) + provider._resource = merged + + tracers = getattr(provider, "_tracers", {}) + for tracer in tracers.values(): + if hasattr(tracer, "resource"): + tracer.resource = merged + logger.info( "Merged sap-cloud-sdk resource attrs onto wrapper-installed " - "TracerProvider (marker: telemetry.auto.version)" + "TracerProvider and %d cached Tracer(s) (marker: telemetry.auto.version)", + len(tracers), ) diff --git a/tests/core/unit/telemetry/test_auto_instrument.py b/tests/core/unit/telemetry/test_auto_instrument.py index 98ed6328..9dfe25b6 100644 --- a/tests/core/unit/telemetry/test_auto_instrument.py +++ b/tests/core/unit/telemetry/test_auto_instrument.py @@ -330,6 +330,49 @@ def test_auto_instrument_merge_overrides_colliding_service_name(self, mock_trace assert wrapper_provider.resource.attributes['service.name'] == 'cloud-sdk-app' + def test_auto_instrument_back_patches_cached_tracers(self, mock_traceloop_components): + """Tracer objects cached in provider._tracers before auto_instrument() is + called must have their _resource updated to the merged resource. + + The OTel operator's sitecustomize instruments libraries (requests, httpx, + langchain, starlette) by calling get_tracer() before the app calls + auto_instrument(). Each cached Tracer holds a _resource snapshot from that + earlier call. Without back-patching those tracers, their spans would export + without the SAP-specific resource attributes even after the provider's + _resource is updated.""" + mock_traceloop_components['get_app_name'].return_value = 'cloud-sdk-app' + sap_attrs = { + 'service.name': 'cloud-sdk-app', + 'sap.cloud_sdk.name': 'SAP Cloud SDK for Python', + 'sap.cloud_sdk.language': 'python', + } + mock_traceloop_components['create_resource'].return_value = sap_attrs + + wrapper_provider = SDKTracerProvider( + resource=Resource.create({ + 'telemetry.auto.version': '0.62b1', + 'service.name': 'operator-supplied-name', + }) + ) + mock_traceloop_components['get_tracer_provider'].return_value = wrapper_provider + + # Simulate libraries calling get_tracer() BEFORE auto_instrument — these + # tracers will hold the pre-merge resource snapshot in their _resource. + pre_merge_tracer_a = wrapper_provider.get_tracer("opentelemetry.instrumentation.requests") + pre_merge_tracer_b = wrapper_provider.get_tracer("opentelemetry.instrumentation.langchain") + pre_merge_resource = pre_merge_tracer_a.resource + + with patch.dict('os.environ', {'OTEL_EXPORTER_OTLP_ENDPOINT': 'http://localhost:4317'}, clear=True): + auto_instrument() + + merged_resource = wrapper_provider.resource + # Provider resource was updated. + assert merged_resource.attributes['sap.cloud_sdk.name'] == 'SAP Cloud SDK for Python' + # Cached tracers now point at the merged resource, not the pre-merge one. + assert pre_merge_tracer_a.resource is merged_resource + assert pre_merge_tracer_b.resource is merged_resource + assert pre_merge_tracer_a.resource is not pre_merge_resource + class TestAutoInstrumentMiddlewares: """Tests for the middlewares parameter of auto_instrument.""" From c6774f372a6d6130d194a2cacdffb1c6f18ef92d Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Wed, 13 May 2026 14:28:58 -0300 Subject: [PATCH 2/2] Autoisntrumentation backpatch --- .../core/telemetry/auto_instrument.py | 16 ++++------------ .../core/unit/telemetry/test_auto_instrument.py | 13 +------------ 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/src/sap_cloud_sdk/core/telemetry/auto_instrument.py b/src/sap_cloud_sdk/core/telemetry/auto_instrument.py index 330da32e..1b4c0ebe 100644 --- a/src/sap_cloud_sdk/core/telemetry/auto_instrument.py +++ b/src/sap_cloud_sdk/core/telemetry/auto_instrument.py @@ -159,13 +159,6 @@ def _merge_resource_attrs_into_active_provider_if_wrapper_installed( Mutates ``provider._resource`` because OTel SDK exposes no public API to swap a TracerProvider's Resource post-construction. - - Also back-patches all Tracer instances already cached inside the provider. - The OTel SDK caches Tracer objects by instrumentation scope; each cached - Tracer holds a ``resource`` snapshot taken at ``get_tracer()`` time. - Auto-instrumented libraries (requests, httpx, starlette, langchain) call - ``get_tracer()` BEFORE the app calls``auto_instrument()``, so their - cached Tracers hold the pre-merge resource. """ provider = trace.get_tracer_provider() if not isinstance(provider, TracerProvider): @@ -177,15 +170,14 @@ def _merge_resource_attrs_into_active_provider_if_wrapper_installed( return merged = provider.resource.merge(Resource.create(sap_attrs)) - provider._resource = merged - tracers = getattr(provider, "_tracers", {}) - for tracer in tracers.values(): - if hasattr(tracer, "resource"): + with provider._tracers_lock: + provider._resource = merged + for tracer in provider._tracers.values(): tracer.resource = merged logger.info( "Merged sap-cloud-sdk resource attrs onto wrapper-installed " "TracerProvider and %d cached Tracer(s) (marker: telemetry.auto.version)", - len(tracers), + len(provider._tracers), ) diff --git a/tests/core/unit/telemetry/test_auto_instrument.py b/tests/core/unit/telemetry/test_auto_instrument.py index 9dfe25b6..220feded 100644 --- a/tests/core/unit/telemetry/test_auto_instrument.py +++ b/tests/core/unit/telemetry/test_auto_instrument.py @@ -332,14 +332,7 @@ def test_auto_instrument_merge_overrides_colliding_service_name(self, mock_trace def test_auto_instrument_back_patches_cached_tracers(self, mock_traceloop_components): """Tracer objects cached in provider._tracers before auto_instrument() is - called must have their _resource updated to the merged resource. - - The OTel operator's sitecustomize instruments libraries (requests, httpx, - langchain, starlette) by calling get_tracer() before the app calls - auto_instrument(). Each cached Tracer holds a _resource snapshot from that - earlier call. Without back-patching those tracers, their spans would export - without the SAP-specific resource attributes even after the provider's - _resource is updated.""" + called must have their resource updated to the merged resource.""" mock_traceloop_components['get_app_name'].return_value = 'cloud-sdk-app' sap_attrs = { 'service.name': 'cloud-sdk-app', @@ -356,8 +349,6 @@ def test_auto_instrument_back_patches_cached_tracers(self, mock_traceloop_compon ) mock_traceloop_components['get_tracer_provider'].return_value = wrapper_provider - # Simulate libraries calling get_tracer() BEFORE auto_instrument — these - # tracers will hold the pre-merge resource snapshot in their _resource. pre_merge_tracer_a = wrapper_provider.get_tracer("opentelemetry.instrumentation.requests") pre_merge_tracer_b = wrapper_provider.get_tracer("opentelemetry.instrumentation.langchain") pre_merge_resource = pre_merge_tracer_a.resource @@ -366,9 +357,7 @@ def test_auto_instrument_back_patches_cached_tracers(self, mock_traceloop_compon auto_instrument() merged_resource = wrapper_provider.resource - # Provider resource was updated. assert merged_resource.attributes['sap.cloud_sdk.name'] == 'SAP Cloud SDK for Python' - # Cached tracers now point at the merged resource, not the pre-merge one. assert pre_merge_tracer_a.resource is merged_resource assert pre_merge_tracer_b.resource is merged_resource assert pre_merge_tracer_a.resource is not pre_merge_resource