Skip to content

Commit 4ae0427

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

File tree

1 file changed

+68
-36
lines changed

1 file changed

+68
-36
lines changed

src/instana/instrumentation/wsgi.py

Lines changed: 68 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
99

1010
from opentelemetry.semconv.trace import SpanAttributes
1111
from 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

Comments
 (0)