Skip to content

Commit 5219242

Browse files
authored
Support PEP 561 to opentelemetry-instrumentation-wsgi (#3129)
1 parent b7e7d0c commit 5219242

File tree

5 files changed

+87
-52
lines changed

5 files changed

+87
-52
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3333
([#3148](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3148))
3434
- add support to Python 3.13
3535
([#3134](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3134))
36+
- `opentelemetry-opentelemetry-wsgi` Add `py.typed` file to enable PEP 561
37+
([#3129](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3129))
3638
- `opentelemetry-util-http` Add `py.typed` file to enable PEP 561
3739
([#3127](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3127))
3840

instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py

+74-50
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,22 @@ def GET(self):
9797
9898
.. code-block:: python
9999
100+
from wsgiref.types import WSGIEnvironment, StartResponse
101+
from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware
102+
103+
def app(environ: WSGIEnvironment, start_response: StartResponse):
104+
start_response("200 OK", [("Content-Type", "text/plain"), ("Content-Length", "13")])
105+
return [b"Hello, World!"]
106+
100107
def request_hook(span: Span, environ: WSGIEnvironment):
101108
if span and span.is_recording():
102109
span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
103110
104-
def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_headers: List):
111+
def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_headers: list[tuple[str, str]]):
105112
if span and span.is_recording():
106113
span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
107114
108-
OpenTelemetryMiddleware(request_hook=request_hook, response_hook=response_hook)
115+
OpenTelemetryMiddleware(app, request_hook=request_hook, response_hook=response_hook)
109116
110117
Capture HTTP request and response headers
111118
*****************************************
@@ -207,10 +214,12 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he
207214
---
208215
"""
209216

217+
from __future__ import annotations
218+
210219
import functools
211-
import typing
212220
import wsgiref.util as wsgiref_util
213221
from timeit import default_timer
222+
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, TypeVar, cast
214223

215224
from opentelemetry import context, trace
216225
from opentelemetry.instrumentation._semconv import (
@@ -240,14 +249,15 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he
240249
)
241250
from opentelemetry.instrumentation.utils import _start_internal_or_server_span
242251
from opentelemetry.instrumentation.wsgi.version import __version__
243-
from opentelemetry.metrics import get_meter
252+
from opentelemetry.metrics import MeterProvider, get_meter
244253
from opentelemetry.propagators.textmap import Getter
245254
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
246255
from opentelemetry.semconv.metrics import MetricInstruments
247256
from opentelemetry.semconv.metrics.http_metrics import (
248257
HTTP_SERVER_REQUEST_DURATION,
249258
)
250259
from opentelemetry.semconv.trace import SpanAttributes
260+
from opentelemetry.trace import TracerProvider
251261
from opentelemetry.trace.status import Status, StatusCode
252262
from opentelemetry.util.http import (
253263
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
@@ -262,15 +272,23 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he
262272
sanitize_method,
263273
)
264274

275+
if TYPE_CHECKING:
276+
from wsgiref.types import StartResponse, WSGIApplication, WSGIEnvironment
277+
278+
279+
T = TypeVar("T")
280+
RequestHook = Callable[[trace.Span, "WSGIEnvironment"], None]
281+
ResponseHook = Callable[
282+
[trace.Span, "WSGIEnvironment", str, "list[tuple[str, str]]"], None
283+
]
284+
265285
_HTTP_VERSION_PREFIX = "HTTP/"
266286
_CARRIER_KEY_PREFIX = "HTTP_"
267287
_CARRIER_KEY_PREFIX_LEN = len(_CARRIER_KEY_PREFIX)
268288

269289

270-
class WSGIGetter(Getter[dict]):
271-
def get(
272-
self, carrier: dict, key: str
273-
) -> typing.Optional[typing.List[str]]:
290+
class WSGIGetter(Getter[Dict[str, Any]]):
291+
def get(self, carrier: dict[str, Any], key: str) -> list[str] | None:
274292
"""Getter implementation to retrieve a HTTP header value from the
275293
PEP3333-conforming WSGI environ
276294
@@ -287,7 +305,7 @@ def get(
287305
return [value]
288306
return None
289307

290-
def keys(self, carrier):
308+
def keys(self, carrier: dict[str, Any]):
291309
return [
292310
key[_CARRIER_KEY_PREFIX_LEN:].lower().replace("_", "-")
293311
for key in carrier
@@ -298,26 +316,19 @@ def keys(self, carrier):
298316
wsgi_getter = WSGIGetter()
299317

300318

301-
def setifnotnone(dic, key, value):
302-
if value is not None:
303-
dic[key] = value
304-
305-
306319
# pylint: disable=too-many-branches
307-
308-
309320
def collect_request_attributes(
310-
environ,
311-
sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
321+
environ: WSGIEnvironment,
322+
sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
312323
):
313324
"""Collects HTTP request attributes from the PEP3333-conforming
314325
WSGI environ and returns a dictionary to be used as span creation attributes.
315326
"""
316-
result = {}
327+
result: dict[str, str | None] = {}
317328
_set_http_method(
318329
result,
319330
environ.get("REQUEST_METHOD", ""),
320-
sanitize_method(environ.get("REQUEST_METHOD", "")),
331+
sanitize_method(cast(str, environ.get("REQUEST_METHOD", ""))),
321332
sem_conv_opt_in_mode,
322333
)
323334
# old semconv v1.12.0
@@ -385,7 +396,7 @@ def collect_request_attributes(
385396
return result
386397

387398

388-
def collect_custom_request_headers_attributes(environ):
399+
def collect_custom_request_headers_attributes(environ: WSGIEnvironment):
389400
"""Returns custom HTTP request headers which are configured by the user
390401
from the PEP3333-conforming WSGI environ to be used as span creation attributes as described
391402
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):
411422
)
412423

413424

414-
def collect_custom_response_headers_attributes(response_headers):
425+
def collect_custom_response_headers_attributes(
426+
response_headers: list[tuple[str, str]],
427+
):
415428
"""Returns custom HTTP response headers which are configured by the user from the
416429
PEP3333-conforming WSGI environ as described in the specification
417430
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):
422435
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
423436
)
424437
)
425-
response_headers_dict = {}
438+
response_headers_dict: dict[str, str] = {}
426439
if response_headers:
427440
for key, val in response_headers:
428441
key = key.lower()
@@ -440,7 +453,8 @@ def collect_custom_response_headers_attributes(response_headers):
440453
)
441454

442455

443-
def _parse_status_code(resp_status):
456+
# TODO: Used only on the `opentelemetry-instrumentation-pyramid` package - It can be moved there.
457+
def _parse_status_code(resp_status: str) -> int | None:
444458
status_code, _ = resp_status.split(" ", 1)
445459
try:
446460
return int(status_code)
@@ -449,7 +463,7 @@ def _parse_status_code(resp_status):
449463

450464

451465
def _parse_active_request_count_attrs(
452-
req_attrs, sem_conv_opt_in_mode=_StabilityMode.DEFAULT
466+
req_attrs, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT
453467
):
454468
return _filter_semconv_active_request_count_attr(
455469
req_attrs,
@@ -460,7 +474,8 @@ def _parse_active_request_count_attrs(
460474

461475

462476
def _parse_duration_attrs(
463-
req_attrs, sem_conv_opt_in_mode=_StabilityMode.DEFAULT
477+
req_attrs: dict[str, str | None],
478+
sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
464479
):
465480
return _filter_semconv_duration_attrs(
466481
req_attrs,
@@ -471,11 +486,11 @@ def _parse_duration_attrs(
471486

472487

473488
def add_response_attributes(
474-
span,
475-
start_response_status,
476-
response_headers,
477-
duration_attrs=None,
478-
sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
489+
span: trace.Span,
490+
start_response_status: str,
491+
response_headers: list[tuple[str, str]],
492+
duration_attrs: dict[str, str | None] | None = None,
493+
sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
479494
): # pylint: disable=unused-argument
480495
"""Adds HTTP response attributes to span using the arguments
481496
passed to a PEP3333-conforming start_response callable.
@@ -497,7 +512,7 @@ def add_response_attributes(
497512
)
498513

499514

500-
def get_default_span_name(environ):
515+
def get_default_span_name(environ: WSGIEnvironment) -> str:
501516
"""
502517
Default span name is the HTTP method and URL path, or just the method.
503518
https://github.com/open-telemetry/opentelemetry-specification/pull/3165
@@ -508,10 +523,12 @@ def get_default_span_name(environ):
508523
Returns:
509524
The span name.
510525
"""
511-
method = sanitize_method(environ.get("REQUEST_METHOD", "").strip())
526+
method = sanitize_method(
527+
cast(str, environ.get("REQUEST_METHOD", "")).strip()
528+
)
512529
if method == "_OTHER":
513530
return "HTTP"
514-
path = environ.get("PATH_INFO", "").strip()
531+
path = cast(str, environ.get("PATH_INFO", "")).strip()
515532
if method and path:
516533
return f"{method} {path}"
517534
return method
@@ -538,11 +555,11 @@ class OpenTelemetryMiddleware:
538555

539556
def __init__(
540557
self,
541-
wsgi,
542-
request_hook=None,
543-
response_hook=None,
544-
tracer_provider=None,
545-
meter_provider=None,
558+
wsgi: WSGIApplication,
559+
request_hook: RequestHook | None = None,
560+
response_hook: ResponseHook | None = None,
561+
tracer_provider: TracerProvider | None = None,
562+
meter_provider: MeterProvider | None = None,
546563
):
547564
# initialize semantic conventions opt-in if needed
548565
_OpenTelemetrySemanticConventionStability._initialize()
@@ -589,14 +606,19 @@ def __init__(
589606

590607
@staticmethod
591608
def _create_start_response(
592-
span,
593-
start_response,
594-
response_hook,
595-
duration_attrs,
596-
sem_conv_opt_in_mode,
609+
span: trace.Span,
610+
start_response: StartResponse,
611+
response_hook: Callable[[str, list[tuple[str, str]]], None] | None,
612+
duration_attrs: dict[str, str | None],
613+
sem_conv_opt_in_mode: _StabilityMode,
597614
):
598615
@functools.wraps(start_response)
599-
def _start_response(status, response_headers, *args, **kwargs):
616+
def _start_response(
617+
status: str,
618+
response_headers: list[tuple[str, str]],
619+
*args: Any,
620+
**kwargs: Any,
621+
):
600622
add_response_attributes(
601623
span,
602624
status,
@@ -617,7 +639,9 @@ def _start_response(status, response_headers, *args, **kwargs):
617639
return _start_response
618640

619641
# pylint: disable=too-many-branches
620-
def __call__(self, environ, start_response):
642+
def __call__(
643+
self, environ: WSGIEnvironment, start_response: StartResponse
644+
):
621645
"""The WSGI application
622646
623647
Args:
@@ -699,7 +723,9 @@ def __call__(self, environ, start_response):
699723
# Put this in a subfunction to not delay the call to the wrapped
700724
# WSGI application (instrumentation should change the application
701725
# behavior as little as possible).
702-
def _end_span_after_iterating(iterable, span, token):
726+
def _end_span_after_iterating(
727+
iterable: Iterable[T], span: trace.Span, token: object
728+
) -> Iterable[T]:
703729
try:
704730
with trace.use_span(span):
705731
yield from iterable
@@ -713,10 +739,8 @@ def _end_span_after_iterating(iterable, span, token):
713739

714740

715741
# TODO: inherit from opentelemetry.instrumentation.propagators.Setter
716-
717-
718742
class ResponsePropagationSetter:
719-
def set(self, carrier, key, value): # pylint: disable=no-self-use
743+
def set(self, carrier: list[tuple[str, T]], key: str, value: T): # pylint: disable=no-self-use
720744
carrier.append((key, value))
721745

722746

instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/package.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from __future__ import annotations
1516

16-
_instruments = tuple()
17+
_instruments: tuple[str, ...] = tuple()
1718

1819
_supports_metrics = True
1920

instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/py.typed

Whitespace-only changes.

util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from re import IGNORECASE as RE_IGNORECASE
2020
from re import compile as re_compile
2121
from re import search
22-
from typing import Callable, Iterable
22+
from typing import Callable, Iterable, overload
2323
from urllib.parse import urlparse, urlunparse
2424

2525
from opentelemetry.semconv.trace import SpanAttributes
@@ -191,6 +191,14 @@ def normalise_response_header_name(header: str) -> str:
191191
return f"http.response.header.{key}"
192192

193193

194+
@overload
195+
def sanitize_method(method: str) -> str: ...
196+
197+
198+
@overload
199+
def sanitize_method(method: None) -> None: ...
200+
201+
194202
def sanitize_method(method: str | None) -> str | None:
195203
if method is None:
196204
return None

0 commit comments

Comments
 (0)