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 , TYPE_CHECKING
99
1010from opentelemetry .semconv .trace import SpanAttributes
1111from opentelemetry import context , trace
1515from instana .util .secrets import strip_secrets_from_query
1616from instana .util .traceutils import extract_custom_headers
1717
18+ if TYPE_CHECKING :
19+ from instana .span .span import InstanaSpan
1820
1921class InstanaWSGIMiddleware (object ):
2022 """Instana WSGI middleware"""
@@ -25,55 +27,86 @@ def __init__(self, app: object) -> None:
2527 def __call__ (self , environ : Dict [str , Any ], start_response : Callable ) -> object :
2628 env = environ
2729
30+ # Extract context and start span
31+ span_context = tracer .extract (Format .HTTP_HEADERS , env )
32+ span = tracer .start_span ("wsgi" , span_context = span_context )
33+
34+ # Attach context - this makes the span current
35+ ctx = trace .set_span_in_context (span )
36+ token = context .attach (ctx )
37+
38+ # Extract custom headers from request
39+ extract_custom_headers (span , env , format = True )
40+
41+ # Set request attributes
42+ if "PATH_INFO" in env :
43+ span .set_attribute ("http.path" , env ["PATH_INFO" ])
44+ if "QUERY_STRING" in env and len (env ["QUERY_STRING" ]):
45+ scrubbed_params = strip_secrets_from_query (
46+ env ["QUERY_STRING" ],
47+ agent .options .secrets_matcher ,
48+ agent .options .secrets_list ,
49+ )
50+ span .set_attribute ("http.params" , scrubbed_params )
51+ if "REQUEST_METHOD" in env :
52+ span .set_attribute (SpanAttributes .HTTP_METHOD , env ["REQUEST_METHOD" ])
53+ if "HTTP_HOST" in env :
54+ span .set_attribute ("http.host" , env ["HTTP_HOST" ])
55+
2856 def new_start_response (
2957 status : str ,
3058 headers : List [Tuple [object , ...]],
3159 exc_info : Optional [Exception ] = None ,
3260 ) -> object :
3361 """Modified start response with additional headers."""
34- extract_custom_headers (self . span , headers )
62+ extract_custom_headers (span , headers )
3563
36- tracer .inject (self . span .context , Format .HTTP_HEADERS , headers )
64+ tracer .inject (span .context , Format .HTTP_HEADERS , headers )
3765
3866 headers_str = [
3967 (header [0 ], str (header [1 ]))
4068 if not isinstance (header [1 ], str )
4169 else header
4270 for header in headers
4371 ]
44- res = start_response (status , headers_str , exc_info )
4572
73+ # Set status code attribute
4674 sc = status .split (" " )[0 ]
4775 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 )
76+ span .mark_as_errored ()
77+
78+ span .set_attribute (SpanAttributes .HTTP_STATUS_CODE , sc )
79+
80+ return start_response (status , headers_str , exc_info )
81+
82+ try :
83+ iterable = self .app (environ , new_start_response )
84+
85+ # Wrap the iterable to ensure span ends after iteration completes
86+ return _end_span_after_iterating (iterable , span , token )
87+
88+ except Exception as exc :
89+ # If exception occurs before iteration completes, end span and detach token
90+ if span and span .is_recording ():
91+ span .record_exception (exc )
92+ span .end ()
93+ if token :
94+ context .detach (token )
95+ raise exc
96+
97+
98+ def _end_span_after_iterating (
99+ iterable : Iterable [object ], span : "InstanaSpan" , token : object
100+ ) -> Iterable [object ]:
101+ try :
102+ yield from iterable
103+ finally :
104+ # Ensure iterable cleanup (important for generators)
105+ if hasattr (iterable , "close" ):
106+ iterable .close ()
107+
108+ # End span and detach token after iteration completes
109+ if span and span .is_recording ():
110+ span .end ()
111+ if token :
112+ context .detach (token )
0 commit comments