From 43cfdcacf7aa77891c28056aaff03be6f3165ef6 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 13 Nov 2025 16:23:18 +0530 Subject: [PATCH 1/3] wsgi: Ensure span stays active throughout the response iteration Signed-off-by: Varsha GS --- src/instana/instrumentation/wsgi.py | 105 ++++++++++++++++++---------- 1 file changed, 69 insertions(+), 36 deletions(-) diff --git a/src/instana/instrumentation/wsgi.py b/src/instana/instrumentation/wsgi.py index 5ab7a2f7..5f039c15 100644 --- a/src/instana/instrumentation/wsgi.py +++ b/src/instana/instrumentation/wsgi.py @@ -5,7 +5,7 @@ Instana WSGI Middleware """ -from typing import Dict, Any, Callable, List, Tuple, Optional +from typing import Dict, Any, Callable, List, Tuple, Optional, Iterable, TYPE_CHECKING from opentelemetry.semconv.trace import SpanAttributes from opentelemetry import context, trace @@ -15,6 +15,8 @@ from instana.util.secrets import strip_secrets_from_query from instana.util.traceutils import extract_custom_headers +if TYPE_CHECKING: + from instana.span.span import InstanaSpan class InstanaWSGIMiddleware(object): """Instana WSGI middleware""" @@ -25,15 +27,41 @@ def __init__(self, app: object) -> None: def __call__(self, environ: Dict[str, Any], start_response: Callable) -> object: env = environ + # Extract context and start span + span_context = tracer.extract(Format.HTTP_HEADERS, env) + span = tracer.start_span("wsgi", span_context=span_context) + + # Attach context - this makes the span current + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + + # Extract custom headers from request + extract_custom_headers(span, env, format=True) + + # Set request attributes + if "PATH_INFO" in env: + span.set_attribute("http.path", env["PATH_INFO"]) + if "QUERY_STRING" in env and len(env["QUERY_STRING"]): + scrubbed_params = strip_secrets_from_query( + env["QUERY_STRING"], + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + span.set_attribute("http.params", scrubbed_params) + if "REQUEST_METHOD" in env: + span.set_attribute(SpanAttributes.HTTP_METHOD, env["REQUEST_METHOD"]) + if "HTTP_HOST" in env: + span.set_attribute("http.host", env["HTTP_HOST"]) + def new_start_response( status: str, headers: List[Tuple[object, ...]], exc_info: Optional[Exception] = None, ) -> object: """Modified start response with additional headers.""" - extract_custom_headers(self.span, headers) + extract_custom_headers(span, headers) - tracer.inject(self.span.context, Format.HTTP_HEADERS, headers) + tracer.inject(span.context, Format.HTTP_HEADERS, headers) headers_str = [ (header[0], str(header[1])) @@ -41,39 +69,44 @@ def new_start_response( else header for header in headers ] - res = start_response(status, headers_str, exc_info) + # Set status code attribute sc = status.split(" ")[0] if 500 <= int(sc): - self.span.mark_as_errored() - - self.span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, sc) - if self.span and self.span.is_recording(): - self.span.end() - if self.token: - context.detach(self.token) - return res - - span_context = tracer.extract(Format.HTTP_HEADERS, env) - self.span = tracer.start_span("wsgi", span_context=span_context) - - ctx = trace.set_span_in_context(self.span) - self.token = context.attach(ctx) - - extract_custom_headers(self.span, env, format=True) - - if "PATH_INFO" in env: - self.span.set_attribute("http.path", env["PATH_INFO"]) - if "QUERY_STRING" in env and len(env["QUERY_STRING"]): - scrubbed_params = strip_secrets_from_query( - env["QUERY_STRING"], - agent.options.secrets_matcher, - agent.options.secrets_list, - ) - self.span.set_attribute("http.params", scrubbed_params) - if "REQUEST_METHOD" in env: - self.span.set_attribute(SpanAttributes.HTTP_METHOD, env["REQUEST_METHOD"]) - if "HTTP_HOST" in env: - self.span.set_attribute("http.host", env["HTTP_HOST"]) - - return self.app(environ, new_start_response) + span.mark_as_errored() + + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, sc) + + return start_response(status, headers_str, exc_info) + + try: + iterable = self.app(environ, new_start_response) + + # Wrap the iterable to ensure span ends after iteration completes + return _end_span_after_iterating(iterable, span, token) + + except Exception as exc: + # If exception occurs before iteration completes, end span and detach token + if span and span.is_recording(): + span.record_exception(exc) + span.end() + if token: + context.detach(token) + raise exc + + +def _end_span_after_iterating( + iterable: Iterable[object], span: "InstanaSpan", token: object +) -> Iterable[object]: + try: + yield from iterable + finally: + # Ensure iterable cleanup (important for generators) + if hasattr(iterable, "close"): + iterable.close() + + # End span and detach token after iteration completes + if span and span.is_recording(): + span.end() + if token: + context.detach(token) From 763d2b8db851f151725e3bcbde2c3dc854d321f2 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 13 Nov 2025 16:43:32 +0530 Subject: [PATCH 2/3] chore(wsgi): move setting request attributes to a separate method Signed-off-by: Varsha GS --- src/instana/instrumentation/wsgi.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/instana/instrumentation/wsgi.py b/src/instana/instrumentation/wsgi.py index 5f039c15..ea020495 100644 --- a/src/instana/instrumentation/wsgi.py +++ b/src/instana/instrumentation/wsgi.py @@ -39,19 +39,7 @@ def __call__(self, environ: Dict[str, Any], start_response: Callable) -> object: extract_custom_headers(span, env, format=True) # Set request attributes - if "PATH_INFO" in env: - span.set_attribute("http.path", env["PATH_INFO"]) - if "QUERY_STRING" in env and len(env["QUERY_STRING"]): - scrubbed_params = strip_secrets_from_query( - env["QUERY_STRING"], - agent.options.secrets_matcher, - agent.options.secrets_list, - ) - span.set_attribute("http.params", scrubbed_params) - if "REQUEST_METHOD" in env: - span.set_attribute(SpanAttributes.HTTP_METHOD, env["REQUEST_METHOD"]) - if "HTTP_HOST" in env: - span.set_attribute("http.host", env["HTTP_HOST"]) + _set_request_attributes(span, env) def new_start_response( status: str, @@ -110,3 +98,18 @@ def _end_span_after_iterating( span.end() if token: context.detach(token) + +def _set_request_attributes(span: "InstanaSpan", env: Dict[str, Any]) -> None: + if "PATH_INFO" in env: + span.set_attribute("http.path", env["PATH_INFO"]) + if "QUERY_STRING" in env and len(env["QUERY_STRING"]): + scrubbed_params = strip_secrets_from_query( + env["QUERY_STRING"], + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + span.set_attribute("http.params", scrubbed_params) + if "REQUEST_METHOD" in env: + span.set_attribute(SpanAttributes.HTTP_METHOD, env["REQUEST_METHOD"]) + if "HTTP_HOST" in env: + span.set_attribute(SpanAttributes.HTTP_HOST, env["HTTP_HOST"]) From 01e175770c0822f7e80fa819d4778204f6259a51 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 13 Nov 2025 17:03:02 +0530 Subject: [PATCH 3/3] chore(version): Bump version to 3.9.3 Signed-off-by: Varsha GS --- src/instana/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instana/version.py b/src/instana/version.py index 1419967c..6db3016f 100644 --- a/src/instana/version.py +++ b/src/instana/version.py @@ -3,4 +3,4 @@ # Module version file. Used by setup.py and snapshot reporting. -VERSION = "3.9.2" +VERSION = "3.9.3"