Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "sap-cloud-sdk"
version = "0.37.0"
version = "0.38.0"
description = "SAP Cloud SDK for Python"
readme = "README.md"
license = "Apache-2.0"
Expand All @@ -26,13 +26,29 @@ dependencies = [
"grpcio>=1.60.0",
"opentelemetry-api>=1.42.1",
"opentelemetry-sdk>=1.42.1",
"opentelemetry-instrumentation-httpx~=0.63b1",
"opentelemetry-instrumentation-requests~=0.63b1",
"opentelemetry-instrumentation-grpc~=0.63b1",
"opentelemetry-instrumentation-logging~=0.63b1",
"opentelemetry-instrumentation-starlette~=0.63b1",
"opentelemetry-instrumentation-fastapi~=0.63b1",
"opentelemetry-instrumentation-aiohttp-client~=0.63b1",
"opentelemetry-instrumentation-sqlalchemy~=0.63b1",
"opentelemetry-instrumentation-redis~=0.63b1",
"opentelemetry-instrumentation-django~=0.63b1",
"opentelemetry-instrumentation-flask~=0.63b1",
"mcp>=1.1.0",
]

[project.optional-dependencies]
extensibility = ["a2a-sdk>=0.2.0"]
starlette = ["starlette>=0.40.0"]
langchain = ["langchain-core>=1.2.7"]
fastapi = ["fastapi>=0.100.0"]
aiohttp = ["aiohttp>=3.9.0"]
sqlalchemy = ["sqlalchemy>=2.0.0"]
redis = ["redis>=5.0.0"]
django = ["django>=4.0"]
flask = ["flask>=3.0"]
langgraph = ["langgraph>=1.0.0"]

[build-system]
Expand All @@ -55,6 +71,12 @@ dev = [
"starlette>=0.40.0",
"anyio>=3.6.2",
"httpx>=0.27.0",
"fastapi>=0.100.0",
"aiohttp>=3.9.0",
"sqlalchemy>=2.0.0",
"redis>=5.0.0",
"django>=4.0",
"flask>=3.0",
"a2a-sdk>=0.2.0",
"langchain-core>=1.2.7",
"langgraph>=0.2.0",
Expand Down
8 changes: 8 additions & 0 deletions src/sap_cloud_sdk/core/telemetry/auto_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from sap_cloud_sdk.core.telemetry.span_processors.propagated_attributes_processor import (
PropagatedAttributesSpanProcessor,
)
from sap_cloud_sdk.core.telemetry.instrumentation import get_registry

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -90,6 +91,8 @@ def auto_instrument(
if middlewares:
_register_middleware_processors(middlewares)

_instrument_libraries()

logger.info("Cloud auto instrumentation initialized successfully")


Expand Down Expand Up @@ -159,6 +162,11 @@ def _register_middleware_processors(middlewares: list[TelemetryMiddleware]) -> N
)


def _instrument_libraries() -> None:
for instrumentor in get_registry():
instrumentor.instrument()


def _merge_resource_attrs_into_active_provider_if_wrapper_installed(
sap_attrs: dict,
) -> None:
Expand Down
55 changes: 55 additions & 0 deletions src/sap_cloud_sdk/core/telemetry/instrumentation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor
from sap_cloud_sdk.core.telemetry.instrumentation._registry import (
register,
get_registry,
)

# Import concrete instrumentors to trigger their register() calls.
from sap_cloud_sdk.core.telemetry.instrumentation.instrumentors import ( # noqa: F401
httpx,
requests,
grpc,
logging,
)

# Optional — guarded so missing extras don't break the import.
try:
from sap_cloud_sdk.core.telemetry.instrumentation.instrumentors import starlette # noqa: F401
except ImportError:
pass

try:
from sap_cloud_sdk.core.telemetry.instrumentation.instrumentors import fastapi # noqa: F401
except ImportError:
pass

try:
from sap_cloud_sdk.core.telemetry.instrumentation.instrumentors import aiohttp # noqa: F401
except ImportError:
pass

try:
from sap_cloud_sdk.core.telemetry.instrumentation.instrumentors import sqlalchemy # noqa: F401
except ImportError:
pass

try:
from sap_cloud_sdk.core.telemetry.instrumentation.instrumentors import redis # noqa: F401
except ImportError:
pass

try:
from sap_cloud_sdk.core.telemetry.instrumentation.instrumentors import django # noqa: F401
except ImportError:
pass

try:
from sap_cloud_sdk.core.telemetry.instrumentation.instrumentors import flask # noqa: F401
except ImportError:
pass

__all__ = [
"LibraryInstrumentor",
"register",
"get_registry",
]
16 changes: 16 additions & 0 deletions src/sap_cloud_sdk/core/telemetry/instrumentation/_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor

_registry: list[LibraryInstrumentor] = []


def register(instrumentor: LibraryInstrumentor) -> None:
"""Add an instrumentor to the registry.

Call this at module level in each concrete instrumentor file, or from
third-party code that wants to plug in additional library coverage.
"""
_registry.append(instrumentor)


def get_registry() -> list[LibraryInstrumentor]:
return list(_registry)
48 changes: 48 additions & 0 deletions src/sap_cloud_sdk/core/telemetry/instrumentation/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import importlib.util
import logging
from abc import ABC, abstractmethod

logger = logging.getLogger(__name__)


class LibraryInstrumentor(ABC):
"""Base class for optional library instrumentors.

Subclasses wrap a single opentelemetry-instrumentation-* package.
The target library (e.g. httpx) is checked at runtime via find_spec so
that missing optional dependencies are silently skipped rather than
raising ImportError.
"""

#: Import name of the library being instrumented (e.g. "httpx").
library_name: str

def instrument(self) -> None:
if not self._is_library_installed():
logger.debug(
"%s not installed, skipping instrumentation", self.library_name
)
return
if self.is_instrumented():
logger.debug("%s already instrumented", self.library_name)
return
self._instrument()
logger.debug("Instrumented %s", self.library_name)

def uninstrument(self) -> None:
if not self.is_instrumented():
return
self._uninstrument()
logger.debug("Uninstrumented %s", self.library_name)

@abstractmethod
def is_instrumented(self) -> bool: ...

@abstractmethod
def _instrument(self) -> None: ...

@abstractmethod
def _uninstrument(self) -> None: ...

def _is_library_installed(self) -> bool:
return importlib.util.find_spec(self.library_name) is not None
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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

_instrumentor = AioHttpClientInstrumentor()


class AiohttpInstrumentor(LibraryInstrumentor):
library_name = "aiohttp"

def is_instrumented(self) -> bool:
return _instrumentor.is_instrumented_by_opentelemetry

def _instrument(self) -> None:
_instrumentor.instrument()

def _uninstrument(self) -> None:
_instrumentor.uninstrument()


register(AiohttpInstrumentor())
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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

_instrumentor = DjangoInstrumentor()


class DjangoInstrumentorWrapper(LibraryInstrumentor):
library_name = "django"

def is_instrumented(self) -> bool:
return _instrumentor.is_instrumented_by_opentelemetry

def _instrument(self) -> None:
_instrumentor.instrument()

def _uninstrument(self) -> None:
_instrumentor.uninstrument()


register(DjangoInstrumentorWrapper())
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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

_instrumentor = FastAPIInstrumentor()


class FastAPIInstrumentorWrapper(LibraryInstrumentor):
library_name = "fastapi"

def is_instrumented(self) -> bool:
return _instrumentor.is_instrumented_by_opentelemetry

def _instrument(self) -> None:
_instrumentor.instrument()

def _uninstrument(self) -> None:
_instrumentor.uninstrument()


register(FastAPIInstrumentorWrapper())
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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

_instrumentor = FlaskInstrumentor()


class FlaskInstrumentorWrapper(LibraryInstrumentor):
library_name = "flask"

def is_instrumented(self) -> bool:
return _instrumentor.is_instrumented_by_opentelemetry

def _instrument(self) -> None:
_instrumentor.instrument()

def _uninstrument(self) -> None:
_instrumentor.uninstrument()


register(FlaskInstrumentorWrapper())
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from opentelemetry.instrumentation.grpc import (
GrpcInstrumentorClient,
GrpcInstrumentorServer,
)

from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor
from sap_cloud_sdk.core.telemetry.instrumentation._registry import register

_client_instrumentor = GrpcInstrumentorClient()
_server_instrumentor = GrpcInstrumentorServer()


class GrpcInstrumentorWrapper(LibraryInstrumentor):
library_name = "grpc"

def is_instrumented(self) -> bool:
return (
_client_instrumentor.is_instrumented_by_opentelemetry
or _server_instrumentor.is_instrumented_by_opentelemetry
)

def _instrument(self) -> None:
_client_instrumentor.instrument()
_server_instrumentor.instrument()

def _uninstrument(self) -> None:
_client_instrumentor.uninstrument()
_server_instrumentor.uninstrument()


register(GrpcInstrumentorWrapper())
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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

_instrumentor = HTTPXClientInstrumentor()


class HttpxInstrumentor(LibraryInstrumentor):
library_name = "httpx"

def is_instrumented(self) -> bool:
return _instrumentor.is_instrumented_by_opentelemetry

def _instrument(self) -> None:
_instrumentor.instrument()

def _uninstrument(self) -> None:
_instrumentor.uninstrument()


register(HttpxInstrumentor())
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from opentelemetry.instrumentation.logging import LoggingInstrumentor

from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor
from sap_cloud_sdk.core.telemetry.instrumentation._registry import register

_instrumentor = LoggingInstrumentor()


class LoggingInstrumentorWrapper(LibraryInstrumentor):
library_name = "logging"

def is_instrumented(self) -> bool:
return _instrumentor.is_instrumented_by_opentelemetry

def _instrument(self) -> None:
_instrumentor.instrument(set_logging_format=True)

def _uninstrument(self) -> None:
_instrumentor.uninstrument()


register(LoggingInstrumentorWrapper())
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from opentelemetry.instrumentation.redis import RedisInstrumentor

from sap_cloud_sdk.core.telemetry.instrumentation.base import LibraryInstrumentor
from sap_cloud_sdk.core.telemetry.instrumentation._registry import register

_instrumentor = RedisInstrumentor()


class RedisInstrumentorWrapper(LibraryInstrumentor):
library_name = "redis"

def is_instrumented(self) -> bool:
return _instrumentor.is_instrumented_by_opentelemetry

def _instrument(self) -> None:
_instrumentor.instrument()

def _uninstrument(self) -> None:
_instrumentor.uninstrument()


register(RedisInstrumentorWrapper())
Loading
Loading