From 11d1650e1f2af89807cb8f7d04e05afb13f6afd5 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Mon, 31 Aug 2026 11:33:44 -0300 Subject: [PATCH 01/10] Registry adapter --- src/sap_cloud_sdk/__init__.py | 9 +++- src/sap_cloud_sdk/core/bootstrap.py | 3 +- .../core/runtime_context/_registry.py | 21 +++++++++ .../runtime_context/adapters/_starlette.py | 2 + .../telemetry/instrumentation/_registry.py | 17 ++++++++ .../core/telemetry/instrumentation/base.py | 4 ++ .../runtime_context/test_runtime_context.py | 40 +++++++++++++++++ .../instrumentation/test_instrumentation.py | 43 +++++++++++++++++++ 8 files changed, 137 insertions(+), 2 deletions(-) diff --git a/src/sap_cloud_sdk/__init__.py b/src/sap_cloud_sdk/__init__.py index f358d571..5986dd08 100644 --- a/src/sap_cloud_sdk/__init__.py +++ b/src/sap_cloud_sdk/__init__.py @@ -1,5 +1,12 @@ # SAP Cloud SDK for Python from sap_cloud_sdk.core.bootstrap import bootstrap, TelemetryConfig +from sap_cloud_sdk.core.runtime_context._registry import get_framework_adapters +from sap_cloud_sdk.core.telemetry.instrumentation._registry import get_instrumented_libraries -__all__ = ["bootstrap", "TelemetryConfig"] +__all__ = [ + "bootstrap", + "TelemetryConfig", + "get_framework_adapters", + "get_instrumented_libraries", +] diff --git a/src/sap_cloud_sdk/core/bootstrap.py b/src/sap_cloud_sdk/core/bootstrap.py index 9ef0a4ec..f3649702 100644 --- a/src/sap_cloud_sdk/core/bootstrap.py +++ b/src/sap_cloud_sdk/core/bootstrap.py @@ -4,7 +4,7 @@ from typing import Any, List, Optional from sap_cloud_sdk.core.runtime_context._protocol import ContextProvider -from sap_cloud_sdk.core.runtime_context._registry import get_registry +from sap_cloud_sdk.core.runtime_context._registry import get_registry, record_attached from sap_cloud_sdk.core.runtime_context import ( DWCContextProvider, IASContextProvider, @@ -89,6 +89,7 @@ def bootstrap( for adapter in get_registry(): if adapter.matches(app): adapter.attach(app, providers) + record_attached(adapter.name) return raise TypeError( diff --git a/src/sap_cloud_sdk/core/runtime_context/_registry.py b/src/sap_cloud_sdk/core/runtime_context/_registry.py index 09e168fd..12dcba7e 100644 --- a/src/sap_cloud_sdk/core/runtime_context/_registry.py +++ b/src/sap_cloud_sdk/core/runtime_context/_registry.py @@ -11,6 +11,7 @@ logger = logging.getLogger(__name__) _registry: List[FrameworkAdapter] = [] +_attached: List[str] = [] def register(adapter: FrameworkAdapter) -> None: @@ -22,6 +23,21 @@ def get_registry() -> List[FrameworkAdapter]: return list(_registry) +def record_attached(name: str) -> None: + """Record a framework adapter as attached. Called by bootstrap().""" + _attached.append(name) + + +def get_framework_adapters() -> List[str]: + """Return the names of framework adapters attached via bootstrap(). + + Each entry corresponds to one :func:`~sap_cloud_sdk.bootstrap` call that + successfully matched and attached an adapter (e.g. ``"starlette"``). + Returns an empty list if bootstrap() has not been called yet. + """ + return list(_attached) + + class FrameworkAdapter(ABC): """Connects a framework or invocation source to the SDK runtime context. @@ -33,6 +49,8 @@ class FrameworkAdapter(ABC): Example:: class FlaskContextAdapter(FrameworkAdapter): + name = "flask" + def _matches(self, app) -> bool: from flask import Flask return isinstance(app, Flask) @@ -43,6 +61,9 @@ def attach(self, app, providers) -> None: register(FlaskContextAdapter()) """ + #: Human-readable identifier for this adapter (e.g. ``"starlette"``). + name: str + def matches(self, app) -> bool: """Return True if this adapter handles *app*'s framework type.""" try: diff --git a/src/sap_cloud_sdk/core/runtime_context/adapters/_starlette.py b/src/sap_cloud_sdk/core/runtime_context/adapters/_starlette.py index 14df6f92..6399f9ce 100644 --- a/src/sap_cloud_sdk/core/runtime_context/adapters/_starlette.py +++ b/src/sap_cloud_sdk/core/runtime_context/adapters/_starlette.py @@ -7,6 +7,8 @@ class _StarletteContextAdapter(FrameworkAdapter): + name = "starlette" + def _matches(self, app) -> bool: from starlette.applications import Starlette diff --git a/src/sap_cloud_sdk/core/telemetry/instrumentation/_registry.py b/src/sap_cloud_sdk/core/telemetry/instrumentation/_registry.py index d0aa9155..84da6a8f 100644 --- a/src/sap_cloud_sdk/core/telemetry/instrumentation/_registry.py +++ b/src/sap_cloud_sdk/core/telemetry/instrumentation/_registry.py @@ -1,6 +1,7 @@ from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor _registry: list[LibraryInstrumentor] = [] +_instrumented: list[str] = [] def register(instrumentor: LibraryInstrumentor) -> None: @@ -14,3 +15,19 @@ def register(instrumentor: LibraryInstrumentor) -> None: def get_registry() -> list[LibraryInstrumentor]: return list(_registry) + + +def record_instrumented(name: str) -> None: + """Record a library as successfully instrumented. Called by LibraryInstrumentor.""" + _instrumented.append(name) + + +def get_instrumented_libraries() -> list[str]: + """Return the names of libraries successfully instrumented via auto_instrument(). + + Each entry corresponds to a library that was installed and patched with OTel + (e.g. ``"httpx"``, ``"sqlalchemy"``). Libraries that were skipped because they + are not installed do not appear in this list. Returns an empty list if + auto_instrument() has not been called yet. + """ + return list(_instrumented) diff --git a/src/sap_cloud_sdk/core/telemetry/instrumentation/base.py b/src/sap_cloud_sdk/core/telemetry/instrumentation/base.py index 6e05da63..1b8206ff 100644 --- a/src/sap_cloud_sdk/core/telemetry/instrumentation/base.py +++ b/src/sap_cloud_sdk/core/telemetry/instrumentation/base.py @@ -37,6 +37,10 @@ def instrument(self, **kwargs: Any) -> None: "%s instrumentation skipped — library not importable", self.library_name ) return + from sap_cloud_sdk.core.telemetry.instrumentation._registry import ( + record_instrumented, + ) + record_instrumented(self.library_name) logger.debug("Instrumented %s", self.library_name) def uninstrument(self) -> None: diff --git a/tests/core/unit/runtime_context/test_runtime_context.py b/tests/core/unit/runtime_context/test_runtime_context.py index ae417b1c..b6ec0a84 100644 --- a/tests/core/unit/runtime_context/test_runtime_context.py +++ b/tests/core/unit/runtime_context/test_runtime_context.py @@ -25,6 +25,10 @@ sdk_context, set_context, ) +from sap_cloud_sdk.core.runtime_context._registry import ( + get_framework_adapters, + record_attached, +) from sap_cloud_sdk.core.runtime_context.providers._ias import ( APP_TENANT_ID, GLOBAL_TENANT_ID, @@ -432,3 +436,39 @@ def test_single_context_passthrough(self): ctx = RuntimeContext({key: "v"}) merged = _merge([ctx]) assert merged.get(key) == "v" + + +# --------------------------------------------------------------------------- +# get_framework_adapters +# --------------------------------------------------------------------------- + + +class TestGetFrameworkAdapters: + def setup_method(self): + from sap_cloud_sdk.core.runtime_context import _registry as registry_mod + self._original = list(registry_mod._attached) + registry_mod._attached.clear() + + def teardown_method(self): + from sap_cloud_sdk.core.runtime_context import _registry as registry_mod + registry_mod._attached.clear() + registry_mod._attached.extend(self._original) + + def test_empty_before_bootstrap(self): + assert get_framework_adapters() == [] + + def test_records_name_after_record_attached(self): + record_attached("starlette") + assert get_framework_adapters() == ["starlette"] + + def test_multiple_calls_accumulate(self): + record_attached("starlette") + record_attached("flask") + assert get_framework_adapters() == ["starlette", "flask"] + + def test_returns_copy(self): + record_attached("starlette") + snapshot = get_framework_adapters() + snapshot.clear() + assert get_framework_adapters() == ["starlette"] + diff --git a/tests/core/unit/telemetry/instrumentation/test_instrumentation.py b/tests/core/unit/telemetry/instrumentation/test_instrumentation.py index 9c198cf2..dc33224d 100644 --- a/tests/core/unit/telemetry/instrumentation/test_instrumentation.py +++ b/tests/core/unit/telemetry/instrumentation/test_instrumentation.py @@ -7,6 +7,8 @@ from sap_cloud_sdk.core.telemetry.instrumentation._registry import ( _registry, get_registry, + get_instrumented_libraries, + record_instrumented, register, ) @@ -258,3 +260,44 @@ def test_instrument_libraries_calls_all_registered(self): with patch.object(registry_mod, "_registry", [mock_inst]): _instrument_libraries() mock_inst.instrument.assert_called_once() + + +# --------------------------------------------------------------------------- +# get_instrumented_libraries +# --------------------------------------------------------------------------- + +class TestGetInstrumentedLibraries: + def setup_method(self): + from sap_cloud_sdk.core.telemetry.instrumentation import _registry as registry_mod + self._original = list(registry_mod._instrumented) + registry_mod._instrumented.clear() + + def teardown_method(self): + from sap_cloud_sdk.core.telemetry.instrumentation import _registry as registry_mod + registry_mod._instrumented.clear() + registry_mod._instrumented.extend(self._original) + + def test_empty_before_any_instrumentation(self): + assert get_instrumented_libraries() == [] + + def test_records_library_after_successful_instrument(self): + inst = _ConcreteInstrumentor() + inst.instrument() + assert "sys" in get_instrumented_libraries() + + def test_skipped_library_not_recorded(self): + inst = _MissingLibraryInstrumentor() + inst.instrument() + assert "_nonexistent_library_xyz" not in get_instrumented_libraries() + + def test_idempotent_instrument_records_only_once(self): + inst = _ConcreteInstrumentor() + inst.instrument() + inst.instrument() # is_instrumented() returns True, so _instrument() is skipped + assert get_instrumented_libraries().count("sys") == 1 + + def test_returns_copy(self): + record_instrumented("httpx") + snapshot = get_instrumented_libraries() + snapshot.clear() + assert "httpx" in get_instrumented_libraries() From ea07ddd82e199a512ea313ca2b8afc1110d3b210 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Mon, 31 Aug 2026 11:34:37 -0300 Subject: [PATCH 02/10] Version bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 419e1792..8395015c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.46.1" +version = "0.47.1" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" From 7ebdaac8d852464fb4e83d60d04ea35e62569062 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Mon, 31 Aug 2026 11:37:46 -0300 Subject: [PATCH 03/10] User guide --- .../core/runtime_context/user-guide.md | 26 +++++++++++++++++++ .../core/telemetry/user-guide.md | 12 +++++++++ 2 files changed, 38 insertions(+) diff --git a/src/sap_cloud_sdk/core/runtime_context/user-guide.md b/src/sap_cloud_sdk/core/runtime_context/user-guide.md index e9709d13..19a1884e 100644 --- a/src/sap_cloud_sdk/core/runtime_context/user-guide.md +++ b/src/sap_cloud_sdk/core/runtime_context/user-guide.md @@ -210,6 +210,8 @@ from sap_cloud_sdk.core.runtime_context import ( class FlaskContextAdapter(FrameworkAdapter): + name = "flask" + def _matches(self, app) -> bool: from flask import Flask @@ -226,6 +228,30 @@ register(FlaskContextAdapter()) --- +## Introspection + +Use `get_framework_adapters()` to check which framework adapters have been attached at runtime: + +```python +from sap_cloud_sdk import get_framework_adapters + +get_framework_adapters() # -> ["starlette"] after bootstrap(app), [] before +``` + +This is useful for modules that need to fail fast if their required framework was never bootstrapped: + +```python +if "starlette" not in get_framework_adapters(): + raise RuntimeError( + "This client requires Starlette to be bootstrapped. " + "Call bootstrap(app) with your Starlette/FastAPI app." + ) +``` + +Returns an empty list if `bootstrap()` has not been called yet. Each entry corresponds to one successful `bootstrap(app)` call. + +--- + ## Running the tests ```bash diff --git a/src/sap_cloud_sdk/core/telemetry/user-guide.md b/src/sap_cloud_sdk/core/telemetry/user-guide.md index ae6ac4c6..4efaf235 100644 --- a/src/sap_cloud_sdk/core/telemetry/user-guide.md +++ b/src/sap_cloud_sdk/core/telemetry/user-guide.md @@ -78,6 +78,18 @@ Instrumentation activates based on what is installed in the service, not on what The SDK ships `opentelemetry-instrumentation-*` packages for all of the above as hard dependencies. The target frameworks themselves are optional — install them via your service's own requirements or via the SDK's convenience extras (e.g. `sap-cloud-sdk[django]`). +### Introspection + +Use `get_instrumented_libraries()` to query which libraries were actually patched at runtime: + +```python +from sap_cloud_sdk import get_instrumented_libraries + +get_instrumented_libraries() # -> ["httpx", "sqlalchemy", ...] after auto_instrument(), [] before +``` + +Only libraries that were installed **and** successfully instrumented appear in the list. Libraries skipped because they are not installed do not appear. Returns an empty list if `auto_instrument()` has not been called yet. + --- ## Span functions From a6614679dcf12a84811ea872160f01150d889f70 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Mon, 31 Aug 2026 11:51:20 -0300 Subject: [PATCH 04/10] Linting --- src/sap_cloud_sdk/__init__.py | 4 +++- src/sap_cloud_sdk/core/telemetry/instrumentation/base.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sap_cloud_sdk/__init__.py b/src/sap_cloud_sdk/__init__.py index 5986dd08..4fa19fd7 100644 --- a/src/sap_cloud_sdk/__init__.py +++ b/src/sap_cloud_sdk/__init__.py @@ -2,7 +2,9 @@ from sap_cloud_sdk.core.bootstrap import bootstrap, TelemetryConfig from sap_cloud_sdk.core.runtime_context._registry import get_framework_adapters -from sap_cloud_sdk.core.telemetry.instrumentation._registry import get_instrumented_libraries +from sap_cloud_sdk.core.telemetry.instrumentation._registry import ( + get_instrumented_libraries, +) __all__ = [ "bootstrap", diff --git a/src/sap_cloud_sdk/core/telemetry/instrumentation/base.py b/src/sap_cloud_sdk/core/telemetry/instrumentation/base.py index 1b8206ff..c11834e3 100644 --- a/src/sap_cloud_sdk/core/telemetry/instrumentation/base.py +++ b/src/sap_cloud_sdk/core/telemetry/instrumentation/base.py @@ -40,6 +40,7 @@ def instrument(self, **kwargs: Any) -> None: from sap_cloud_sdk.core.telemetry.instrumentation._registry import ( record_instrumented, ) + record_instrumented(self.library_name) logger.debug("Instrumented %s", self.library_name) From c2da9e7c59a320c2c4709ce411e5a8c78587dfff Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Mon, 31 Aug 2026 11:59:17 -0300 Subject: [PATCH 05/10] Linting --- tests/core/unit/runtime_context/test_runtime_context.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/core/unit/runtime_context/test_runtime_context.py b/tests/core/unit/runtime_context/test_runtime_context.py index b6ec0a84..9b1e32b7 100644 --- a/tests/core/unit/runtime_context/test_runtime_context.py +++ b/tests/core/unit/runtime_context/test_runtime_context.py @@ -471,4 +471,3 @@ def test_returns_copy(self): snapshot = get_framework_adapters() snapshot.clear() assert get_framework_adapters() == ["starlette"] - From 2860af4530ebddefc113ed04e731ae197804d3ff Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Mon, 31 Aug 2026 12:40:21 -0300 Subject: [PATCH 06/10] Version bump --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8395015c..4124dc96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.47.1" +version = "0.47.0" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" diff --git a/uv.lock b/uv.lock index c55004dc..ac0e3fb6 100644 --- a/uv.lock +++ b/uv.lock @@ -3925,7 +3925,7 @@ wheels = [ [[package]] name = "sap-cloud-sdk" -version = "0.46.1" +version = "0.47.0" source = { editable = "." } dependencies = [ { name = "cryptography" }, From c76b6a99de5ce79ca02322ac85d4dbc9dcfe4c72 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Mon, 31 Aug 2026 16:36:55 -0300 Subject: [PATCH 07/10] Used enums instead of plain strings for libraries and adapters --- src/sap_cloud_sdk/__init__.py | 7 ++-- .../core/runtime_context/_registry.py | 30 +++++++++++----- .../runtime_context/adapters/_starlette.py | 6 ++-- .../core/runtime_context/user-guide.md | 10 +++--- .../telemetry/instrumentation/_registry.py | 35 ++++++++++++++----- .../core/telemetry/instrumentation/base.py | 9 +++-- .../instrumentation/instrumentors/aiohttp.py | 4 +-- .../instrumentation/instrumentors/django.py | 4 +-- .../instrumentation/instrumentors/fastapi.py | 4 +-- .../instrumentation/instrumentors/flask.py | 4 +-- .../instrumentation/instrumentors/grpc.py | 4 +-- .../instrumentation/instrumentors/httpx.py | 4 +-- .../instrumentation/instrumentors/logging.py | 4 +-- .../instrumentation/instrumentors/requests.py | 4 +-- .../instrumentors/sqlalchemy.py | 4 +-- .../instrumentors/starlette.py | 4 +-- .../core/telemetry/user-guide.md | 4 +-- .../runtime_context/test_runtime_context.py | 15 ++++---- .../instrumentation/test_instrumentation.py | 5 +-- 19 files changed, 102 insertions(+), 59 deletions(-) diff --git a/src/sap_cloud_sdk/__init__.py b/src/sap_cloud_sdk/__init__.py index 4fa19fd7..de2283a2 100644 --- a/src/sap_cloud_sdk/__init__.py +++ b/src/sap_cloud_sdk/__init__.py @@ -1,14 +1,17 @@ # SAP Cloud SDK for Python from sap_cloud_sdk.core.bootstrap import bootstrap, TelemetryConfig -from sap_cloud_sdk.core.runtime_context._registry import get_framework_adapters +from sap_cloud_sdk.core.runtime_context._registry import Adapter, get_framework_adapters from sap_cloud_sdk.core.telemetry.instrumentation._registry import ( + Library, get_instrumented_libraries, ) __all__ = [ + "Adapter", "bootstrap", - "TelemetryConfig", "get_framework_adapters", "get_instrumented_libraries", + "Library", + "TelemetryConfig", ] diff --git a/src/sap_cloud_sdk/core/runtime_context/_registry.py b/src/sap_cloud_sdk/core/runtime_context/_registry.py index 12dcba7e..2752c6f5 100644 --- a/src/sap_cloud_sdk/core/runtime_context/_registry.py +++ b/src/sap_cloud_sdk/core/runtime_context/_registry.py @@ -4,14 +4,22 @@ import logging from abc import ABC, abstractmethod +from enum import StrEnum from typing import List from sap_cloud_sdk.core.runtime_context._protocol import ContextProvider logger = logging.getLogger(__name__) + +class Adapter(StrEnum): + """Known framework adapters that can be attached via :func:`~sap_cloud_sdk.bootstrap`.""" + + STARLETTE = "starlette" + + _registry: List[FrameworkAdapter] = [] -_attached: List[str] = [] +_attached: List[Adapter] = [] def register(adapter: FrameworkAdapter) -> None: @@ -23,16 +31,17 @@ def get_registry() -> List[FrameworkAdapter]: return list(_registry) -def record_attached(name: str) -> None: +def record_attached(name: Adapter) -> None: """Record a framework adapter as attached. Called by bootstrap().""" - _attached.append(name) + if name not in _attached: + _attached.append(name) -def get_framework_adapters() -> List[str]: - """Return the names of framework adapters attached via bootstrap(). +def get_framework_adapters() -> List[Adapter]: + """Return the adapters attached via bootstrap(). Each entry corresponds to one :func:`~sap_cloud_sdk.bootstrap` call that - successfully matched and attached an adapter (e.g. ``"starlette"``). + successfully matched and attached an adapter (e.g. :attr:`Adapter.STARLETTE`). Returns an empty list if bootstrap() has not been called yet. """ return list(_attached) @@ -49,7 +58,9 @@ class FrameworkAdapter(ABC): Example:: class FlaskContextAdapter(FrameworkAdapter): - name = "flask" + @property + def name(self) -> str: + return "flask" def _matches(self, app) -> bool: from flask import Flask @@ -61,8 +72,9 @@ def attach(self, app, providers) -> None: register(FlaskContextAdapter()) """ - #: Human-readable identifier for this adapter (e.g. ``"starlette"``). - name: str + @property + @abstractmethod + def name(self) -> Adapter: ... def matches(self, app) -> bool: """Return True if this adapter handles *app*'s framework type.""" diff --git a/src/sap_cloud_sdk/core/runtime_context/adapters/_starlette.py b/src/sap_cloud_sdk/core/runtime_context/adapters/_starlette.py index 6399f9ce..38c94517 100644 --- a/src/sap_cloud_sdk/core/runtime_context/adapters/_starlette.py +++ b/src/sap_cloud_sdk/core/runtime_context/adapters/_starlette.py @@ -3,11 +3,13 @@ from typing import List from sap_cloud_sdk.core.runtime_context._protocol import ContextProvider -from sap_cloud_sdk.core.runtime_context._registry import FrameworkAdapter, register +from sap_cloud_sdk.core.runtime_context._registry import Adapter, FrameworkAdapter, register class _StarletteContextAdapter(FrameworkAdapter): - name = "starlette" + @property + def name(self) -> Adapter: + return Adapter.STARLETTE def _matches(self, app) -> bool: from starlette.applications import Starlette diff --git a/src/sap_cloud_sdk/core/runtime_context/user-guide.md b/src/sap_cloud_sdk/core/runtime_context/user-guide.md index 19a1884e..7dec3909 100644 --- a/src/sap_cloud_sdk/core/runtime_context/user-guide.md +++ b/src/sap_cloud_sdk/core/runtime_context/user-guide.md @@ -210,7 +210,9 @@ from sap_cloud_sdk.core.runtime_context import ( class FlaskContextAdapter(FrameworkAdapter): - name = "flask" + @property + def name(self) -> str: + return "flask" def _matches(self, app) -> bool: from flask import Flask @@ -233,15 +235,15 @@ register(FlaskContextAdapter()) Use `get_framework_adapters()` to check which framework adapters have been attached at runtime: ```python -from sap_cloud_sdk import get_framework_adapters +from sap_cloud_sdk import Adapter, get_framework_adapters -get_framework_adapters() # -> ["starlette"] after bootstrap(app), [] before +get_framework_adapters() # -> [Adapter.STARLETTE] after bootstrap(app), [] before ``` This is useful for modules that need to fail fast if their required framework was never bootstrapped: ```python -if "starlette" not in get_framework_adapters(): +if Adapter.STARLETTE not in get_framework_adapters(): raise RuntimeError( "This client requires Starlette to be bootstrapped. " "Call bootstrap(app) with your Starlette/FastAPI app." diff --git a/src/sap_cloud_sdk/core/telemetry/instrumentation/_registry.py b/src/sap_cloud_sdk/core/telemetry/instrumentation/_registry.py index 84da6a8f..f787cddc 100644 --- a/src/sap_cloud_sdk/core/telemetry/instrumentation/_registry.py +++ b/src/sap_cloud_sdk/core/telemetry/instrumentation/_registry.py @@ -1,7 +1,25 @@ +from enum import StrEnum + from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor + +class Library(StrEnum): + """Known libraries that can be instrumented via :func:`~sap_cloud_sdk.core.telemetry.auto_instrument`.""" + + AIOHTTP = "aiohttp" + DJANGO = "django" + FASTAPI = "fastapi" + FLASK = "flask" + GRPC = "grpc" + HTTPX = "httpx" + LOGGING = "logging" + REQUESTS = "requests" + SQLALCHEMY = "sqlalchemy" + STARLETTE = "starlette" + + _registry: list[LibraryInstrumentor] = [] -_instrumented: list[str] = [] +_instrumented: list[Library] = [] def register(instrumentor: LibraryInstrumentor) -> None: @@ -17,17 +35,18 @@ def get_registry() -> list[LibraryInstrumentor]: return list(_registry) -def record_instrumented(name: str) -> None: +def record_instrumented(name: Library) -> None: """Record a library as successfully instrumented. Called by LibraryInstrumentor.""" - _instrumented.append(name) + if name not in _instrumented: + _instrumented.append(name) -def get_instrumented_libraries() -> list[str]: - """Return the names of libraries successfully instrumented via auto_instrument(). +def get_instrumented_libraries() -> list[Library]: + """Return the libraries successfully instrumented via auto_instrument(). Each entry corresponds to a library that was installed and patched with OTel - (e.g. ``"httpx"``, ``"sqlalchemy"``). Libraries that were skipped because they - are not installed do not appear in this list. Returns an empty list if - auto_instrument() has not been called yet. + (e.g. :attr:`Library.HTTPX`, :attr:`Library.SQLALCHEMY`). Libraries that were + skipped because they are not installed do not appear in this list. Returns an + empty list if auto_instrument() has not been called yet. """ return list(_instrumented) diff --git a/src/sap_cloud_sdk/core/telemetry/instrumentation/base.py b/src/sap_cloud_sdk/core/telemetry/instrumentation/base.py index c11834e3..3e99f470 100644 --- a/src/sap_cloud_sdk/core/telemetry/instrumentation/base.py +++ b/src/sap_cloud_sdk/core/telemetry/instrumentation/base.py @@ -1,7 +1,10 @@ import importlib.util import logging from abc import ABC, abstractmethod -from typing import Any +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from sap_cloud_sdk.core.telemetry.instrumentation._registry import Library logger = logging.getLogger(__name__) @@ -18,8 +21,8 @@ class LibraryInstrumentor(ABC): subclasses to accept optional arguments (e.g. app= for framework instrumentors). """ - #: Import name of the library being instrumented (e.g. "httpx"). - library_name: str + #: Library enum member identifying the library being instrumented. + library_name: "Library" def instrument(self, **kwargs: Any) -> None: if not self._is_library_installed(): diff --git a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/aiohttp.py b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/aiohttp.py index 4d5c172e..419def07 100644 --- a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/aiohttp.py +++ b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/aiohttp.py @@ -1,7 +1,7 @@ from opentelemetry.instrumentation.aiohttp_client import AioHttpClientInstrumentor from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor -from sap_cloud_sdk.core.telemetry.instrumentation._registry import register +from sap_cloud_sdk.core.telemetry.instrumentation._registry import Library, register _instrumentor = AioHttpClientInstrumentor() @@ -9,7 +9,7 @@ class AiohttpInstrumentor(LibraryInstrumentor): """Instruments aiohttp client sessions with OTel spans and W3C header propagation.""" - library_name = "aiohttp" + library_name = Library.AIOHTTP def is_instrumented(self) -> bool: return _instrumentor.is_instrumented_by_opentelemetry diff --git a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/django.py b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/django.py index e0ce5f2e..55da6f82 100644 --- a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/django.py +++ b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/django.py @@ -1,7 +1,7 @@ from opentelemetry.instrumentation.django import DjangoInstrumentor from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor -from sap_cloud_sdk.core.telemetry.instrumentation._registry import register +from sap_cloud_sdk.core.telemetry.instrumentation._registry import Library, register _instrumentor = DjangoInstrumentor() @@ -9,7 +9,7 @@ class DjangoInstrumentorWrapper(LibraryInstrumentor): """Instruments Django with OTel spans for inbound HTTP requests and baggage extraction.""" - library_name = "django" + library_name = Library.DJANGO def is_instrumented(self) -> bool: return _instrumentor.is_instrumented_by_opentelemetry diff --git a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/fastapi.py b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/fastapi.py index 7457aa94..b236bb2b 100644 --- a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/fastapi.py +++ b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/fastapi.py @@ -1,7 +1,7 @@ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor -from sap_cloud_sdk.core.telemetry.instrumentation._registry import register +from sap_cloud_sdk.core.telemetry.instrumentation._registry import Library, register _instrumentor = FastAPIInstrumentor() @@ -13,7 +13,7 @@ class FastAPIInstrumentorWrapper(LibraryInstrumentor): instance via auto_instrument(app=app) from within a lifespan handler. """ - library_name = "fastapi" + library_name = Library.FASTAPI def is_instrumented(self) -> bool: return _instrumentor.is_instrumented_by_opentelemetry diff --git a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/flask.py b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/flask.py index 9950438f..256b55f0 100644 --- a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/flask.py +++ b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/flask.py @@ -1,7 +1,7 @@ from opentelemetry.instrumentation.flask import FlaskInstrumentor from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor -from sap_cloud_sdk.core.telemetry.instrumentation._registry import register +from sap_cloud_sdk.core.telemetry.instrumentation._registry import Library, register _instrumentor = FlaskInstrumentor() @@ -9,7 +9,7 @@ class FlaskInstrumentorWrapper(LibraryInstrumentor): """Instruments Flask with OTel spans for inbound HTTP requests and baggage extraction.""" - library_name = "flask" + library_name = Library.FLASK def is_instrumented(self) -> bool: return _instrumentor.is_instrumented_by_opentelemetry diff --git a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/grpc.py b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/grpc.py index de95929a..d6b9d413 100644 --- a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/grpc.py +++ b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/grpc.py @@ -4,7 +4,7 @@ ) from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor -from sap_cloud_sdk.core.telemetry.instrumentation._registry import register +from sap_cloud_sdk.core.telemetry.instrumentation._registry import Library, register _client_instrumentor = GrpcInstrumentorClient() _server_instrumentor = GrpcInstrumentorServer() @@ -13,7 +13,7 @@ class GrpcInstrumentorWrapper(LibraryInstrumentor): """Instruments gRPC client and server interceptors with OTel spans.""" - library_name = "grpc" + library_name = Library.GRPC def is_instrumented(self) -> bool: return ( diff --git a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/httpx.py b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/httpx.py index 3b9ac7e7..cc85df47 100644 --- a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/httpx.py +++ b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/httpx.py @@ -1,7 +1,7 @@ from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor -from sap_cloud_sdk.core.telemetry.instrumentation._registry import register +from sap_cloud_sdk.core.telemetry.instrumentation._registry import Library, register _instrumentor = HTTPXClientInstrumentor() @@ -9,7 +9,7 @@ class HttpxInstrumentor(LibraryInstrumentor): """Instruments httpx sync and async clients with OTel spans and W3C header propagation.""" - library_name = "httpx" + library_name = Library.HTTPX def is_instrumented(self) -> bool: return _instrumentor.is_instrumented_by_opentelemetry diff --git a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/logging.py b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/logging.py index 97d11827..cc0aff81 100644 --- a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/logging.py +++ b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/logging.py @@ -6,7 +6,7 @@ ) from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor -from sap_cloud_sdk.core.telemetry.instrumentation._registry import register +from sap_cloud_sdk.core.telemetry.instrumentation._registry import Library, register _instrumentor = LoggingInstrumentor() @@ -27,7 +27,7 @@ def _has_otel_handler_on_root() -> bool: class LoggingInstrumentorWrapper(LibraryInstrumentor): """Injects trace_id and span_id into every stdlib log record for log-trace correlation.""" - library_name = "logging" + library_name = Library.LOGGING def is_instrumented(self) -> bool: return _instrumentor.is_instrumented_by_opentelemetry diff --git a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/requests.py b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/requests.py index 72d8198d..aafea74a 100644 --- a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/requests.py +++ b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/requests.py @@ -1,7 +1,7 @@ from opentelemetry.instrumentation.requests import RequestsInstrumentor from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor -from sap_cloud_sdk.core.telemetry.instrumentation._registry import register +from sap_cloud_sdk.core.telemetry.instrumentation._registry import Library, register _instrumentor = RequestsInstrumentor() @@ -9,7 +9,7 @@ class RequestsInstrumentorWrapper(LibraryInstrumentor): """Instruments the requests library with OTel spans and W3C header propagation.""" - library_name = "requests" + library_name = Library.REQUESTS def is_instrumented(self) -> bool: return _instrumentor.is_instrumented_by_opentelemetry diff --git a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/sqlalchemy.py b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/sqlalchemy.py index cbc09aab..4ee02af5 100644 --- a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/sqlalchemy.py +++ b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/sqlalchemy.py @@ -1,7 +1,7 @@ from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor -from sap_cloud_sdk.core.telemetry.instrumentation._registry import register +from sap_cloud_sdk.core.telemetry.instrumentation._registry import Library, register _instrumentor = SQLAlchemyInstrumentor() @@ -9,7 +9,7 @@ class SQLAlchemyInstrumentorWrapper(LibraryInstrumentor): """Instruments SQLAlchemy with OTel spans for database queries.""" - library_name = "sqlalchemy" + library_name = Library.SQLALCHEMY def is_instrumented(self) -> bool: return _instrumentor.is_instrumented_by_opentelemetry diff --git a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/starlette.py b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/starlette.py index 9e8378fc..15bfcd93 100644 --- a/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/starlette.py +++ b/src/sap_cloud_sdk/core/telemetry/instrumentation/instrumentors/starlette.py @@ -1,7 +1,7 @@ from opentelemetry.instrumentation.starlette import StarletteInstrumentor from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor -from sap_cloud_sdk.core.telemetry.instrumentation._registry import register +from sap_cloud_sdk.core.telemetry.instrumentation._registry import Library, register _instrumentor = StarletteInstrumentor() @@ -13,7 +13,7 @@ class StarletteInstrumentorWrapper(LibraryInstrumentor): instance via auto_instrument(app=app) from within a lifespan handler. """ - library_name = "starlette" + library_name = Library.STARLETTE def is_instrumented(self) -> bool: return _instrumentor.is_instrumented_by_opentelemetry diff --git a/src/sap_cloud_sdk/core/telemetry/user-guide.md b/src/sap_cloud_sdk/core/telemetry/user-guide.md index 4efaf235..27c1fde8 100644 --- a/src/sap_cloud_sdk/core/telemetry/user-guide.md +++ b/src/sap_cloud_sdk/core/telemetry/user-guide.md @@ -83,9 +83,9 @@ The SDK ships `opentelemetry-instrumentation-*` packages for all of the above as Use `get_instrumented_libraries()` to query which libraries were actually patched at runtime: ```python -from sap_cloud_sdk import get_instrumented_libraries +from sap_cloud_sdk import Library, get_instrumented_libraries -get_instrumented_libraries() # -> ["httpx", "sqlalchemy", ...] after auto_instrument(), [] before +get_instrumented_libraries() # -> [Library.HTTPX, Library.SQLALCHEMY, ...] after auto_instrument(), [] before ``` Only libraries that were installed **and** successfully instrumented appear in the list. Libraries skipped because they are not installed do not appear. Returns an empty list if `auto_instrument()` has not been called yet. diff --git a/tests/core/unit/runtime_context/test_runtime_context.py b/tests/core/unit/runtime_context/test_runtime_context.py index 9b1e32b7..b21bcabd 100644 --- a/tests/core/unit/runtime_context/test_runtime_context.py +++ b/tests/core/unit/runtime_context/test_runtime_context.py @@ -26,6 +26,7 @@ set_context, ) from sap_cloud_sdk.core.runtime_context._registry import ( + Adapter, get_framework_adapters, record_attached, ) @@ -458,16 +459,16 @@ def test_empty_before_bootstrap(self): assert get_framework_adapters() == [] def test_records_name_after_record_attached(self): - record_attached("starlette") - assert get_framework_adapters() == ["starlette"] + record_attached(Adapter.STARLETTE) + assert get_framework_adapters() == [Adapter.STARLETTE] def test_multiple_calls_accumulate(self): - record_attached("starlette") - record_attached("flask") - assert get_framework_adapters() == ["starlette", "flask"] + record_attached(Adapter.STARLETTE) + record_attached(Adapter.STARLETTE) # idempotent + assert get_framework_adapters() == [Adapter.STARLETTE] def test_returns_copy(self): - record_attached("starlette") + record_attached(Adapter.STARLETTE) snapshot = get_framework_adapters() snapshot.clear() - assert get_framework_adapters() == ["starlette"] + assert get_framework_adapters() == [Adapter.STARLETTE] diff --git a/tests/core/unit/telemetry/instrumentation/test_instrumentation.py b/tests/core/unit/telemetry/instrumentation/test_instrumentation.py index dc33224d..ca297d75 100644 --- a/tests/core/unit/telemetry/instrumentation/test_instrumentation.py +++ b/tests/core/unit/telemetry/instrumentation/test_instrumentation.py @@ -5,6 +5,7 @@ from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor from sap_cloud_sdk.core.telemetry.instrumentation._registry import ( + Library, _registry, get_registry, get_instrumented_libraries, @@ -297,7 +298,7 @@ def test_idempotent_instrument_records_only_once(self): assert get_instrumented_libraries().count("sys") == 1 def test_returns_copy(self): - record_instrumented("httpx") + record_instrumented(Library.HTTPX) snapshot = get_instrumented_libraries() snapshot.clear() - assert "httpx" in get_instrumented_libraries() + assert Library.HTTPX in get_instrumented_libraries() From 105283724cfa05fc423bf3d6e3e1de1ac5452b21 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Mon, 31 Aug 2026 17:01:58 -0300 Subject: [PATCH 08/10] User guide --- src/sap_cloud_sdk/core/runtime_context/user-guide.md | 3 ++- .../unit/telemetry/instrumentation/test_instrumentation.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sap_cloud_sdk/core/runtime_context/user-guide.md b/src/sap_cloud_sdk/core/runtime_context/user-guide.md index 7dec3909..4ec562cb 100644 --- a/src/sap_cloud_sdk/core/runtime_context/user-guide.md +++ b/src/sap_cloud_sdk/core/runtime_context/user-guide.md @@ -202,6 +202,7 @@ editing `bootstrap`. ### Adding a new framework or invocation source ```python +from sap_cloud_sdk import Adapter from sap_cloud_sdk.core.runtime_context import ( ContextProvider, FrameworkAdapter, @@ -211,7 +212,7 @@ from sap_cloud_sdk.core.runtime_context import ( class FlaskContextAdapter(FrameworkAdapter): @property - def name(self) -> str: + def name(self) -> Adapter: return "flask" def _matches(self, app) -> bool: diff --git a/tests/core/unit/telemetry/instrumentation/test_instrumentation.py b/tests/core/unit/telemetry/instrumentation/test_instrumentation.py index ca297d75..ff3d61a3 100644 --- a/tests/core/unit/telemetry/instrumentation/test_instrumentation.py +++ b/tests/core/unit/telemetry/instrumentation/test_instrumentation.py @@ -19,7 +19,7 @@ # --------------------------------------------------------------------------- class _ConcreteInstrumentor(LibraryInstrumentor): - library_name = "sys" # always installed + library_name = Library.HTTPX # always installed (hard SDK dependency) def __init__(self): self._instrumented = False @@ -284,7 +284,7 @@ def test_empty_before_any_instrumentation(self): def test_records_library_after_successful_instrument(self): inst = _ConcreteInstrumentor() inst.instrument() - assert "sys" in get_instrumented_libraries() + assert Library.HTTPX in get_instrumented_libraries() def test_skipped_library_not_recorded(self): inst = _MissingLibraryInstrumentor() @@ -295,7 +295,7 @@ def test_idempotent_instrument_records_only_once(self): inst = _ConcreteInstrumentor() inst.instrument() inst.instrument() # is_instrumented() returns True, so _instrument() is skipped - assert get_instrumented_libraries().count("sys") == 1 + assert get_instrumented_libraries().count(Library.HTTPX) == 1 def test_returns_copy(self): record_instrumented(Library.HTTPX) From 109ae858e8972058d81d6d429c5e6072cbd59be8 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Mon, 31 Aug 2026 17:53:48 -0300 Subject: [PATCH 09/10] Linting --- .../core/runtime_context/adapters/_starlette.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sap_cloud_sdk/core/runtime_context/adapters/_starlette.py b/src/sap_cloud_sdk/core/runtime_context/adapters/_starlette.py index 38c94517..d85648e5 100644 --- a/src/sap_cloud_sdk/core/runtime_context/adapters/_starlette.py +++ b/src/sap_cloud_sdk/core/runtime_context/adapters/_starlette.py @@ -3,7 +3,11 @@ from typing import List from sap_cloud_sdk.core.runtime_context._protocol import ContextProvider -from sap_cloud_sdk.core.runtime_context._registry import Adapter, FrameworkAdapter, register +from sap_cloud_sdk.core.runtime_context._registry import ( + Adapter, + FrameworkAdapter, + register, +) class _StarletteContextAdapter(FrameworkAdapter): From 2745f6e720cf6c9fa4ff53d4565291908024404a Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Mon, 31 Aug 2026 18:18:47 -0300 Subject: [PATCH 10/10] Moved imports to lower level --- src/sap_cloud_sdk/__init__.py | 9 --------- src/sap_cloud_sdk/core/runtime_context/__init__.py | 9 ++++++++- .../core/runtime_context/_registry.py | 2 +- .../core/runtime_context/user-guide.md | 10 +++++----- src/sap_cloud_sdk/core/telemetry/__init__.py | 6 ++++++ src/sap_cloud_sdk/core/telemetry/user-guide.md | 2 +- .../unit/runtime_context/test_runtime_context.py | 14 +++++++------- 7 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/sap_cloud_sdk/__init__.py b/src/sap_cloud_sdk/__init__.py index de2283a2..ab09f6d4 100644 --- a/src/sap_cloud_sdk/__init__.py +++ b/src/sap_cloud_sdk/__init__.py @@ -1,17 +1,8 @@ # SAP Cloud SDK for Python from sap_cloud_sdk.core.bootstrap import bootstrap, TelemetryConfig -from sap_cloud_sdk.core.runtime_context._registry import Adapter, get_framework_adapters -from sap_cloud_sdk.core.telemetry.instrumentation._registry import ( - Library, - get_instrumented_libraries, -) __all__ = [ - "Adapter", "bootstrap", - "get_framework_adapters", - "get_instrumented_libraries", - "Library", "TelemetryConfig", ] diff --git a/src/sap_cloud_sdk/core/runtime_context/__init__.py b/src/sap_cloud_sdk/core/runtime_context/__init__.py index 16d8ba29..520a237b 100644 --- a/src/sap_cloud_sdk/core/runtime_context/__init__.py +++ b/src/sap_cloud_sdk/core/runtime_context/__init__.py @@ -32,7 +32,12 @@ TRIGGER_TYPE, ) from sap_cloud_sdk.core.runtime_context._protocol import ContextProvider -from sap_cloud_sdk.core.runtime_context._registry import FrameworkAdapter, register +from sap_cloud_sdk.core.runtime_context._registry import ( + Adapter, + FrameworkAdapter, + get_attached_adapters, + register, +) from sap_cloud_sdk.core.runtime_context.providers import ( DWCContextProvider, IASContextProvider, @@ -46,6 +51,7 @@ import sap_cloud_sdk.core.runtime_context.adapters # noqa: F401 __all__ = [ + "Adapter", "APP_TENANT_ID", "ContextKey", "ContextProvider", @@ -54,6 +60,7 @@ "DWCContextProvider", "FEATURE_TOGGLES", "FrameworkAdapter", + "get_attached_adapters", "GLOBAL_TENANT_ID", "IASContextProvider", "RuntimeContext", diff --git a/src/sap_cloud_sdk/core/runtime_context/_registry.py b/src/sap_cloud_sdk/core/runtime_context/_registry.py index 2752c6f5..6b3c83e1 100644 --- a/src/sap_cloud_sdk/core/runtime_context/_registry.py +++ b/src/sap_cloud_sdk/core/runtime_context/_registry.py @@ -37,7 +37,7 @@ def record_attached(name: Adapter) -> None: _attached.append(name) -def get_framework_adapters() -> List[Adapter]: +def get_attached_adapters() -> List[Adapter]: """Return the adapters attached via bootstrap(). Each entry corresponds to one :func:`~sap_cloud_sdk.bootstrap` call that diff --git a/src/sap_cloud_sdk/core/runtime_context/user-guide.md b/src/sap_cloud_sdk/core/runtime_context/user-guide.md index 4ec562cb..de1b41fd 100644 --- a/src/sap_cloud_sdk/core/runtime_context/user-guide.md +++ b/src/sap_cloud_sdk/core/runtime_context/user-guide.md @@ -202,8 +202,8 @@ editing `bootstrap`. ### Adding a new framework or invocation source ```python -from sap_cloud_sdk import Adapter from sap_cloud_sdk.core.runtime_context import ( + Adapter, ContextProvider, FrameworkAdapter, register, @@ -233,18 +233,18 @@ register(FlaskContextAdapter()) ## Introspection -Use `get_framework_adapters()` to check which framework adapters have been attached at runtime: +Use `get_attached_adapters()` to check which framework adapters have been attached at runtime: ```python -from sap_cloud_sdk import Adapter, get_framework_adapters +from sap_cloud_sdk.core.runtime_context import Adapter, get_attached_adapters -get_framework_adapters() # -> [Adapter.STARLETTE] after bootstrap(app), [] before +get_attached_adapters() # -> [Adapter.STARLETTE] after bootstrap(app), [] before ``` This is useful for modules that need to fail fast if their required framework was never bootstrapped: ```python -if Adapter.STARLETTE not in get_framework_adapters(): +if Adapter.STARLETTE not in get_attached_adapters(): raise RuntimeError( "This client requires Starlette to be bootstrapped. " "Call bootstrap(app) with your Starlette/FastAPI app." diff --git a/src/sap_cloud_sdk/core/telemetry/__init__.py b/src/sap_cloud_sdk/core/telemetry/__init__.py index 1febaddd..37c28f02 100644 --- a/src/sap_cloud_sdk/core/telemetry/__init__.py +++ b/src/sap_cloud_sdk/core/telemetry/__init__.py @@ -56,6 +56,10 @@ ExtensionContextLogFilter, ) from sap_cloud_sdk.core.telemetry.middleware import TelemetryMiddleware +from sap_cloud_sdk.core.telemetry.instrumentation._registry import ( + Library, + get_instrumented_libraries, +) __all__ = [ "Module", @@ -103,6 +107,8 @@ "emit_extensions_summary_span", "ExtensionContextLogFilter", "TelemetryMiddleware", + "Library", + "get_instrumented_libraries", ] try: diff --git a/src/sap_cloud_sdk/core/telemetry/user-guide.md b/src/sap_cloud_sdk/core/telemetry/user-guide.md index 27c1fde8..5b9fbeed 100644 --- a/src/sap_cloud_sdk/core/telemetry/user-guide.md +++ b/src/sap_cloud_sdk/core/telemetry/user-guide.md @@ -83,7 +83,7 @@ The SDK ships `opentelemetry-instrumentation-*` packages for all of the above as Use `get_instrumented_libraries()` to query which libraries were actually patched at runtime: ```python -from sap_cloud_sdk import Library, get_instrumented_libraries +from sap_cloud_sdk.core.telemetry import Library, get_instrumented_libraries get_instrumented_libraries() # -> [Library.HTTPX, Library.SQLALCHEMY, ...] after auto_instrument(), [] before ``` diff --git a/tests/core/unit/runtime_context/test_runtime_context.py b/tests/core/unit/runtime_context/test_runtime_context.py index b21bcabd..bbb40bc5 100644 --- a/tests/core/unit/runtime_context/test_runtime_context.py +++ b/tests/core/unit/runtime_context/test_runtime_context.py @@ -27,7 +27,7 @@ ) from sap_cloud_sdk.core.runtime_context._registry import ( Adapter, - get_framework_adapters, + get_attached_adapters, record_attached, ) from sap_cloud_sdk.core.runtime_context.providers._ias import ( @@ -440,7 +440,7 @@ def test_single_context_passthrough(self): # --------------------------------------------------------------------------- -# get_framework_adapters +# get_attached_adapters # --------------------------------------------------------------------------- @@ -456,19 +456,19 @@ def teardown_method(self): registry_mod._attached.extend(self._original) def test_empty_before_bootstrap(self): - assert get_framework_adapters() == [] + assert get_attached_adapters() == [] def test_records_name_after_record_attached(self): record_attached(Adapter.STARLETTE) - assert get_framework_adapters() == [Adapter.STARLETTE] + assert get_attached_adapters() == [Adapter.STARLETTE] def test_multiple_calls_accumulate(self): record_attached(Adapter.STARLETTE) record_attached(Adapter.STARLETTE) # idempotent - assert get_framework_adapters() == [Adapter.STARLETTE] + assert get_attached_adapters() == [Adapter.STARLETTE] def test_returns_copy(self): record_attached(Adapter.STARLETTE) - snapshot = get_framework_adapters() + snapshot = get_attached_adapters() snapshot.clear() - assert get_framework_adapters() == [Adapter.STARLETTE] + assert get_attached_adapters() == [Adapter.STARLETTE]