|
5 | 5 | Instana WSGI Middleware |
6 | 6 | """ |
7 | 7 |
|
8 | | -from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple |
| 8 | +from typing import Any, Callable |
9 | 9 |
|
10 | | -from opentelemetry import context, trace |
11 | | -from opentelemetry.semconv.trace import SpanAttributes |
| 10 | +from opentelemetry import context |
12 | 11 |
|
13 | | -from instana.propagators.format import Format |
14 | | -from instana.singletons import agent, get_tracer |
15 | | -from instana.util.secrets import strip_secrets_from_query |
16 | | -from instana.util.traceutils import extract_custom_headers |
17 | | - |
18 | | -if TYPE_CHECKING: |
19 | | - from instana.span.span import InstanaSpan |
| 12 | +from instana.util.wsgi_utils import ( |
| 13 | + build_start_response, |
| 14 | + create_span_with_context, |
| 15 | + end_span_after_iterating, |
| 16 | +) |
20 | 17 |
|
21 | 18 |
|
22 | 19 | class InstanaWSGIMiddleware(object): |
23 | 20 | """Instana WSGI middleware""" |
24 | 21 |
|
25 | | - def __init__(self, app: object) -> None: |
| 22 | + def __init__(self, app: Callable) -> None: |
26 | 23 | self.app = app |
27 | 24 |
|
28 | | - def __call__(self, environ: Dict[str, Any], start_response: Callable) -> object: |
29 | | - env = environ |
30 | | - tracer = get_tracer() |
31 | | - |
32 | | - # Extract context and start span |
33 | | - parent_context = tracer.extract(Format.HTTP_HEADERS, env) |
34 | | - span = tracer.start_span("wsgi", context=parent_context) |
35 | | - |
36 | | - # Attach context - this makes the span current |
37 | | - ctx = trace.set_span_in_context(span) |
38 | | - token = context.attach(ctx) |
39 | | - |
40 | | - # Extract custom headers from request |
41 | | - extract_custom_headers(span, env, format=True) |
42 | | - |
43 | | - # Set request attributes |
44 | | - _set_request_attributes(span, env) |
45 | | - |
46 | | - def new_start_response( |
47 | | - status: str, |
48 | | - headers: List[Tuple[object, ...]], |
49 | | - exc_info: Optional[Exception] = None, |
50 | | - ) -> object: |
51 | | - """Modified start response with additional headers.""" |
52 | | - extract_custom_headers(span, headers) |
53 | | - |
54 | | - tracer.inject(span.context, Format.HTTP_HEADERS, headers) |
55 | | - |
56 | | - headers_str = [ |
57 | | - (header[0], str(header[1])) |
58 | | - if not isinstance(header[1], str) |
59 | | - else header |
60 | | - for header in headers |
61 | | - ] |
62 | | - |
63 | | - # Set status code attribute |
64 | | - sc = status.split(" ")[0] |
65 | | - if int(sc) >= 500: |
66 | | - span.mark_as_errored() |
67 | | - |
68 | | - span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, sc) |
69 | | - |
70 | | - return start_response(status, headers_str, exc_info) |
71 | | - |
| 25 | + def __call__(self, environ: dict[str, Any], start_response: Callable) -> object: |
72 | 26 | try: |
73 | | - iterable = self.app(environ, new_start_response) |
74 | | - |
75 | | - # Wrap the iterable to ensure span ends after iteration completes |
76 | | - return _end_span_after_iterating(iterable, span, token) |
| 27 | + span, token = create_span_with_context(environ) |
| 28 | + wrapped_start_response = build_start_response( |
| 29 | + span, start_response, status_as_string=True |
| 30 | + ) |
| 31 | + except Exception: |
| 32 | + return self.app(environ, start_response) |
77 | 33 |
|
| 34 | + try: |
| 35 | + iterable = self.app(environ, wrapped_start_response) |
| 36 | + return end_span_after_iterating(iterable, span, token) |
78 | 37 | except Exception as exc: |
79 | | - # If exception occurs before iteration completes, end span and detach token |
80 | 38 | if span and span.is_recording(): |
81 | 39 | span.record_exception(exc) |
82 | 40 | span.end() |
83 | 41 | if token: |
84 | 42 | context.detach(token) |
85 | 43 | raise exc |
86 | | - |
87 | | - |
88 | | -def _end_span_after_iterating( |
89 | | - iterable: Iterable[object], span: "InstanaSpan", token: object |
90 | | -) -> Iterable[object]: |
91 | | - try: |
92 | | - yield from iterable |
93 | | - finally: |
94 | | - # Ensure iterable cleanup (important for generators) |
95 | | - if hasattr(iterable, "close"): |
96 | | - iterable.close() |
97 | | - |
98 | | - # End span and detach token after iteration completes |
99 | | - if span and span.is_recording(): |
100 | | - span.end() |
101 | | - if token: |
102 | | - context.detach(token) |
103 | | - |
104 | | - |
105 | | -def _set_request_attributes(span: "InstanaSpan", env: Dict[str, Any]) -> None: |
106 | | - if "PATH_INFO" in env: |
107 | | - span.set_attribute("http.path", env["PATH_INFO"]) |
108 | | - if "QUERY_STRING" in env and len(env["QUERY_STRING"]): |
109 | | - scrubbed_params = strip_secrets_from_query( |
110 | | - env["QUERY_STRING"], |
111 | | - agent.options.secrets_matcher, |
112 | | - agent.options.secrets_list, |
113 | | - ) |
114 | | - span.set_attribute("http.params", scrubbed_params) |
115 | | - if "REQUEST_METHOD" in env: |
116 | | - span.set_attribute(SpanAttributes.HTTP_METHOD, env["REQUEST_METHOD"]) |
117 | | - if "HTTP_HOST" in env: |
118 | | - span.set_attribute(SpanAttributes.HTTP_HOST, env["HTTP_HOST"]) |
0 commit comments