Add type hints to Starlette instrumentation (#3045)

* Add type hints to Starlette instrumentation

* format

* Add changelog

* Add changelog

* Remove pyright ignore

---------

Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com>
This commit is contained in:
Marcelo Trylesinski 2024-12-03 04:28:45 +01:00 committed by GitHub
parent 142b86c1bf
commit 0da62aa532
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 44 additions and 26 deletions

View File

@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- `opentelemetry-instrumentation-starlette` Add type hints to the instrumentation
([#3045](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3045))
- `opentelemetry-distro` default to OTLP log exporter. - `opentelemetry-distro` default to OTLP log exporter.
([#3042](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3042)) ([#3042](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3042))
- `opentelemetry-instrumentation-sqlalchemy` Update unit tests to run with SQLALchemy 2 - `opentelemetry-instrumentation-sqlalchemy` Update unit tests to run with SQLALchemy 2

View File

@ -170,7 +170,9 @@ API
--- ---
""" """
from typing import Collection from __future__ import annotations
from typing import TYPE_CHECKING, Any, Collection, cast
from starlette import applications from starlette import applications
from starlette.routing import Match from starlette.routing import Match
@ -184,18 +186,29 @@ from opentelemetry.instrumentation.asgi.types import (
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.starlette.package import _instruments from opentelemetry.instrumentation.starlette.package import _instruments
from opentelemetry.instrumentation.starlette.version import __version__ from opentelemetry.instrumentation.starlette.version import __version__
from opentelemetry.metrics import get_meter from opentelemetry.metrics import MeterProvider, get_meter
from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import get_tracer from opentelemetry.trace import TracerProvider, get_tracer
from opentelemetry.util.http import get_excluded_urls from opentelemetry.util.http import get_excluded_urls
if TYPE_CHECKING:
from typing import TypedDict, Unpack
class InstrumentKwargs(TypedDict, total=False):
tracer_provider: TracerProvider
meter_provider: MeterProvider
server_request_hook: ServerRequestHook
client_request_hook: ClientRequestHook
client_response_hook: ClientResponseHook
_excluded_urls = get_excluded_urls("STARLETTE") _excluded_urls = get_excluded_urls("STARLETTE")
class StarletteInstrumentor(BaseInstrumentor): class StarletteInstrumentor(BaseInstrumentor):
"""An instrumentor for starlette """An instrumentor for Starlette.
See `BaseInstrumentor` See `BaseInstrumentor`.
""" """
_original_starlette = None _original_starlette = None
@ -206,8 +219,8 @@ class StarletteInstrumentor(BaseInstrumentor):
server_request_hook: ServerRequestHook = None, server_request_hook: ServerRequestHook = None,
client_request_hook: ClientRequestHook = None, client_request_hook: ClientRequestHook = None,
client_response_hook: ClientResponseHook = None, client_response_hook: ClientResponseHook = None,
meter_provider=None, meter_provider: MeterProvider | None = None,
tracer_provider=None, tracer_provider: TracerProvider | None = None,
): ):
"""Instrument an uninstrumented Starlette application.""" """Instrument an uninstrumented Starlette application."""
tracer = get_tracer( tracer = get_tracer(
@ -253,7 +266,7 @@ class StarletteInstrumentor(BaseInstrumentor):
def instrumentation_dependencies(self) -> Collection[str]: def instrumentation_dependencies(self) -> Collection[str]:
return _instruments return _instruments
def _instrument(self, **kwargs): def _instrument(self, **kwargs: Unpack[InstrumentKwargs]):
self._original_starlette = applications.Starlette self._original_starlette = applications.Starlette
_InstrumentedStarlette._tracer_provider = kwargs.get("tracer_provider") _InstrumentedStarlette._tracer_provider = kwargs.get("tracer_provider")
_InstrumentedStarlette._server_request_hook = kwargs.get( _InstrumentedStarlette._server_request_hook = kwargs.get(
@ -269,7 +282,7 @@ class StarletteInstrumentor(BaseInstrumentor):
applications.Starlette = _InstrumentedStarlette applications.Starlette = _InstrumentedStarlette
def _uninstrument(self, **kwargs): def _uninstrument(self, **kwargs: Any):
"""uninstrumenting all created apps by user""" """uninstrumenting all created apps by user"""
for instance in _InstrumentedStarlette._instrumented_starlette_apps: for instance in _InstrumentedStarlette._instrumented_starlette_apps:
self.uninstrument_app(instance) self.uninstrument_app(instance)
@ -278,14 +291,14 @@ class StarletteInstrumentor(BaseInstrumentor):
class _InstrumentedStarlette(applications.Starlette): class _InstrumentedStarlette(applications.Starlette):
_tracer_provider = None _tracer_provider: TracerProvider | None = None
_meter_provider = None _meter_provider: MeterProvider | None = None
_server_request_hook: ServerRequestHook = None _server_request_hook: ServerRequestHook = None
_client_request_hook: ClientRequestHook = None _client_request_hook: ClientRequestHook = None
_client_response_hook: ClientResponseHook = None _client_response_hook: ClientResponseHook = None
_instrumented_starlette_apps = set() _instrumented_starlette_apps: set[applications.Starlette] = set()
def __init__(self, *args, **kwargs): def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
tracer = get_tracer( tracer = get_tracer(
__name__, __name__,
@ -318,21 +331,22 @@ class _InstrumentedStarlette(applications.Starlette):
_InstrumentedStarlette._instrumented_starlette_apps.remove(self) _InstrumentedStarlette._instrumented_starlette_apps.remove(self)
def _get_route_details(scope): def _get_route_details(scope: dict[str, Any]) -> str | None:
""" """
Function to retrieve Starlette route from scope. Function to retrieve Starlette route from ASGI scope.
TODO: there is currently no way to retrieve http.route from TODO: there is currently no way to retrieve http.route from
a starlette application from scope. a starlette application from scope.
See: https://github.com/encode/starlette/pull/804 See: https://github.com/encode/starlette/pull/804
Args: Args:
scope: A Starlette scope scope: The ASGI scope that contains the Starlette application in the "app" key.
Returns: Returns:
A string containing the route or None The path to the route if found, otherwise None.
""" """
app = scope["app"] app = cast(applications.Starlette, scope["app"])
route = None route: str | None = None
for starlette_route in app.routes: for starlette_route in app.routes:
match, _ = starlette_route.matches(scope) match, _ = starlette_route.matches(scope)
@ -344,18 +358,20 @@ def _get_route_details(scope):
return route return route
def _get_default_span_details(scope): def _get_default_span_details(
""" scope: dict[str, Any],
Callback to retrieve span name and attributes from scope. ) -> tuple[str, dict[str, Any]]:
"""Callback to retrieve span name and attributes from ASGI scope.
Args: Args:
scope: A Starlette scope scope: The ASGI scope that contains the Starlette application in the "app" key.
Returns: Returns:
A tuple of span name and attributes A tuple of span name and attributes.
""" """
route = _get_route_details(scope) route = _get_route_details(scope)
method = scope.get("method", "") method: str = scope.get("method", "")
attributes = {} attributes: dict[str, Any] = {}
if route: if route:
attributes[SpanAttributes.HTTP_ROUTE] = route attributes[SpanAttributes.HTTP_ROUTE] = route
if method and route: # http if method and route: # http