Skip to content

Commit ec8c38a

Browse files
Deprecation
1 parent 4e47ffe commit ec8c38a

11 files changed

Lines changed: 225 additions & 230 deletions

File tree

src/sap_cloud_sdk/core/telemetry/auto_instrument.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import os
3+
import warnings
34
from collections.abc import Mapping
45

56
from sap_cloud_sdk.core.telemetry.middleware.base import TelemetryMiddleware
@@ -43,7 +44,10 @@ def auto_instrument(
4344
middlewares: list[TelemetryMiddleware] | None = None,
4445
):
4546
"""
46-
Initialize meta-instrumentation for GenAI tracing. Should be initialized before any AI frameworks.
47+
Initialize meta-instrumentation for GenAI tracing.
48+
49+
Should be called before importing your Starlette/FastAPI app so that
50+
IAS JWT telemetry middleware is registered automatically.
4751
4852
Traces are exported to the OTEL collector endpoint configured in environment with
4953
OTEL_EXPORTER_OTLP_ENDPOINT, or printed to console when OTEL_TRACES_EXPORTER=console.
@@ -52,13 +56,19 @@ def auto_instrument(
5256
disable_batch: If True, uses SimpleSpanProcessor (synchronous, lower throughput).
5357
Defaults to False, which uses BatchSpanProcessor (asynchronous,
5458
recommended for production workloads).
55-
middlewares: Optional list of TelemetryMiddleware instances. When provided,
56-
each middleware is registered with its application and a
57-
MiddlewareSpanProcessor is added so that headers extracted by
58-
the middlewares appear as attributes on every span.
59-
Must be called before the ASGI application begins serving
60-
requests so that register() runs before the first request.
59+
middlewares: Deprecated. Pass explicit TelemetryMiddleware instances.
60+
Use auto_instrument() without this parameter instead — IAS middleware
61+
is now registered automatically for supported frameworks.
62+
Will be removed in the next major version.
6163
"""
64+
if middlewares is not None:
65+
warnings.warn(
66+
"The middlewares= parameter of auto_instrument() is deprecated and will be "
67+
"removed in the next major version. Call auto_instrument() without it — "
68+
"IAS middleware is now registered automatically for supported frameworks.",
69+
DeprecationWarning,
70+
stacklevel=2,
71+
)
6272
otel_endpoint = os.getenv(ENV_OTLP_ENDPOINT, "")
6373
console_traces = os.getenv(ENV_TRACES_EXPORTER, "").lower() == "console"
6474

@@ -161,7 +171,7 @@ def _register_middleware_processors(middlewares: list[TelemetryMiddleware]) -> N
161171
def _auto_instrument_frameworks(
162172
middlewares: list[TelemetryMiddleware] | None = None,
163173
) -> None:
164-
from sap_cloud_sdk.core.telemetry.middleware.registry import get_available
174+
from sap_cloud_sdk.core.telemetry.middleware._registry import _get_available
165175
from sap_cloud_sdk.core.telemetry.middleware.span_processor import (
166176
MiddlewareSpanProcessor,
167177
)
@@ -173,29 +183,28 @@ def _auto_instrument_frameworks(
173183
)
174184
return
175185

176-
manual_keys = {m.framework_key for m in (middlewares or []) if m.framework_key}
186+
manual_types = {type(m) for m in (middlewares or [])}
177187

178188
instrumentors = [
179-
i for i in get_available()
189+
i for i in _get_available()
180190
if not i.__class__._processor_registered
181-
and i.framework_key not in manual_keys
191+
and not (i.__class__.supersedes and i.__class__.supersedes in manual_types)
182192
]
183193

184-
if not instrumentors:
185-
return
186-
187194
skipped = [
188-
i for i in get_available()
189-
if i.framework_key in manual_keys
195+
i for i in _get_available()
196+
if i.__class__.supersedes and i.__class__.supersedes in manual_types
190197
]
191198
for i in skipped:
192199
logger.warning(
193-
"%s skipped: framework '%s' is already covered by an explicit middlewares= entry. "
194-
"Remove the explicit middleware or do not pass it to avoid duplicate IAS span attributes.",
200+
"%s skipped: an explicit middlewares= entry already covers this framework. "
201+
"Remove the explicit middleware to avoid duplicate IAS span attributes.",
195202
type(i).__name__,
196-
i.framework_key,
197203
)
198204

205+
if not instrumentors:
206+
return
207+
199208
for instr in instrumentors:
200209
instr.instrument()
201210

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
"""HTTP framework middleware integration for Cloud SDK telemetry.
22
3-
Provides middleware adapters that extract request-scoped attributes
4-
(e.g. A2A session/task headers) and surface them as OpenTelemetry span
5-
attributes via auto_instrument().
3+
The explicit middleware path (``TelemetryMiddleware``, ``StarletteIASTelemetryMiddleware``,
4+
and the ``middlewares=`` parameter of ``auto_instrument()``) is deprecated and will be
5+
removed in the next major version. Call ``auto_instrument()`` before creating your app
6+
instead — IAS middleware is registered automatically.
67
"""
78

8-
from sap_cloud_sdk.core.telemetry.middleware.base import (
9-
FrameworkInstrumentor,
10-
TelemetryMiddleware,
11-
)
12-
from sap_cloud_sdk.core.telemetry.middleware.registry import register
9+
from sap_cloud_sdk.core.telemetry.middleware.base import TelemetryMiddleware
1310

1411
__all__ = [
15-
"FrameworkInstrumentor",
1612
"TelemetryMiddleware",
17-
"register",
1813
]
1914

2015
try:
@@ -26,7 +21,8 @@
2621
except ImportError:
2722
pass
2823

24+
# Trigger @_register side-effect for the internal Starlette instrumentor.
2925
try:
30-
import sap_cloud_sdk.core.telemetry.middleware.starlette_instrumentor # noqa: F401 — triggers @register
26+
import sap_cloud_sdk.core.telemetry.middleware._starlette_instrumentor # noqa: F401
3127
except ImportError:
3228
pass
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Internal base class for framework auto-instrumentation adapters."""
2+
3+
import logging
4+
from abc import ABC, abstractmethod
5+
from typing import Any, Dict, TYPE_CHECKING
6+
7+
if TYPE_CHECKING:
8+
from sap_cloud_sdk.core.telemetry.middleware.base import TelemetryMiddleware
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class FrameworkInstrumentor(ABC):
14+
"""Auto-instrumentation adapter for a specific web framework.
15+
16+
Internal to the SDK — not part of the public API.
17+
18+
Subclasses patch the framework at the class level so that any application
19+
created after ``instrument()`` is called automatically gets IAS telemetry
20+
middleware — no ``app=`` reference required.
21+
22+
The idempotency guard lives here so subclasses never need to implement it.
23+
Subclasses implement ``_do_instrument`` / ``_do_uninstrument`` only.
24+
25+
To add support for a new framework, create a new module under
26+
``sap_cloud_sdk.core.telemetry.middleware`` (prefixed with ``_``) and
27+
decorate the class with ``@_register``::
28+
29+
from sap_cloud_sdk.core.telemetry.middleware._registry import _register
30+
from sap_cloud_sdk.core.telemetry.middleware._framework_instrumentor import FrameworkInstrumentor
31+
32+
@_register
33+
class _DjangoIASInstrumentor(FrameworkInstrumentor):
34+
@classmethod
35+
def is_available(cls) -> bool:
36+
try:
37+
import django # noqa: F401
38+
return True
39+
except ImportError:
40+
return False
41+
42+
def _do_instrument(self) -> None: ...
43+
def _do_uninstrument(self) -> None: ...
44+
def get_attributes(self) -> Dict[str, Any]: ...
45+
"""
46+
47+
_instrumented: bool = False
48+
_processor_registered: bool = False
49+
supersedes: "type[TelemetryMiddleware] | None" = None
50+
51+
def instrument(self) -> None:
52+
if self.__class__._instrumented:
53+
logger.debug("%s already instrumented, skipping", type(self).__name__)
54+
return
55+
self._do_instrument()
56+
self.__class__._instrumented = True
57+
logger.info("Instrumented %s", type(self).__name__)
58+
59+
def uninstrument(self) -> None:
60+
if not self.__class__._instrumented:
61+
return
62+
self._do_uninstrument()
63+
self.__class__._instrumented = False
64+
self.__class__._processor_registered = False
65+
logger.debug("Uninstrumented %s", type(self).__name__)
66+
67+
@classmethod
68+
def is_available(cls) -> bool:
69+
"""Return True when the target framework is importable."""
70+
return False
71+
72+
@abstractmethod
73+
def _do_instrument(self) -> None: ...
74+
75+
@abstractmethod
76+
def _do_uninstrument(self) -> None: ...
77+
78+
@abstractmethod
79+
def get_attributes(self) -> Dict[str, Any]: ...

src/sap_cloud_sdk/core/telemetry/middleware/registry.py renamed to src/sap_cloud_sdk/core/telemetry/middleware/_registry.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,32 @@
1-
"""Registry for FrameworkInstrumentor subclasses.
2-
3-
Instrumentors self-register via the ``@register`` decorator. ``auto_instrument()``
4-
calls ``get_available()`` to discover and activate all installed frameworks without
5-
needing to know about any specific framework.
6-
"""
1+
"""Internal registry for FrameworkInstrumentor subclasses."""
72

83
from __future__ import annotations
94

105
import logging
116
from typing import TYPE_CHECKING
127

138
if TYPE_CHECKING:
14-
from sap_cloud_sdk.core.telemetry.middleware.base import FrameworkInstrumentor
9+
from sap_cloud_sdk.core.telemetry.middleware._framework_instrumentor import FrameworkInstrumentor
1510

1611
logger = logging.getLogger(__name__)
1712

1813
_registry: list[type[FrameworkInstrumentor]] = []
1914

2015

21-
def register(cls: type[FrameworkInstrumentor]) -> type[FrameworkInstrumentor]:
16+
def _register(cls: type[FrameworkInstrumentor]) -> type[FrameworkInstrumentor]:
2217
"""Register a FrameworkInstrumentor subclass for auto-discovery.
2318
24-
Use as a class decorator::
19+
Internal decorator — not part of the public API::
2520
26-
@register
27-
class StarletteIASInstrumentor(FrameworkInstrumentor):
21+
@_register
22+
class _StarletteIASInstrumentor(FrameworkInstrumentor):
2823
...
2924
"""
3025
_registry.append(cls)
3126
return cls
3227

3328

34-
def get_available() -> list[FrameworkInstrumentor]:
29+
def _get_available() -> list[FrameworkInstrumentor]:
3530
"""Return one instance of each registered instrumentor whose framework is installed."""
3631
available = []
3732
for cls in _registry:

src/sap_cloud_sdk/core/telemetry/middleware/starlette_instrumentor.py renamed to src/sap_cloud_sdk/core/telemetry/middleware/_starlette_instrumentor.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Zero-config IAS telemetry instrumentation for Starlette and FastAPI.
1+
"""Internal: zero-config IAS telemetry instrumentation for Starlette.
22
33
Patches ``starlette.applications.Starlette`` via class substitution so that
44
any app created after ``auto_instrument()`` automatically gets the IAS JWT
@@ -13,24 +13,21 @@
1313
from contextvars import ContextVar
1414
from typing import Any, Dict
1515

16-
from sap_cloud_sdk.core.telemetry.middleware.base import FrameworkInstrumentor
17-
from sap_cloud_sdk.core.telemetry.middleware.registry import register
16+
from sap_cloud_sdk.core.telemetry.middleware._framework_instrumentor import FrameworkInstrumentor
17+
from sap_cloud_sdk.core.telemetry.middleware._registry import _register
18+
from sap_cloud_sdk.core.telemetry.middleware.starlette_a2a import (
19+
StarletteIASTelemetryMiddleware,
20+
)
1821

1922
logger = logging.getLogger(__name__)
2023

2124
_attrs_var: ContextVar[Dict[str, Any]] = ContextVar("_sap_ias_attrs", default={})
2225

2326

24-
@register
25-
class StarletteIASInstrumentor(FrameworkInstrumentor):
26-
"""Instruments Starlette and FastAPI with IAS JWT telemetry middleware.
27-
28-
FastAPI subclasses Starlette and calls ``super().__init__()``, so patching
29-
``starlette.applications.Starlette`` covers both frameworks with one patch.
30-
"""
31-
27+
@_register
28+
class _StarletteIASInstrumentor(FrameworkInstrumentor):
3229
_original: Any = None
33-
framework_key = "starlette"
30+
supersedes = StarletteIASTelemetryMiddleware
3431

3532
@classmethod
3633
def is_available(cls) -> bool:
@@ -53,15 +50,15 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
5350
self._sap_ias_done = True
5451
self.add_middleware(_IASMiddleware, attrs_var=_attrs_var)
5552

56-
StarletteIASInstrumentor._original = original
53+
_StarletteIASInstrumentor._original = original
5754
applications.Starlette = _SAPInstrumented
5855

5956
def _do_uninstrument(self) -> None:
60-
if StarletteIASInstrumentor._original is None:
57+
if _StarletteIASInstrumentor._original is None:
6158
return
6259
from starlette import applications
63-
applications.Starlette = StarletteIASInstrumentor._original
64-
StarletteIASInstrumentor._original = None
60+
applications.Starlette = _StarletteIASInstrumentor._original
61+
_StarletteIASInstrumentor._original = None
6562

6663
def get_attributes(self) -> Dict[str, Any]:
6764
return _attrs_var.get()

0 commit comments

Comments
 (0)