Skip to content

Commit 71ca528

Browse files
committed
Support PEP 561 to opentelemetry-instrumentation-wsgi
1 parent 54cbf59 commit 71ca528

File tree

3 files changed

+86
-52
lines changed
  • instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi
  • util/opentelemetry-util-http/src/opentelemetry/util/http

3 files changed

+86
-52
lines changed

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

Lines changed: 75 additions & 50 deletions
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,13 @@ 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, Iterable, TypeVar, cast
223+
from wsgiref.types import StartResponse
214224

215225
from opentelemetry import context, trace
216226
from opentelemetry.instrumentation._semconv import (
@@ -240,14 +250,15 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he
240250
)
241251
from opentelemetry.instrumentation.utils import _start_internal_or_server_span
242252
from opentelemetry.instrumentation.wsgi.version import __version__
243-
from opentelemetry.metrics import get_meter
253+
from opentelemetry.metrics import MeterProvider, get_meter
244254
from opentelemetry.propagators.textmap import Getter
245255
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
246256
from opentelemetry.semconv.metrics import MetricInstruments
247257
from opentelemetry.semconv.metrics.http_metrics import (
248258
HTTP_SERVER_REQUEST_DURATION,
249259
)
250260
from opentelemetry.semconv.trace import SpanAttributes
261+
from opentelemetry.trace import TracerProvider
251262
from opentelemetry.trace.status import Status, StatusCode
252263
from opentelemetry.util.http import (
253264
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
@@ -262,15 +273,23 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he
262273
sanitize_method,
263274
)
264275

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

269290

270-
class WSGIGetter(Getter[dict]):
271-
def get(
272-
self, carrier: dict, key: str
273-
) -> typing.Optional[typing.List[str]]:
291+
class WSGIGetter(Getter[dict[str, Any]]):
292+
def get(self, carrier: dict[str, Any], key: str) -> list[str] | None:
274293
"""Getter implementation to retrieve a HTTP header value from the
275294
PEP3333-conforming WSGI environ
276295
@@ -287,7 +306,7 @@ def get(
287306
return [value]
288307
return None
289308

290-
def keys(self, carrier):
309+
def keys(self, carrier: dict[str, Any]):
291310
return [
292311
key[_CARRIER_KEY_PREFIX_LEN:].lower().replace("_", "-")
293312
for key in carrier
@@ -298,26 +317,19 @@ def keys(self, carrier):
298317
wsgi_getter = WSGIGetter()
299318

300319

301-
def setifnotnone(dic, key, value):
302-
if value is not None:
303-
dic[key] = value
304-
305-
306320
# pylint: disable=too-many-branches
307-
308-
309321
def collect_request_attributes(
310-
environ,
311-
sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
322+
environ: WSGIEnvironment,
323+
sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
312324
):
313325
"""Collects HTTP request attributes from the PEP3333-conforming
314326
WSGI environ and returns a dictionary to be used as span creation attributes.
315327
"""
316-
result = {}
328+
result: dict[str, str | None] = {}
317329
_set_http_method(
318330
result,
319331
environ.get("REQUEST_METHOD", ""),
320-
sanitize_method(environ.get("REQUEST_METHOD", "")),
332+
sanitize_method(cast(str, environ.get("REQUEST_METHOD", ""))),
321333
sem_conv_opt_in_mode,
322334
)
323335
# old semconv v1.12.0
@@ -385,7 +397,7 @@ def collect_request_attributes(
385397
return result
386398

387399

388-
def collect_custom_request_headers_attributes(environ):
400+
def collect_custom_request_headers_attributes(environ: WSGIEnvironment):
389401
"""Returns custom HTTP request headers which are configured by the user
390402
from the PEP3333-conforming WSGI environ to be used as span creation attributes as described
391403
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 +423,9 @@ def collect_custom_request_headers_attributes(environ):
411423
)
412424

413425

414-
def collect_custom_response_headers_attributes(response_headers):
426+
def collect_custom_response_headers_attributes(
427+
response_headers: list[tuple[str, str]],
428+
):
415429
"""Returns custom HTTP response headers which are configured by the user from the
416430
PEP3333-conforming WSGI environ as described in the specification
417431
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers
@@ -422,7 +436,7 @@ def collect_custom_response_headers_attributes(response_headers):
422436
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
423437
)
424438
)
425-
response_headers_dict = {}
439+
response_headers_dict: dict[str, str] = {}
426440
if response_headers:
427441
for key, val in response_headers:
428442
key = key.lower()
@@ -440,7 +454,8 @@ def collect_custom_response_headers_attributes(response_headers):
440454
)
441455

442456

443-
def _parse_status_code(resp_status):
457+
# TODO: Used only on the `opentelemetry-instrumentation-pyramid` package - It can be moved there.
458+
def _parse_status_code(resp_status: str) -> int | None:
444459
status_code, _ = resp_status.split(" ", 1)
445460
try:
446461
return int(status_code)
@@ -449,7 +464,7 @@ def _parse_status_code(resp_status):
449464

450465

451466
def _parse_active_request_count_attrs(
452-
req_attrs, sem_conv_opt_in_mode=_StabilityMode.DEFAULT
467+
req_attrs, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT
453468
):
454469
return _filter_semconv_active_request_count_attr(
455470
req_attrs,
@@ -460,7 +475,8 @@ def _parse_active_request_count_attrs(
460475

461476

462477
def _parse_duration_attrs(
463-
req_attrs, sem_conv_opt_in_mode=_StabilityMode.DEFAULT
478+
req_attrs: dict[str, str | None],
479+
sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
464480
):
465481
return _filter_semconv_duration_attrs(
466482
req_attrs,
@@ -471,11 +487,11 @@ def _parse_duration_attrs(
471487

472488

473489
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,
490+
span: trace.Span,
491+
start_response_status: str,
492+
response_headers: list[tuple[str, str]],
493+
duration_attrs: dict[str, str | None] | None = None,
494+
sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
479495
): # pylint: disable=unused-argument
480496
"""Adds HTTP response attributes to span using the arguments
481497
passed to a PEP3333-conforming start_response callable.
@@ -501,7 +517,7 @@ def add_response_attributes(
501517
)
502518

503519

504-
def get_default_span_name(environ):
520+
def get_default_span_name(environ: WSGIEnvironment) -> str:
505521
"""
506522
Default span name is the HTTP method and URL path, or just the method.
507523
https://github.com/open-telemetry/opentelemetry-specification/pull/3165
@@ -512,10 +528,12 @@ def get_default_span_name(environ):
512528
Returns:
513529
The span name.
514530
"""
515-
method = sanitize_method(environ.get("REQUEST_METHOD", "").strip())
531+
method = sanitize_method(
532+
cast(str, environ.get("REQUEST_METHOD", "")).strip()
533+
)
516534
if method == "_OTHER":
517535
return "HTTP"
518-
path = environ.get("PATH_INFO", "").strip()
536+
path = cast(str, environ.get("PATH_INFO", "")).strip()
519537
if method and path:
520538
return f"{method} {path}"
521539
return method
@@ -542,11 +560,11 @@ class OpenTelemetryMiddleware:
542560

543561
def __init__(
544562
self,
545-
wsgi,
546-
request_hook=None,
547-
response_hook=None,
548-
tracer_provider=None,
549-
meter_provider=None,
563+
wsgi: WSGIApplication,
564+
request_hook: RequestHook | None = None,
565+
response_hook: ResponseHook | None = None,
566+
tracer_provider: TracerProvider | None = None,
567+
meter_provider: MeterProvider | None = None,
550568
):
551569
# initialize semantic conventions opt-in if needed
552570
_OpenTelemetrySemanticConventionStability._initialize()
@@ -593,14 +611,19 @@ def __init__(
593611

594612
@staticmethod
595613
def _create_start_response(
596-
span,
597-
start_response,
598-
response_hook,
599-
duration_attrs,
600-
sem_conv_opt_in_mode,
614+
span: trace.Span,
615+
start_response: StartResponse,
616+
response_hook: Callable[[str, list[tuple[str, str]]], None] | None,
617+
duration_attrs: dict[str, str | None],
618+
sem_conv_opt_in_mode: _StabilityMode,
601619
):
602620
@functools.wraps(start_response)
603-
def _start_response(status, response_headers, *args, **kwargs):
621+
def _start_response(
622+
status: str,
623+
response_headers: list[tuple[str, str]],
624+
*args: Any,
625+
**kwargs: Any,
626+
):
604627
add_response_attributes(
605628
span,
606629
status,
@@ -621,7 +644,9 @@ def _start_response(status, response_headers, *args, **kwargs):
621644
return _start_response
622645

623646
# pylint: disable=too-many-branches
624-
def __call__(self, environ, start_response):
647+
def __call__(
648+
self, environ: WSGIEnvironment, start_response: StartResponse
649+
):
625650
"""The WSGI application
626651
627652
Args:
@@ -703,7 +728,9 @@ def __call__(self, environ, start_response):
703728
# Put this in a subfunction to not delay the call to the wrapped
704729
# WSGI application (instrumentation should change the application
705730
# behavior as little as possible).
706-
def _end_span_after_iterating(iterable, span, token):
731+
def _end_span_after_iterating(
732+
iterable: Iterable[T], span: trace.Span, token: object
733+
) -> Iterable[T]:
707734
try:
708735
with trace.use_span(span):
709736
yield from iterable
@@ -717,10 +744,8 @@ def _end_span_after_iterating(iterable, span, token):
717744

718745

719746
# TODO: inherit from opentelemetry.instrumentation.propagators.Setter
720-
721-
722747
class ResponsePropagationSetter:
723-
def set(self, carrier, key, value): # pylint: disable=no-self-use
748+
def set(self, carrier: list[tuple[str, T]], key: str, value: T): # pylint: disable=no-self-use
724749
carrier.append((key, value))
725750

726751

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

Lines changed: 2 additions & 1 deletion
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

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

Lines changed: 9 additions & 1 deletion
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, Optional
22+
from typing import Callable, Iterable, Optional, overload
2323
from urllib.parse import urlparse, urlunparse
2424

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

195195

196+
@overload
197+
def sanitize_method(method: str) -> str: ...
198+
199+
200+
@overload
201+
def sanitize_method(method: None) -> None: ...
202+
203+
196204
def sanitize_method(method: Optional[str]) -> Optional[str]:
197205
if method is None:
198206
return None

0 commit comments

Comments
 (0)