55Instana WSGI Middleware
66"""
77
8- from typing import Dict , Any , Callable , List , Tuple , Optional
8+ from typing import Dict , Any , Callable , List , Tuple , Optional , Iterable
99
1010from opentelemetry .semconv .trace import SpanAttributes
1111from opentelemetry import context , trace
@@ -25,55 +25,87 @@ def __init__(self, app: object) -> None:
2525 def __call__ (self , environ : Dict [str , Any ], start_response : Callable ) -> object :
2626 env = environ
2727
28+ # Extract context and start span
29+ span_context = tracer .extract (Format .HTTP_HEADERS , env )
30+ span = tracer .start_span ("wsgi" , span_context = span_context )
31+
32+ # Attach context - this makes the span current
33+ ctx = trace .set_span_in_context (span )
34+ token = context .attach (ctx )
35+
36+ # Extract custom headers from request
37+ extract_custom_headers (span , env , format = True )
38+
39+ # Set request attributes
40+ if "PATH_INFO" in env :
41+ span .set_attribute ("http.path" , env ["PATH_INFO" ])
42+ if "QUERY_STRING" in env and len (env ["QUERY_STRING" ]):
43+ scrubbed_params = strip_secrets_from_query (
44+ env ["QUERY_STRING" ],
45+ agent .options .secrets_matcher ,
46+ agent .options .secrets_list ,
47+ )
48+ span .set_attribute ("http.params" , scrubbed_params )
49+ if "REQUEST_METHOD" in env :
50+ span .set_attribute (SpanAttributes .HTTP_METHOD , env ["REQUEST_METHOD" ])
51+ if "HTTP_HOST" in env :
52+ span .set_attribute ("http.host" , env ["HTTP_HOST" ])
53+
2854 def new_start_response (
2955 status : str ,
3056 headers : List [Tuple [object , ...]],
3157 exc_info : Optional [Exception ] = None ,
3258 ) -> object :
3359 """Modified start response with additional headers."""
34- extract_custom_headers (self . span , headers )
60+ extract_custom_headers (span , headers )
3561
36- tracer .inject (self . span .context , Format .HTTP_HEADERS , headers )
62+ tracer .inject (span .context , Format .HTTP_HEADERS , headers )
3763
3864 headers_str = [
3965 (header [0 ], str (header [1 ]))
4066 if not isinstance (header [1 ], str )
4167 else header
4268 for header in headers
4369 ]
44- res = start_response (status , headers_str , exc_info )
4570
71+ # Set status code attribute
4672 sc = status .split (" " )[0 ]
4773 if 500 <= int (sc ):
48- self .span .mark_as_errored ()
49-
50- self .span .set_attribute (SpanAttributes .HTTP_STATUS_CODE , sc )
51- if self .span and self .span .is_recording ():
52- self .span .end ()
53- if self .token :
54- context .detach (self .token )
55- return res
56-
57- span_context = tracer .extract (Format .HTTP_HEADERS , env )
58- self .span = tracer .start_span ("wsgi" , span_context = span_context )
59-
60- ctx = trace .set_span_in_context (self .span )
61- self .token = context .attach (ctx )
62-
63- extract_custom_headers (self .span , env , format = True )
64-
65- if "PATH_INFO" in env :
66- self .span .set_attribute ("http.path" , env ["PATH_INFO" ])
67- if "QUERY_STRING" in env and len (env ["QUERY_STRING" ]):
68- scrubbed_params = strip_secrets_from_query (
69- env ["QUERY_STRING" ],
70- agent .options .secrets_matcher ,
71- agent .options .secrets_list ,
72- )
73- self .span .set_attribute ("http.params" , scrubbed_params )
74- if "REQUEST_METHOD" in env :
75- self .span .set_attribute (SpanAttributes .HTTP_METHOD , env ["REQUEST_METHOD" ])
76- if "HTTP_HOST" in env :
77- self .span .set_attribute ("http.host" , env ["HTTP_HOST" ])
78-
79- return self .app (environ , new_start_response )
74+ span .mark_as_errored ()
75+
76+ span .set_attribute (SpanAttributes .HTTP_STATUS_CODE , sc )
77+
78+ return start_response (status , headers_str , exc_info )
79+
80+ try :
81+ iterable = self .app (environ , new_start_response )
82+
83+ # Wrap the iterable to ensure span ends after iteration completes
84+ return _end_span_after_iterating (iterable , span , token )
85+
86+ except Exception as exc :
87+ # If exception occurs before iteration, end span and detach token
88+ if span and span .is_recording ():
89+ span .record_exception (exc )
90+ span .end ()
91+ if token :
92+ context .detach (token )
93+ raise exc
94+
95+
96+ def _end_span_after_iterating (
97+ iterable : Iterable [object ], span : trace .Span , token : object
98+ ) -> Iterable [object ]:
99+ try :
100+ yield from iterable
101+ finally :
102+ # Ensure iterable cleanup (important for generators)
103+ close = getattr (iterable , "close" , None )
104+ if close :
105+ close ()
106+
107+ # End span and detach token after iteration completes
108+ if span and span .is_recording ():
109+ span .end ()
110+ if token :
111+ context .detach (token )
0 commit comments