From b57420e478eef2d968e2e4dfd6007fd37c295682 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Wed, 12 Aug 2026 16:20:12 -0700 Subject: [PATCH 1/3] Renamed subaccount id attribute --- src/sap_cloud_sdk/core/telemetry/config.py | 2 +- src/sap_cloud_sdk/core/telemetry/constants.py | 2 +- .../core/unit/auditlog_ng/unit/test_client.py | 118 ++++++++++++++++++ 3 files changed, 120 insertions(+), 2 deletions(-) diff --git a/src/sap_cloud_sdk/core/telemetry/config.py b/src/sap_cloud_sdk/core/telemetry/config.py index dbbe23b9..45f3565d 100644 --- a/src/sap_cloud_sdk/core/telemetry/config.py +++ b/src/sap_cloud_sdk/core/telemetry/config.py @@ -135,7 +135,7 @@ def create_resource_attributes_from_env() -> dict: - service.instance.id (from HOSTNAME, defaults to "unknown") - deployment.environment.name (from APPFND_CONHOS_ENVIRONMENT, defaults to "unknown") - cloud.region (from APPFND_CONHOS_REGION, defaults to "unknown") - - sap.cld.subaccount_id (from APPFND_CONHOS_SUBACCOUNTID, defaults to "unknown") + - sap.cloud.provider.subaccount_id (from APPFND_CONHOS_SUBACCOUNTID, defaults to "unknown") - sap.cld.system_role (from APPFND_CONHOS_SYSTEM_ROLE, defaults to "ZAFT") - sap.cloud_sdk.name (constant: "SAP Cloud SDK for Python") - sap.cloud_sdk.language (constant: "python") diff --git a/src/sap_cloud_sdk/core/telemetry/constants.py b/src/sap_cloud_sdk/core/telemetry/constants.py index 875f00ed..fdc17890 100644 --- a/src/sap_cloud_sdk/core/telemetry/constants.py +++ b/src/sap_cloud_sdk/core/telemetry/constants.py @@ -17,7 +17,7 @@ ATTR_USER_ID = "user.id" # Attribute keys - SAP standard -ATTR_SAP_SUBACCOUNT_ID = "sap.cld.subaccount_id" +ATTR_SAP_SUBACCOUNT_ID = "sap.cloud.provider.subaccount_id" ATTR_SAP_TENANT_ID = "sap.tenancy.tenant_id" ATTR_SAP_SYSTEM_ROLE = "sap.cld.system_role" ATTR_SAP_TRIGGER_TYPE = "sap.ai.agent.trigger.type" diff --git a/tests/core/unit/auditlog_ng/unit/test_client.py b/tests/core/unit/auditlog_ng/unit/test_client.py index 8d22a882..04c39e4d 100644 --- a/tests/core/unit/auditlog_ng/unit/test_client.py +++ b/tests/core/unit/auditlog_ng/unit/test_client.py @@ -8,6 +8,12 @@ import pytest +from opentelemetry import context as context_api, trace +from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk._logs.export import InMemoryLogRecordExporter, SimpleLogRecordProcessor +from opentelemetry.sdk.resources import Resource +from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags + from sap_cloud_sdk.core.auditlog_ng.client import AuditClient, _fill_common_from_auth_context from sap_cloud_sdk.core.auditlog_ng.config import AuditLogNGConfig, SCHEMA_URL from sap_cloud_sdk.core.auditlog_ng.exceptions import ValidationError @@ -392,3 +398,115 @@ def test_does_not_overwrite_explicit_timestamp(self): def test_event_without_common_field_is_skipped(self): event = MagicMock(spec=[]) # no 'common' attribute _fill_common_from_auth_context(event) # must not raise + + +# --------------------------------------------------------------------------- +# Helpers shared by TestTraceSpanCorrelation +# --------------------------------------------------------------------------- + +_TRACE_ID = 0xABCD1234ABCD1234ABCD1234ABCD1234 +_SPAN_ID = 0x1234ABCD1234ABCD + + +def _make_real_client_with_exporter() -> tuple[AuditClient, InMemoryLogRecordExporter]: + """Build an AuditClient backed by an InMemoryLogRecordExporter (no mocks).""" + exporter = InMemoryLogRecordExporter() + provider = LoggerProvider( + resource=Resource.create( + { + "service.name": "test", + "sap.ucl.deployment_id": "dep-1", + "sap.ucl.system_namespace": "ns-1", + } + ) + ) + provider.add_log_record_processor(SimpleLogRecordProcessor(exporter)) + + config = _make_config() + # Bypass the real gRPC exporter and inject our in-memory provider directly. + with patch("sap_cloud_sdk.core.auditlog_ng.client.GRPCLogExporter"), \ + patch("sap_cloud_sdk.core.auditlog_ng.client.LoggerProvider", return_value=provider): + client = AuditClient(config) + + return client, exporter + + +def _make_valid_event(tenant_id: str = "tenant-123") -> pb.DataAccess: + event = pb.DataAccess() + event.common.tenant_id = tenant_id + event.common.user_initiator_id = "user@example.com" + event.common.timestamp.FromDatetime(datetime.now(timezone.utc)) + event.channel_type = "API" + event.channel_id = "ch-001" + event.object_type = "Resource" + event.object_id = "obj-001" + event.attribute = "attr-001" + return event + + +class TestTraceSpanCorrelation: + """Verify that trace_id and span_id from an active OTel span are + automatically captured on emitted log records.""" + + def test_active_span_correlates_trace_and_span_id(self): + client, exporter = _make_real_client_with_exporter() + + span_ctx = SpanContext( + trace_id=_TRACE_ID, + span_id=_SPAN_ID, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + span = NonRecordingSpan(span_ctx) + ctx = trace.set_span_in_context(span) + token = context_api.attach(ctx) + try: + client.send(_make_valid_event()) + finally: + context_api.detach(token) + + records = exporter.get_finished_logs() + assert len(records) == 1 + log = records[0].log_record + assert log.trace_id == _TRACE_ID + assert log.span_id == _SPAN_ID + assert log.trace_flags == TraceFlags(TraceFlags.SAMPLED) + + def test_no_active_span_yields_zero_ids(self): + client, exporter = _make_real_client_with_exporter() + + # Ensure no span is active (detach any inherited context). + empty_ctx = context_api.Context() + token = context_api.attach(empty_ctx) + try: + client.send(_make_valid_event()) + finally: + context_api.detach(token) + + records = exporter.get_finished_logs() + assert len(records) == 1 + log = records[0].log_record + assert log.trace_id == 0 + assert log.span_id == 0 + + def test_log_record_carries_audit_attributes_alongside_trace_context(self): + client, exporter = _make_real_client_with_exporter() + + span_ctx = SpanContext( + trace_id=_TRACE_ID, + span_id=_SPAN_ID, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + token = context_api.attach(trace.set_span_in_context(NonRecordingSpan(span_ctx))) + try: + event_id = client.send(_make_valid_event()) + finally: + context_api.detach(token) + + log = exporter.get_finished_logs()[0].log_record + assert log.trace_id == _TRACE_ID + assert log.span_id == _SPAN_ID + assert log.attributes["cloudevents.event_id"] == event_id + assert log.attributes["sap.tenancy.tenant_id"] == "tenant-123" + assert log.attributes["sap.auditlogging.mime_type"] == "application/protobuf" From 76c6fd5faa2a8db28b5489206af33593323b6831 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Wed, 12 Aug 2026 16:23:55 -0700 Subject: [PATCH 2/3] Version bump --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 05f1d1b8..9baab7b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.43.1" +version = "0.43.2" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" diff --git a/uv.lock b/uv.lock index f7b38236..643d657f 100644 --- a/uv.lock +++ b/uv.lock @@ -3925,7 +3925,7 @@ wheels = [ [[package]] name = "sap-cloud-sdk" -version = "0.42.0" +version = "0.43.2" source = { editable = "." } dependencies = [ { name = "cryptography" }, From 46d37442f6b4de9a021bfa212810853c3e0242c5 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Wed, 12 Aug 2026 16:25:43 -0700 Subject: [PATCH 3/3] revert --- .../core/unit/auditlog_ng/unit/test_client.py | 118 ------------------ 1 file changed, 118 deletions(-) diff --git a/tests/core/unit/auditlog_ng/unit/test_client.py b/tests/core/unit/auditlog_ng/unit/test_client.py index 04c39e4d..8d22a882 100644 --- a/tests/core/unit/auditlog_ng/unit/test_client.py +++ b/tests/core/unit/auditlog_ng/unit/test_client.py @@ -8,12 +8,6 @@ import pytest -from opentelemetry import context as context_api, trace -from opentelemetry.sdk._logs import LoggerProvider -from opentelemetry.sdk._logs.export import InMemoryLogRecordExporter, SimpleLogRecordProcessor -from opentelemetry.sdk.resources import Resource -from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags - from sap_cloud_sdk.core.auditlog_ng.client import AuditClient, _fill_common_from_auth_context from sap_cloud_sdk.core.auditlog_ng.config import AuditLogNGConfig, SCHEMA_URL from sap_cloud_sdk.core.auditlog_ng.exceptions import ValidationError @@ -398,115 +392,3 @@ def test_does_not_overwrite_explicit_timestamp(self): def test_event_without_common_field_is_skipped(self): event = MagicMock(spec=[]) # no 'common' attribute _fill_common_from_auth_context(event) # must not raise - - -# --------------------------------------------------------------------------- -# Helpers shared by TestTraceSpanCorrelation -# --------------------------------------------------------------------------- - -_TRACE_ID = 0xABCD1234ABCD1234ABCD1234ABCD1234 -_SPAN_ID = 0x1234ABCD1234ABCD - - -def _make_real_client_with_exporter() -> tuple[AuditClient, InMemoryLogRecordExporter]: - """Build an AuditClient backed by an InMemoryLogRecordExporter (no mocks).""" - exporter = InMemoryLogRecordExporter() - provider = LoggerProvider( - resource=Resource.create( - { - "service.name": "test", - "sap.ucl.deployment_id": "dep-1", - "sap.ucl.system_namespace": "ns-1", - } - ) - ) - provider.add_log_record_processor(SimpleLogRecordProcessor(exporter)) - - config = _make_config() - # Bypass the real gRPC exporter and inject our in-memory provider directly. - with patch("sap_cloud_sdk.core.auditlog_ng.client.GRPCLogExporter"), \ - patch("sap_cloud_sdk.core.auditlog_ng.client.LoggerProvider", return_value=provider): - client = AuditClient(config) - - return client, exporter - - -def _make_valid_event(tenant_id: str = "tenant-123") -> pb.DataAccess: - event = pb.DataAccess() - event.common.tenant_id = tenant_id - event.common.user_initiator_id = "user@example.com" - event.common.timestamp.FromDatetime(datetime.now(timezone.utc)) - event.channel_type = "API" - event.channel_id = "ch-001" - event.object_type = "Resource" - event.object_id = "obj-001" - event.attribute = "attr-001" - return event - - -class TestTraceSpanCorrelation: - """Verify that trace_id and span_id from an active OTel span are - automatically captured on emitted log records.""" - - def test_active_span_correlates_trace_and_span_id(self): - client, exporter = _make_real_client_with_exporter() - - span_ctx = SpanContext( - trace_id=_TRACE_ID, - span_id=_SPAN_ID, - is_remote=False, - trace_flags=TraceFlags(TraceFlags.SAMPLED), - ) - span = NonRecordingSpan(span_ctx) - ctx = trace.set_span_in_context(span) - token = context_api.attach(ctx) - try: - client.send(_make_valid_event()) - finally: - context_api.detach(token) - - records = exporter.get_finished_logs() - assert len(records) == 1 - log = records[0].log_record - assert log.trace_id == _TRACE_ID - assert log.span_id == _SPAN_ID - assert log.trace_flags == TraceFlags(TraceFlags.SAMPLED) - - def test_no_active_span_yields_zero_ids(self): - client, exporter = _make_real_client_with_exporter() - - # Ensure no span is active (detach any inherited context). - empty_ctx = context_api.Context() - token = context_api.attach(empty_ctx) - try: - client.send(_make_valid_event()) - finally: - context_api.detach(token) - - records = exporter.get_finished_logs() - assert len(records) == 1 - log = records[0].log_record - assert log.trace_id == 0 - assert log.span_id == 0 - - def test_log_record_carries_audit_attributes_alongside_trace_context(self): - client, exporter = _make_real_client_with_exporter() - - span_ctx = SpanContext( - trace_id=_TRACE_ID, - span_id=_SPAN_ID, - is_remote=False, - trace_flags=TraceFlags(TraceFlags.SAMPLED), - ) - token = context_api.attach(trace.set_span_in_context(NonRecordingSpan(span_ctx))) - try: - event_id = client.send(_make_valid_event()) - finally: - context_api.detach(token) - - log = exporter.get_finished_logs()[0].log_record - assert log.trace_id == _TRACE_ID - assert log.span_id == _SPAN_ID - assert log.attributes["cloudevents.event_id"] == event_id - assert log.attributes["sap.tenancy.tenant_id"] == "tenant-123" - assert log.attributes["sap.auditlogging.mime_type"] == "application/protobuf"