Skip to content

Commit 5bd20fe

Browse files
committed
wsgi: Ensure span stays active throughout the response iteration
Signed-off-by: Varsha GS <[email protected]>
1 parent 4121d8d commit 5bd20fe

File tree

1 file changed

+69
-36
lines changed

1 file changed

+69
-36
lines changed

src/instana/instrumentation/wsgi.py

Lines changed: 69 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Instana 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

1010
from opentelemetry.semconv.trace import SpanAttributes
1111
from opentelemetry import context, trace
@@ -15,6 +15,8 @@
1515
from instana.util.secrets import strip_secrets_from_query
1616
from instana.util.traceutils import extract_custom_headers
1717

18+
if TYPE_CHECKING:
19+
from instana.span.span import InstanaSpan
1820

1921
class 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

Comments
 (0)