From fb011a0ae88a22405902260e71050c8c8e50453d Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Fri, 4 Sep 2026 11:37:33 -0300 Subject: [PATCH 1/9] Added log filter with identity attributes --- pyproject.toml | 2 +- src/sap_cloud_sdk/core/telemetry/_provider.py | 11 ++- .../core/telemetry/log_filters/identity.py | 35 ++++++++ .../telemetry/log_filters/test_identity.py | 89 +++++++++++++++++++ tests/core/unit/telemetry/test_provider.py | 73 +++++++++++++++ uv.lock | 12 +-- 6 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 src/sap_cloud_sdk/core/telemetry/log_filters/identity.py create mode 100644 tests/core/unit/telemetry/log_filters/test_identity.py diff --git a/pyproject.toml b/pyproject.toml index 14fcb01a..d1ce3d06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.49.2" +version = "0.50.0" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" diff --git a/src/sap_cloud_sdk/core/telemetry/_provider.py b/src/sap_cloud_sdk/core/telemetry/_provider.py index 05851ab1..9c902971 100644 --- a/src/sap_cloud_sdk/core/telemetry/_provider.py +++ b/src/sap_cloud_sdk/core/telemetry/_provider.py @@ -43,6 +43,7 @@ ) from sap_cloud_sdk.core._version import get_version from sap_cloud_sdk.core.telemetry.constants import SDK_PACKAGE_NAME +from sap_cloud_sdk.core.telemetry.log_filters.identity import IdentityLogFilter logger = logging.getLogger(__name__) @@ -114,6 +115,12 @@ def shutdown() -> None: _meter_provider = None +def _make_logging_handler(provider: LoggerProvider) -> LoggingHandler: + handler = LoggingHandler(logger_provider=provider) + handler.addFilter(IdentityLogFilter()) + return handler + + def setup_log_provider() -> Optional[LoggerProvider]: """Set up the global OTel LoggerProvider using the shared resource attributes. @@ -143,7 +150,7 @@ def setup_log_provider() -> Optional[LoggerProvider]: ) _merge_sdk_resource_into_log_provider(existing, resource) if not _root_logger_has_otel_handler(): - logging.getLogger().addHandler(LoggingHandler(logger_provider=existing)) + logging.getLogger().addHandler(_make_logging_handler(existing)) _log_provider = existing else: provider = LoggerProvider(resource=resource) @@ -151,7 +158,7 @@ def setup_log_provider() -> Optional[LoggerProvider]: BatchLogRecordProcessor(_create_log_exporter()) ) set_logger_provider(provider) - logging.getLogger().addHandler(LoggingHandler(logger_provider=provider)) + logging.getLogger().addHandler(_make_logging_handler(provider)) _log_provider = provider logger.info( diff --git a/src/sap_cloud_sdk/core/telemetry/log_filters/identity.py b/src/sap_cloud_sdk/core/telemetry/log_filters/identity.py new file mode 100644 index 00000000..c9f892f9 --- /dev/null +++ b/src/sap_cloud_sdk/core/telemetry/log_filters/identity.py @@ -0,0 +1,35 @@ +"""Log filter that stamps identity attributes (tenant ID, user ID) onto every log record.""" + +import logging + +from sap_cloud_sdk.core.telemetry.constants import ATTR_SAP_TENANT_ID, ATTR_USER_ID + + +def _resolve_log_attributes() -> dict: + try: + from sap_cloud_sdk.core.runtime_context import get_context, GLOBAL_TENANT_ID, USER_ID + from sap_cloud_sdk.ias import get_auth_context + + ctx = get_context() + claims = get_auth_context() + candidates = { + ATTR_SAP_TENANT_ID: ctx.get(GLOBAL_TENANT_ID) or (claims and claims.sap_gtid), + ATTR_USER_ID: ctx.get(USER_ID) or (claims and claims.user_uuid), + } + return {k: v for k, v in candidates.items() if v} + except Exception: + return {} + + +class IdentityLogFilter(logging.Filter): + """Stamps ``sap.tenancy.tenant_id`` and ``user.id`` onto every log record. + + Reads from the SDK runtime context first (populated by ``bootstrap()``), + then falls back to the IAS auth context set by the Starlette middleware. + Attributes are omitted when no identity is available (e.g. outside a request). + """ + + def filter(self, record: logging.LogRecord) -> bool: + for attr, value in _resolve_log_attributes().items(): + setattr(record, attr, value) + return True diff --git a/tests/core/unit/telemetry/log_filters/test_identity.py b/tests/core/unit/telemetry/log_filters/test_identity.py new file mode 100644 index 00000000..563f7fb3 --- /dev/null +++ b/tests/core/unit/telemetry/log_filters/test_identity.py @@ -0,0 +1,89 @@ +"""Tests for log_filters.identity.""" + +import logging +from unittest.mock import patch + +import pytest + +from sap_cloud_sdk.core.telemetry.log_filters.identity import ( + IdentityLogFilter, + _resolve_log_attributes, +) +from sap_cloud_sdk.core.telemetry.constants import ATTR_SAP_TENANT_ID, ATTR_USER_ID + + +def _make_record() -> logging.LogRecord: + return logging.LogRecord("test", logging.INFO, "", 0, "msg", (), None) + + +class TestResolveLogAttributes: + def test_returns_tenant_and_user_from_runtime_context(self): + from sap_cloud_sdk.core.runtime_context.providers._ias import GLOBAL_TENANT_ID, USER_ID + from sap_cloud_sdk.core.runtime_context._context import RuntimeContext, sdk_context + + ctx = RuntimeContext({GLOBAL_TENANT_ID: "t-1", USER_ID: "u-1"}) + with sdk_context(ctx): + attrs = _resolve_log_attributes() + + assert attrs == {ATTR_SAP_TENANT_ID: "t-1", ATTR_USER_ID: "u-1"} + + def test_falls_back_to_auth_context(self): + from sap_cloud_sdk.core.runtime_context._context import RuntimeContext, sdk_context + from sap_cloud_sdk.ias._context import _auth_context_var + from sap_cloud_sdk.ias._token import IASClaims + + claims = IASClaims(sap_gtid="gtid-1", user_uuid="uuid-1") + token = _auth_context_var.set(claims) + try: + with sdk_context(RuntimeContext()): + attrs = _resolve_log_attributes() + finally: + _auth_context_var.reset(token) + + assert attrs == {ATTR_SAP_TENANT_ID: "gtid-1", ATTR_USER_ID: "uuid-1"} + + def test_returns_empty_when_nothing_set(self): + from sap_cloud_sdk.core.runtime_context._context import RuntimeContext, sdk_context + + with sdk_context(RuntimeContext()): + assert _resolve_log_attributes() == {} + + def test_returns_empty_on_exception(self): + with patch("sap_cloud_sdk.core.runtime_context.get_context", side_effect=Exception("boom")): + assert _resolve_log_attributes() == {} + + +class TestIdentityLogFilter: + def test_stamps_all_resolved_attributes(self): + from sap_cloud_sdk.core.runtime_context.providers._ias import GLOBAL_TENANT_ID, USER_ID + from sap_cloud_sdk.core.runtime_context._context import RuntimeContext, sdk_context + + ctx = RuntimeContext({GLOBAL_TENANT_ID: "t-1", USER_ID: "u-1"}) + record = _make_record() + filt = IdentityLogFilter() + + with sdk_context(ctx): + result = filt.filter(record) + + assert result is True + assert getattr(record, ATTR_SAP_TENANT_ID) == "t-1" + assert getattr(record, ATTR_USER_ID) == "u-1" + + def test_skips_attributes_when_nothing_set(self): + from sap_cloud_sdk.core.runtime_context._context import RuntimeContext, sdk_context + + record = _make_record() + filt = IdentityLogFilter() + + with sdk_context(RuntimeContext()): + result = filt.filter(record) + + assert result is True + assert not hasattr(record, ATTR_SAP_TENANT_ID) + assert not hasattr(record, ATTR_USER_ID) + + def test_always_returns_true(self): + record = _make_record() + filt = IdentityLogFilter() + with patch("sap_cloud_sdk.core.telemetry.log_filters.identity._resolve_log_attributes", return_value={}): + assert filt.filter(record) is True diff --git a/tests/core/unit/telemetry/test_provider.py b/tests/core/unit/telemetry/test_provider.py index 7d0d49af..9118eff9 100644 --- a/tests/core/unit/telemetry/test_provider.py +++ b/tests/core/unit/telemetry/test_provider.py @@ -25,7 +25,9 @@ _create_log_exporter, _merge_sdk_resource_into_log_provider, _root_logger_has_otel_handler, + _make_logging_handler, ) +from sap_cloud_sdk.core.telemetry.log_filters.identity import IdentityLogFilter from sap_cloud_sdk.core.telemetry.config import InstrumentationConfig _DELTA_TEMPORALITY = { @@ -399,3 +401,74 @@ def test_returns_true_when_sdk_level_handler_present(self): assert _root_logger_has_otel_handler() is True finally: root.handlers = original + + +class TestMakeLoggingHandler: + def test_returns_logging_handler(self): + from opentelemetry.instrumentation.logging.handler import LoggingHandler + + mock_provider = MagicMock() + handler = _make_logging_handler(mock_provider) + assert isinstance(handler, LoggingHandler) + + def test_installs_identity_filter(self): + mock_provider = MagicMock() + handler = _make_logging_handler(mock_provider) + assert any(isinstance(f, IdentityLogFilter) for f in handler.filters) + + +class TestSetupLogProviderInstallsFilter: + @pytest.fixture(autouse=True) + def reset_log_provider(self): + import sap_cloud_sdk.core.telemetry._provider as provider_module + provider_module._log_provider = None + yield + provider_module._log_provider = None + + def test_normal_path_installs_tenant_id_filter(self): + from opentelemetry.instrumentation.logging.handler import LoggingHandler + + installed_handlers = [] + + def capture_add_handler(handler): + installed_handlers.append(handler) + + mock_root = MagicMock() + mock_root.addHandler.side_effect = capture_add_handler + + with patch("sap_cloud_sdk.core.telemetry._provider.get_config", return_value=_ENABLED_CONFIG): + with patch("sap_cloud_sdk.core.telemetry._provider.Resource"): + with patch("sap_cloud_sdk.core.telemetry._provider._create_log_exporter"): + with patch("sap_cloud_sdk.core.telemetry._provider.BatchLogRecordProcessor"): + with patch("sap_cloud_sdk.core.telemetry._provider.get_logger_provider", return_value=MagicMock()): + with patch("sap_cloud_sdk.core.telemetry._provider.set_logger_provider"): + with patch("logging.getLogger", return_value=mock_root): + setup_log_provider() + + assert len(installed_handlers) == 1 + handler = installed_handlers[0] + assert isinstance(handler, LoggingHandler) + assert any(isinstance(f, IdentityLogFilter) for f in handler.filters) + + def test_merge_path_installs_tenant_id_filter(self): + from opentelemetry.sdk._logs import LoggerProvider as _LP + from opentelemetry.instrumentation.logging.handler import LoggingHandler + + external = MagicMock(spec=_LP) + installed_handlers = [] + + mock_root = MagicMock() + mock_root.addHandler.side_effect = lambda h: installed_handlers.append(h) + + with patch("sap_cloud_sdk.core.telemetry._provider.get_config", return_value=_ENABLED_CONFIG): + with patch("sap_cloud_sdk.core.telemetry._provider.Resource"): + with patch("sap_cloud_sdk.core.telemetry._provider.get_logger_provider", return_value=external): + with patch("sap_cloud_sdk.core.telemetry._provider._merge_sdk_resource_into_log_provider"): + with patch("sap_cloud_sdk.core.telemetry._provider._root_logger_has_otel_handler", return_value=False): + with patch("logging.getLogger", return_value=mock_root): + setup_log_provider() + + assert len(installed_handlers) == 1 + handler = installed_handlers[0] + assert isinstance(handler, LoggingHandler) + assert any(isinstance(f, IdentityLogFilter) for f in handler.filters) diff --git a/uv.lock b/uv.lock index 076a79b4..84eecabd 100644 --- a/uv.lock +++ b/uv.lock @@ -163,9 +163,9 @@ name = "aiologic" version = "0.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sniffio" }, - { name = "typing-extensions" }, - { name = "wrapt" }, + { name = "sniffio", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "wrapt", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/7a/d51f2fde1e8ae8a83431f8e97b7a71e9358cdb1d4d2ce6be387fa44d68de/aiologic-0.17.1.tar.gz", hash = "sha256:2e1b93b9e88ced318c2a63ad7b382688f40cbfe40e3d42258d49dc9c5aea179d", size = 252354, upload-time = "2026-06-27T20:41:33.25Z" } wheels = [ @@ -808,8 +808,8 @@ name = "culsans" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiologic" }, - { name = "typing-extensions" }, + { name = "aiologic", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/e3/49afa1bc180e0d28008ec6bcdf82a4072d1c7a41032b5b759b60814ca4b0/culsans-0.11.0.tar.gz", hash = "sha256:0b43d0d05dce6106293d114c86e3fb4bfc63088cfe8ff08ed3fe36891447fe33", size = 107546, upload-time = "2025-12-31T23:15:38.196Z" } wheels = [ @@ -4152,7 +4152,7 @@ wheels = [ [[package]] name = "sap-cloud-sdk" -version = "0.49.1" +version = "0.50.0" source = { editable = "." } dependencies = [ { name = "cryptography" }, From 17120bbc0ec9d69500d3f0170ca57d4d769a0adf Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Fri, 4 Sep 2026 13:11:40 -0300 Subject: [PATCH 2/9] Linting --- .../core/telemetry/log_filters/identity.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/sap_cloud_sdk/core/telemetry/log_filters/identity.py b/src/sap_cloud_sdk/core/telemetry/log_filters/identity.py index c9f892f9..09e24d69 100644 --- a/src/sap_cloud_sdk/core/telemetry/log_filters/identity.py +++ b/src/sap_cloud_sdk/core/telemetry/log_filters/identity.py @@ -7,14 +7,20 @@ def _resolve_log_attributes() -> dict: try: - from sap_cloud_sdk.core.runtime_context import get_context, GLOBAL_TENANT_ID, USER_ID + from sap_cloud_sdk.core.runtime_context import ( + get_context, + GLOBAL_TENANT_ID, + USER_ID, + ) from sap_cloud_sdk.ias import get_auth_context ctx = get_context() claims = get_auth_context() candidates = { - ATTR_SAP_TENANT_ID: ctx.get(GLOBAL_TENANT_ID) or (claims and claims.sap_gtid), - ATTR_USER_ID: ctx.get(USER_ID) or (claims and claims.user_uuid), + ATTR_SAP_TENANT_ID: ctx.get(GLOBAL_TENANT_ID) + or (claims and claims.sap_gtid), + ATTR_USER_ID: ctx.get(USER_ID) + or (claims and claims.user_uuid), } return {k: v for k, v in candidates.items() if v} except Exception: From 41cb08c1e6d9438518d7e5cf4ea9061cc60dcb27 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Fri, 4 Sep 2026 14:11:21 -0300 Subject: [PATCH 3/9] Linting --- src/sap_cloud_sdk/core/telemetry/log_filters/identity.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sap_cloud_sdk/core/telemetry/log_filters/identity.py b/src/sap_cloud_sdk/core/telemetry/log_filters/identity.py index 09e24d69..14c0df6b 100644 --- a/src/sap_cloud_sdk/core/telemetry/log_filters/identity.py +++ b/src/sap_cloud_sdk/core/telemetry/log_filters/identity.py @@ -19,8 +19,7 @@ def _resolve_log_attributes() -> dict: candidates = { ATTR_SAP_TENANT_ID: ctx.get(GLOBAL_TENANT_ID) or (claims and claims.sap_gtid), - ATTR_USER_ID: ctx.get(USER_ID) - or (claims and claims.user_uuid), + ATTR_USER_ID: ctx.get(USER_ID) or (claims and claims.user_uuid), } return {k: v for k, v in candidates.items() if v} except Exception: From bb723aa86a64aa74493e5036e8b1a573a43d5a60 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Fri, 4 Sep 2026 16:43:07 -0300 Subject: [PATCH 4/9] version bump --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d1ce3d06..98ebd1b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.50.0" +version = "0.51.0" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" diff --git a/uv.lock b/uv.lock index 84eecabd..3636353d 100644 --- a/uv.lock +++ b/uv.lock @@ -4152,7 +4152,7 @@ wheels = [ [[package]] name = "sap-cloud-sdk" -version = "0.50.0" +version = "0.51.0" source = { editable = "." } dependencies = [ { name = "cryptography" }, From 8c266e76d7acdaf70c806e2392b3c7c06ef75fdd Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Fri, 4 Sep 2026 16:56:03 -0300 Subject: [PATCH 5/9] User guide --- .../core/telemetry/log_filters/__init__.py | 3 ++ .../core/telemetry/user-guide.md | 49 +++++++++++++++---- 2 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 src/sap_cloud_sdk/core/telemetry/log_filters/__init__.py diff --git a/src/sap_cloud_sdk/core/telemetry/log_filters/__init__.py b/src/sap_cloud_sdk/core/telemetry/log_filters/__init__.py new file mode 100644 index 00000000..9273143c --- /dev/null +++ b/src/sap_cloud_sdk/core/telemetry/log_filters/__init__.py @@ -0,0 +1,3 @@ +from sap_cloud_sdk.core.telemetry.log_filters.identity import IdentityLogFilter + +__all__ = ["IdentityLogFilter"] diff --git a/src/sap_cloud_sdk/core/telemetry/user-guide.md b/src/sap_cloud_sdk/core/telemetry/user-guide.md index 5b9fbeed..4ff28441 100644 --- a/src/sap_cloud_sdk/core/telemetry/user-guide.md +++ b/src/sap_cloud_sdk/core/telemetry/user-guide.md @@ -43,16 +43,20 @@ with invoke_agent_span( response = client.chat.completions.create(...) ``` -### 3. Set tenant ID at the request boundary +### 3. Tenant and user identity propagates automatically -```python -from sap_cloud_sdk.core.telemetry import set_tenant_id +For Starlette/FastAPI apps, wire `StarletteIASTelemetryMiddleware` once and `sap.tenancy.tenant_id` / `user.id` appear on all traces and logs automatically: +```python +from sap_cloud_sdk.core.telemetry import auto_instrument +from sap_cloud_sdk.core.telemetry.middleware import StarletteIASTelemetryMiddleware -def handle_request(request): - set_tenant_id(extract_tenant_from_jwt(request)) +app = Starlette(...) +auto_instrument(middlewares=[StarletteIASTelemetryMiddleware(app=app)]) ``` +Or use `bootstrap()` with `IASContextProvider` for framework-agnostic identity extraction. See [Multi-tenancy](#multi-tenancy) for details. + --- ## Library instrumentation @@ -204,6 +208,15 @@ logger.warning("Retrying request, attempt %d", attempt) logger.error("Failed to connect", exc_info=True) ``` +### Identity attributes + +`sap.tenancy.tenant_id` and `user.id` are automatically stamped on every log record when a request context is active — no extra code needed. The same identity used for traces is used for logs. + +Resolution priority: +1. Runtime context populated by `bootstrap()` (`GLOBAL_TENANT_ID`, `USER_ID` from `IASContextProvider`) +2. IAS auth context set by `StarletteIASTelemetryMiddleware` (`sap_gtid`, `user_uuid` claims) +3. Omitted when no identity is available (e.g. log lines emitted at startup) + ### Structured fields Use `extra={}` to attach structured attributes to a log record: @@ -384,10 +397,28 @@ auto_instrument(middlewares=[StarletteIASTelemetryMiddleware(app=app)]) ## Multi-tenancy -- **Supported:** N/A -- **Authentication:** N/A -- **How to use:** This is an infrastructure module. `set_tenant_id()` and `StarletteIASTelemetryMiddleware` allow attaching a tenant identifier to OpenTelemetry spans as metadata, but this is observability context. -- **Further reading:** N/A +`sap.tenancy.tenant_id` and `user.id` are automatically propagated to all three OTel signal types — traces, metrics, and logs — when a request context is active. No manual attribute setting is required. + +**For apps using `bootstrap()`** (recommended): + +`IASContextProvider` extracts the tenant and user from the `Authorization: Bearer` header and populates the runtime context. The SDK reads from this context automatically. + +**For Starlette/FastAPI apps using `StarletteIASTelemetryMiddleware`:** + +The middleware parses the IAS JWT on each request and makes the identity available as a fallback when the runtime context is not populated. + +**For other frameworks:** + +Call `set_tenant_id()` manually at the request boundary. Note: this only affects SDK-recorded metrics and spans that explicitly read it — use `bootstrap()` or a middleware for full coverage across all signal types. + +```python +from sap_cloud_sdk.core.telemetry import set_tenant_id + +def handle_request(request): + set_tenant_id(extract_tenant_from_jwt(request)) +``` + +Identity attributes are omitted on signals emitted outside a request context (e.g. at startup) — no empty-string values are injected. ## Configuration From 48f87eab17705ce8b513fd5fca1a4b62ab68b712 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Fri, 4 Sep 2026 16:58:38 -0300 Subject: [PATCH 6/9] Module indicators --- .../core/telemetry/span_processors/__init__.py | 15 +++++++++++++++ tests/core/unit/telemetry/log_filters/__init__.py | 0 2 files changed, 15 insertions(+) create mode 100644 src/sap_cloud_sdk/core/telemetry/span_processors/__init__.py create mode 100644 tests/core/unit/telemetry/log_filters/__init__.py diff --git a/src/sap_cloud_sdk/core/telemetry/span_processors/__init__.py b/src/sap_cloud_sdk/core/telemetry/span_processors/__init__.py new file mode 100644 index 00000000..c9d36a43 --- /dev/null +++ b/src/sap_cloud_sdk/core/telemetry/span_processors/__init__.py @@ -0,0 +1,15 @@ +from sap_cloud_sdk.core.telemetry.span_processors.baggage_span_processor import ( + BaggageSpanProcessor, +) +from sap_cloud_sdk.core.telemetry.span_processors.propagated_attributes_processor import ( + PropagatedAttributesSpanProcessor, +) +from sap_cloud_sdk.core.telemetry.span_processors.runtime_context_processor import ( + RuntimeContextSpanProcessor, +) + +__all__ = [ + "BaggageSpanProcessor", + "PropagatedAttributesSpanProcessor", + "RuntimeContextSpanProcessor", +] diff --git a/tests/core/unit/telemetry/log_filters/__init__.py b/tests/core/unit/telemetry/log_filters/__init__.py new file mode 100644 index 00000000..e69de29b From d67b91dcdecff4e09318274c1b17f7ce980a5deb Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Fri, 4 Sep 2026 16:59:49 -0300 Subject: [PATCH 7/9] Module indicators --- src/sap_cloud_sdk/core/telemetry/user-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sap_cloud_sdk/core/telemetry/user-guide.md b/src/sap_cloud_sdk/core/telemetry/user-guide.md index 4ff28441..b58bdc38 100644 --- a/src/sap_cloud_sdk/core/telemetry/user-guide.md +++ b/src/sap_cloud_sdk/core/telemetry/user-guide.md @@ -55,7 +55,7 @@ app = Starlette(...) auto_instrument(middlewares=[StarletteIASTelemetryMiddleware(app=app)]) ``` -Or use `bootstrap()` with `IASContextProvider` for framework-agnostic identity extraction. See [Multi-tenancy](#multi-tenancy) for details. +Or use `bootstrap()` with `IASContextProvider` for framework-agnostic identity extraction. --- From 6bdee89158b178795a1b2ac51226190f66c284f1 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Fri, 4 Sep 2026 17:00:14 -0300 Subject: [PATCH 8/9] Module indicators --- src/sap_cloud_sdk/core/telemetry/user-guide.md | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/sap_cloud_sdk/core/telemetry/user-guide.md b/src/sap_cloud_sdk/core/telemetry/user-guide.md index b58bdc38..71c1d3cd 100644 --- a/src/sap_cloud_sdk/core/telemetry/user-guide.md +++ b/src/sap_cloud_sdk/core/telemetry/user-guide.md @@ -42,21 +42,6 @@ with invoke_agent_span( # autoinstrumented LLM call is a child of this span response = client.chat.completions.create(...) ``` - -### 3. Tenant and user identity propagates automatically - -For Starlette/FastAPI apps, wire `StarletteIASTelemetryMiddleware` once and `sap.tenancy.tenant_id` / `user.id` appear on all traces and logs automatically: - -```python -from sap_cloud_sdk.core.telemetry import auto_instrument -from sap_cloud_sdk.core.telemetry.middleware import StarletteIASTelemetryMiddleware - -app = Starlette(...) -auto_instrument(middlewares=[StarletteIASTelemetryMiddleware(app=app)]) -``` - -Or use `bootstrap()` with `IASContextProvider` for framework-agnostic identity extraction. - --- ## Library instrumentation From 765801111cb2ad40df391b45ecec803cf2268a98 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Fri, 4 Sep 2026 17:01:20 -0300 Subject: [PATCH 9/9] User guide --- .../core/telemetry/user-guide.md | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/src/sap_cloud_sdk/core/telemetry/user-guide.md b/src/sap_cloud_sdk/core/telemetry/user-guide.md index 71c1d3cd..f0b0f5df 100644 --- a/src/sap_cloud_sdk/core/telemetry/user-guide.md +++ b/src/sap_cloud_sdk/core/telemetry/user-guide.md @@ -380,31 +380,6 @@ auto_instrument(middlewares=[StarletteIASTelemetryMiddleware(app=app)]) --- -## Multi-tenancy - -`sap.tenancy.tenant_id` and `user.id` are automatically propagated to all three OTel signal types — traces, metrics, and logs — when a request context is active. No manual attribute setting is required. - -**For apps using `bootstrap()`** (recommended): - -`IASContextProvider` extracts the tenant and user from the `Authorization: Bearer` header and populates the runtime context. The SDK reads from this context automatically. - -**For Starlette/FastAPI apps using `StarletteIASTelemetryMiddleware`:** - -The middleware parses the IAS JWT on each request and makes the identity available as a fallback when the runtime context is not populated. - -**For other frameworks:** - -Call `set_tenant_id()` manually at the request boundary. Note: this only affects SDK-recorded metrics and spans that explicitly read it — use `bootstrap()` or a middleware for full coverage across all signal types. - -```python -from sap_cloud_sdk.core.telemetry import set_tenant_id - -def handle_request(request): - set_tenant_id(extract_tenant_from_jwt(request)) -``` - -Identity attributes are omitted on signals emitted outside a request context (e.g. at startup) — no empty-string values are injected. - ## Configuration ### Production