diff --git a/CHANGELOG.md b/CHANGELOG.md index 0848cf9f7a..ea1a44b574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3148](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3148)) - add support to Python 3.13 ([#3134](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3134)) +- `opentelemetry-opentelemetry-wsgi` Add `py.typed` file to enable PEP 561 + ([#3129](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3129)) - `opentelemetry-util-http` Add `py.typed` file to enable PEP 561 ([#3127](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3127)) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py index bd3b2d18db..a0a2ce9a35 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py @@ -97,15 +97,22 @@ def GET(self): .. code-block:: python + from wsgiref.types import WSGIEnvironment, StartResponse + from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware + + def app(environ: WSGIEnvironment, start_response: StartResponse): + start_response("200 OK", [("Content-Type", "text/plain"), ("Content-Length", "13")]) + return [b"Hello, World!"] + def request_hook(span: Span, environ: WSGIEnvironment): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_request_hook", "some-value") - def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_headers: List): + def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_headers: list[tuple[str, str]]): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_response_hook", "some-value") - OpenTelemetryMiddleware(request_hook=request_hook, response_hook=response_hook) + OpenTelemetryMiddleware(app, request_hook=request_hook, response_hook=response_hook) Capture HTTP request and response headers ***************************************** @@ -207,10 +214,12 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he --- """ +from __future__ import annotations + import functools -import typing import wsgiref.util as wsgiref_util from timeit import default_timer +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, TypeVar, cast from opentelemetry import context, trace from opentelemetry.instrumentation._semconv import ( @@ -240,7 +249,7 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he ) from opentelemetry.instrumentation.utils import _start_internal_or_server_span from opentelemetry.instrumentation.wsgi.version import __version__ -from opentelemetry.metrics import get_meter +from opentelemetry.metrics import MeterProvider, get_meter from opentelemetry.propagators.textmap import Getter from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.metrics import MetricInstruments @@ -248,6 +257,7 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he HTTP_SERVER_REQUEST_DURATION, ) from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import TracerProvider from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, @@ -262,15 +272,23 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he sanitize_method, ) +if TYPE_CHECKING: + from wsgiref.types import StartResponse, WSGIApplication, WSGIEnvironment + + +T = TypeVar("T") +RequestHook = Callable[[trace.Span, "WSGIEnvironment"], None] +ResponseHook = Callable[ + [trace.Span, "WSGIEnvironment", str, "list[tuple[str, str]]"], None +] + _HTTP_VERSION_PREFIX = "HTTP/" _CARRIER_KEY_PREFIX = "HTTP_" _CARRIER_KEY_PREFIX_LEN = len(_CARRIER_KEY_PREFIX) -class WSGIGetter(Getter[dict]): - def get( - self, carrier: dict, key: str - ) -> typing.Optional[typing.List[str]]: +class WSGIGetter(Getter[Dict[str, Any]]): + def get(self, carrier: dict[str, Any], key: str) -> list[str] | None: """Getter implementation to retrieve a HTTP header value from the PEP3333-conforming WSGI environ @@ -287,7 +305,7 @@ def get( return [value] return None - def keys(self, carrier): + def keys(self, carrier: dict[str, Any]): return [ key[_CARRIER_KEY_PREFIX_LEN:].lower().replace("_", "-") for key in carrier @@ -298,26 +316,19 @@ def keys(self, carrier): wsgi_getter = WSGIGetter() -def setifnotnone(dic, key, value): - if value is not None: - dic[key] = value - - # pylint: disable=too-many-branches - - def collect_request_attributes( - environ, - sem_conv_opt_in_mode=_StabilityMode.DEFAULT, + environ: WSGIEnvironment, + sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): """Collects HTTP request attributes from the PEP3333-conforming WSGI environ and returns a dictionary to be used as span creation attributes. """ - result = {} + result: dict[str, str | None] = {} _set_http_method( result, environ.get("REQUEST_METHOD", ""), - sanitize_method(environ.get("REQUEST_METHOD", "")), + sanitize_method(cast(str, environ.get("REQUEST_METHOD", ""))), sem_conv_opt_in_mode, ) # old semconv v1.12.0 @@ -385,7 +396,7 @@ def collect_request_attributes( return result -def collect_custom_request_headers_attributes(environ): +def collect_custom_request_headers_attributes(environ: WSGIEnvironment): """Returns custom HTTP request headers which are configured by the user from the PEP3333-conforming WSGI environ to be used as span creation attributes as described in the specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers @@ -411,7 +422,9 @@ def collect_custom_request_headers_attributes(environ): ) -def collect_custom_response_headers_attributes(response_headers): +def collect_custom_response_headers_attributes( + response_headers: list[tuple[str, str]], +): """Returns custom HTTP response headers which are configured by the user from the PEP3333-conforming WSGI environ as described in the specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers @@ -422,7 +435,7 @@ def collect_custom_response_headers_attributes(response_headers): OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS ) ) - response_headers_dict = {} + response_headers_dict: dict[str, str] = {} if response_headers: for key, val in response_headers: key = key.lower() @@ -440,7 +453,8 @@ def collect_custom_response_headers_attributes(response_headers): ) -def _parse_status_code(resp_status): +# TODO: Used only on the `opentelemetry-instrumentation-pyramid` package - It can be moved there. +def _parse_status_code(resp_status: str) -> int | None: status_code, _ = resp_status.split(" ", 1) try: return int(status_code) @@ -449,7 +463,7 @@ def _parse_status_code(resp_status): def _parse_active_request_count_attrs( - req_attrs, sem_conv_opt_in_mode=_StabilityMode.DEFAULT + req_attrs, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT ): return _filter_semconv_active_request_count_attr( req_attrs, @@ -460,7 +474,8 @@ def _parse_active_request_count_attrs( def _parse_duration_attrs( - req_attrs, sem_conv_opt_in_mode=_StabilityMode.DEFAULT + req_attrs: dict[str, str | None], + sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): return _filter_semconv_duration_attrs( req_attrs, @@ -471,11 +486,11 @@ def _parse_duration_attrs( def add_response_attributes( - span, - start_response_status, - response_headers, - duration_attrs=None, - sem_conv_opt_in_mode=_StabilityMode.DEFAULT, + span: trace.Span, + start_response_status: str, + response_headers: list[tuple[str, str]], + duration_attrs: dict[str, str | None] | None = None, + sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): # pylint: disable=unused-argument """Adds HTTP response attributes to span using the arguments passed to a PEP3333-conforming start_response callable. @@ -497,7 +512,7 @@ def add_response_attributes( ) -def get_default_span_name(environ): +def get_default_span_name(environ: WSGIEnvironment) -> str: """ Default span name is the HTTP method and URL path, or just the method. https://github.com/open-telemetry/opentelemetry-specification/pull/3165 @@ -508,10 +523,12 @@ def get_default_span_name(environ): Returns: The span name. """ - method = sanitize_method(environ.get("REQUEST_METHOD", "").strip()) + method = sanitize_method( + cast(str, environ.get("REQUEST_METHOD", "")).strip() + ) if method == "_OTHER": return "HTTP" - path = environ.get("PATH_INFO", "").strip() + path = cast(str, environ.get("PATH_INFO", "")).strip() if method and path: return f"{method} {path}" return method @@ -538,11 +555,11 @@ class OpenTelemetryMiddleware: def __init__( self, - wsgi, - request_hook=None, - response_hook=None, - tracer_provider=None, - meter_provider=None, + wsgi: WSGIApplication, + request_hook: RequestHook | None = None, + response_hook: ResponseHook | None = None, + tracer_provider: TracerProvider | None = None, + meter_provider: MeterProvider | None = None, ): # initialize semantic conventions opt-in if needed _OpenTelemetrySemanticConventionStability._initialize() @@ -589,14 +606,19 @@ def __init__( @staticmethod def _create_start_response( - span, - start_response, - response_hook, - duration_attrs, - sem_conv_opt_in_mode, + span: trace.Span, + start_response: StartResponse, + response_hook: Callable[[str, list[tuple[str, str]]], None] | None, + duration_attrs: dict[str, str | None], + sem_conv_opt_in_mode: _StabilityMode, ): @functools.wraps(start_response) - def _start_response(status, response_headers, *args, **kwargs): + def _start_response( + status: str, + response_headers: list[tuple[str, str]], + *args: Any, + **kwargs: Any, + ): add_response_attributes( span, status, @@ -617,7 +639,9 @@ def _start_response(status, response_headers, *args, **kwargs): return _start_response # pylint: disable=too-many-branches - def __call__(self, environ, start_response): + def __call__( + self, environ: WSGIEnvironment, start_response: StartResponse + ): """The WSGI application Args: @@ -699,7 +723,9 @@ def __call__(self, environ, start_response): # Put this in a subfunction to not delay the call to the wrapped # WSGI application (instrumentation should change the application # behavior as little as possible). -def _end_span_after_iterating(iterable, span, token): +def _end_span_after_iterating( + iterable: Iterable[T], span: trace.Span, token: object +) -> Iterable[T]: try: with trace.use_span(span): yield from iterable @@ -713,10 +739,8 @@ def _end_span_after_iterating(iterable, span, token): # TODO: inherit from opentelemetry.instrumentation.propagators.Setter - - class ResponsePropagationSetter: - def set(self, carrier, key, value): # pylint: disable=no-self-use + def set(self, carrier: list[tuple[str, T]], key: str, value: T): # pylint: disable=no-self-use carrier.append((key, value)) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/package.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/package.py index 1bb8350a06..2dbb19055f 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/package.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/package.py @@ -12,8 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations -_instruments = tuple() +_instruments: tuple[str, ...] = tuple() _supports_metrics = True diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/py.typed b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py index c7dd9f7b06..71a6403a7d 100644 --- a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py +++ b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py @@ -19,7 +19,7 @@ from re import IGNORECASE as RE_IGNORECASE from re import compile as re_compile from re import search -from typing import Callable, Iterable +from typing import Callable, Iterable, overload from urllib.parse import urlparse, urlunparse from opentelemetry.semconv.trace import SpanAttributes @@ -191,6 +191,14 @@ def normalise_response_header_name(header: str) -> str: return f"http.response.header.{key}" +@overload +def sanitize_method(method: str) -> str: ... + + +@overload +def sanitize_method(method: None) -> None: ... + + def sanitize_method(method: str | None) -> str | None: if method is None: return None