Skip to content

Commit 8bee669

Browse files
Merge branch 'main' into read-ordid-envvar
2 parents 628abe1 + 8791271 commit 8bee669

4 files changed

Lines changed: 124 additions & 7 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.11.6"
3+
version = "0.11.7"
44
description = "SAP Cloud SDK for Python"
55
readme = "README.md"
66
license = "Apache-2.0"

src/sap_cloud_sdk/core/telemetry/auto_instrument.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
import logging
22
import os
3+
from collections.abc import Mapping
34

5+
from opentelemetry import trace
46
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
57
OTLPSpanExporter as GRPCSpanExporter,
68
)
79
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
810
OTLPSpanExporter as HTTPSpanExporter,
911
)
10-
from opentelemetry import trace
11-
from opentelemetry.sdk.trace import TracerProvider
1212
from opentelemetry.processor.baggage import ALLOW_ALL_BAGGAGE_KEYS, BaggageSpanProcessor
13+
from opentelemetry.sdk.resources import Resource
14+
from opentelemetry.sdk.trace import TracerProvider
1315
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SpanExporter
1416
from traceloop.sdk import Traceloop
1517

1618
from sap_cloud_sdk.core.telemetry import Module, Operation
1719
from sap_cloud_sdk.core.telemetry.config import (
18-
create_resource_attributes_from_env,
19-
_get_app_name,
2020
ENV_OTLP_ENDPOINT,
21-
ENV_TRACES_EXPORTER,
2221
ENV_OTLP_PROTOCOL,
22+
ENV_TRACES_EXPORTER,
23+
_get_app_name,
24+
create_resource_attributes_from_env,
2325
)
2426
from sap_cloud_sdk.core.telemetry.genai_attribute_transformer import (
2527
GenAIAttributeTransformer,
@@ -62,6 +64,8 @@ def auto_instrument(disable_batch: bool = False):
6264
disable_batch=disable_batch,
6365
)
6466

67+
_merge_resource_attrs_into_active_provider_if_wrapper_installed(resource)
68+
6569
_set_baggage_processor()
6670

6771
logger.info("Cloud auto instrumentation initialized successfully")
@@ -96,3 +100,33 @@ def _set_baggage_processor():
96100

97101
provider.add_span_processor(BaggageSpanProcessor(ALLOW_ALL_BAGGAGE_KEYS))
98102
logger.info("Registered BaggageSpanProcessor for extension attribute propagation")
103+
104+
105+
def _merge_resource_attrs_into_active_provider_if_wrapper_installed(
106+
sap_attrs: dict,
107+
) -> None:
108+
"""Merge sap-cloud-sdk resource attrs onto the active TracerProvider's
109+
Resource when an OTel auto-instrumentation wrapper has pre-installed it.
110+
111+
Resource.merge direction puts ``sap_attrs`` on the right, so colliding
112+
keys (e.g. ``service.name``) are won by the sap-cloud-sdk side (e.g. the
113+
APPFND_CONHOS_APP_NAME-derived value beats the operator's
114+
k8s-deployment-derived default).
115+
116+
Mutates ``provider._resource`` because OTel SDK exposes no public API
117+
to swap a TracerProvider's Resource post-construction.
118+
"""
119+
provider = trace.get_tracer_provider()
120+
if not isinstance(provider, TracerProvider):
121+
return
122+
existing_attrs = getattr(provider.resource, "attributes", None)
123+
if not isinstance(existing_attrs, Mapping):
124+
return
125+
if "telemetry.auto.version" not in existing_attrs:
126+
return
127+
128+
provider._resource = provider.resource.merge(Resource.create(sap_attrs))
129+
logger.info(
130+
"Merged sap-cloud-sdk resource attrs onto wrapper-installed "
131+
"TracerProvider (marker: telemetry.auto.version)"
132+
)

tests/core/unit/telemetry/test_auto_instrument.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from unittest.mock import patch, MagicMock, create_autospec
55
from contextlib import ExitStack
66

7+
from opentelemetry.sdk.resources import Resource
78
from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider
89

910
from sap_cloud_sdk.core.telemetry.auto_instrument import auto_instrument
@@ -232,3 +233,85 @@ def test_auto_instrument_passes_baggage_span_processor(self, mock_traceloop_comp
232233

233234
mock_traceloop_components['baggage_processor'].assert_called_once()
234235
mock_traceloop_components['get_tracer_provider'].return_value.add_span_processor.assert_called_once_with(mock_processor_instance)
236+
237+
def test_auto_instrument_merges_resource_when_wrapper_installed(self, mock_traceloop_components):
238+
"""When an OTel auto-instrumentation wrapper (e.g. an OpenTelemetry-Operator
239+
init-container injection) has pre-installed a TracerProvider whose Resource
240+
carries the standard `telemetry.auto.version` marker, auto_instrument merges
241+
sap-cloud-sdk attrs onto that provider's existing Resource — preserving the
242+
operator-supplied attrs and adding our SAP enrichment on top."""
243+
mock_traceloop_components['get_app_name'].return_value = 'cloud-sdk-app'
244+
sap_attrs = {
245+
'service.name': 'cloud-sdk-app',
246+
'sap.cloud_sdk.name': 'SAP Cloud SDK for Python',
247+
'sap.cloud_sdk.language': 'python',
248+
'sap.solution_area': 'fina',
249+
'mlflow.experiment_id': '1635264705567712',
250+
}
251+
mock_traceloop_components['create_resource'].return_value = sap_attrs
252+
253+
wrapper_provider = SDKTracerProvider(
254+
resource=Resource.create({
255+
'telemetry.auto.version': '0.62b1',
256+
'k8s.deployment.name': 'cloud-sdk-app-deployment',
257+
'service.name': 'cloud-sdk-app-deployment',
258+
})
259+
)
260+
mock_traceloop_components['get_tracer_provider'].return_value = wrapper_provider
261+
262+
with patch.dict('os.environ', {'OTEL_EXPORTER_OTLP_ENDPOINT': 'http://localhost:4317'}, clear=True):
263+
auto_instrument()
264+
265+
merged_attrs = wrapper_provider.resource.attributes
266+
# Operator-supplied attrs are preserved.
267+
assert merged_attrs['telemetry.auto.version'] == '0.62b1'
268+
assert merged_attrs['k8s.deployment.name'] == 'cloud-sdk-app-deployment'
269+
# sap-cloud-sdk attrs are added.
270+
assert merged_attrs['sap.cloud_sdk.name'] == 'SAP Cloud SDK for Python'
271+
assert merged_attrs['sap.cloud_sdk.language'] == 'python'
272+
assert merged_attrs['sap.solution_area'] == 'fina'
273+
assert merged_attrs['mlflow.experiment_id'] == '1635264705567712'
274+
275+
def test_auto_instrument_skips_merge_when_no_wrapper_marker(self, mock_traceloop_components):
276+
"""When the active TracerProvider's Resource lacks the
277+
`telemetry.auto.version` marker (e.g. a self-installed provider, or no
278+
wrapper at all), auto_instrument does not mutate the provider's Resource."""
279+
mock_traceloop_components['get_app_name'].return_value = 'cloud-sdk-app'
280+
mock_traceloop_components['create_resource'].return_value = {
281+
'sap.cloud_sdk.name': 'SAP Cloud SDK for Python',
282+
'sap.solution_area': 'fina',
283+
}
284+
285+
initial_resource = Resource.create({'service.name': 'self-installed'})
286+
plain_provider = SDKTracerProvider(resource=initial_resource)
287+
mock_traceloop_components['get_tracer_provider'].return_value = plain_provider
288+
289+
with patch.dict('os.environ', {'OTEL_EXPORTER_OTLP_ENDPOINT': 'http://localhost:4317'}, clear=True):
290+
auto_instrument()
291+
292+
# Resource is the original instance — no merge took place.
293+
assert plain_provider.resource is initial_resource
294+
assert 'sap.cloud_sdk.name' not in plain_provider.resource.attributes
295+
assert 'sap.solution_area' not in plain_provider.resource.attributes
296+
297+
def test_auto_instrument_merge_overrides_colliding_service_name(self, mock_traceloop_components):
298+
"""On a wrapper-installed provider, sap-cloud-sdk attrs override colliding
299+
keys: service.name from APPFND_CONHOS_APP_NAME wins over the operator's
300+
k8s-deployment-derived service.name."""
301+
mock_traceloop_components['get_app_name'].return_value = 'cloud-sdk-app'
302+
mock_traceloop_components['create_resource'].return_value = {
303+
'service.name': 'cloud-sdk-app',
304+
}
305+
306+
wrapper_provider = SDKTracerProvider(
307+
resource=Resource.create({
308+
'telemetry.auto.version': '0.62b1',
309+
'service.name': 'operator-supplied-name',
310+
})
311+
)
312+
mock_traceloop_components['get_tracer_provider'].return_value = wrapper_provider
313+
314+
with patch.dict('os.environ', {'OTEL_EXPORTER_OTLP_ENDPOINT': 'http://localhost:4317'}, clear=True):
315+
auto_instrument()
316+
317+
assert wrapper_provider.resource.attributes['service.name'] == 'cloud-sdk-app'

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)