1- # (c) Copyright IBM Corp. 2021
2- # (c) Copyright Instana Inc. 2019
1+ # (c) Copyright IBM Corp. 2021, 2025
32
43
5- import wrapt
6- import flask
4+ import re
75from importlib .metadata import version
8- from typing import Callable , Tuple , Dict , Any , TYPE_CHECKING , Union
6+ from typing import TYPE_CHECKING , Any , Callable , Dict , Tuple , Type , Union
97
8+ import flask
9+ import wrapt
10+ from opentelemetry import context , trace
1011from opentelemetry .semconv .trace import SpanAttributes
1112
1213from instana .log import logger
13- from instana .singletons import tracer
1414from instana .propagators .format import Format
15-
15+ from instana .singletons import agent , get_tracer
16+ from instana .util .secrets import strip_secrets_from_query
17+ from instana .util .traceutils import extract_custom_headers
1618
1719if TYPE_CHECKING :
18- from werkzeug .exceptions import HTTPException
1920 from flask .typing import ResponseReturnValue
2021 from jinja2 .environment import Template
22+ from werkzeug .exceptions import HTTPException
2123
22- @wrapt .patch_function_wrapper ('flask' , 'templating._render' )
24+ path_tpl_re = re .compile ("<.*>" )
25+
26+
27+ @wrapt .patch_function_wrapper ("flask" , "templating._render" )
2328def render_with_instana (
2429 wrapped : Callable [..., str ],
2530 instance : object ,
@@ -32,6 +37,7 @@ def render_with_instana(
3237
3338 parent_span = flask .g .span
3439 parent_context = parent_span .get_span_context ()
40+ tracer = get_tracer ()
3541
3642 with tracer .start_as_current_span ("render" , span_context = parent_context ) as span :
3743 try :
@@ -50,7 +56,7 @@ def render_with_instana(
5056 raise
5157
5258
53- @wrapt .patch_function_wrapper (' flask' , ' Flask.handle_user_exception' )
59+ @wrapt .patch_function_wrapper (" flask" , " Flask.handle_user_exception" )
5460def handle_user_exception_with_instana (
5561 wrapped : Callable [..., Union ["HTTPException" , "ResponseReturnValue" ]],
5662 instance : flask .app .Flask ,
@@ -70,7 +76,7 @@ def handle_user_exception_with_instana(
7076 if isinstance (response , tuple ):
7177 status_code = response [1 ]
7278 else :
73- if hasattr (response , ' code' ):
79+ if hasattr (response , " code" ):
7480 status_code = response .code
7581 else :
7682 status_code = response .status_code
@@ -80,12 +86,99 @@ def handle_user_exception_with_instana(
8086
8187 span .set_attribute (SpanAttributes .HTTP_STATUS_CODE , int (status_code ))
8288
83- if hasattr (response , 'headers' ):
89+ if hasattr (response , "headers" ):
90+ tracer = get_tracer ()
8491 tracer .inject (span .context , Format .HTTP_HEADERS , response .headers )
8592 if span and span .is_recording ():
8693 span .end ()
8794 flask .g .span = None
88- except :
95+ except Exception :
8996 logger .debug ("handle_user_exception_with_instana:" , exc_info = True )
9097
9198 return response
99+
100+
101+ def create_span ():
102+ env = flask .request .environ
103+ tracer = get_tracer ()
104+ span_context = tracer .extract (Format .HTTP_HEADERS , env )
105+
106+ span = tracer .start_span ("wsgi" , span_context = span_context )
107+ flask .g .span = span
108+
109+ ctx = trace .set_span_in_context (span )
110+ token = context .attach (ctx )
111+ flask .g .token = token
112+
113+ extract_custom_headers (span , env , format = True )
114+
115+ span .set_attribute (SpanAttributes .HTTP_METHOD , flask .request .method )
116+ if "PATH_INFO" in env :
117+ span .set_attribute (SpanAttributes .HTTP_URL , env ["PATH_INFO" ])
118+ if "QUERY_STRING" in env and len (env ["QUERY_STRING" ]):
119+ scrubbed_params = strip_secrets_from_query (
120+ env ["QUERY_STRING" ],
121+ agent .options .secrets_matcher ,
122+ agent .options .secrets_list ,
123+ )
124+ span .set_attribute ("http.params" , scrubbed_params )
125+ if "HTTP_HOST" in env :
126+ span .set_attribute ("http.host" , env ["HTTP_HOST" ])
127+
128+ if hasattr (flask .request .url_rule , "rule" ) and path_tpl_re .search (
129+ flask .request .url_rule .rule
130+ ):
131+ path_tpl = flask .request .url_rule .rule .replace ("<" , "{" )
132+ path_tpl = path_tpl .replace (">" , "}" )
133+ span .set_attribute ("http.path_tpl" , path_tpl )
134+
135+
136+ def inject_span (
137+ response : flask .wrappers .Response ,
138+ error_message : str ,
139+ set_flask_g_none : bool = False ,
140+ ):
141+ span = None
142+ try :
143+ # If we're not tracing, just return
144+ if not hasattr (flask .g , "span" ):
145+ return response
146+
147+ span = flask .g .span
148+ if span :
149+ if 500 <= response .status_code :
150+ span .mark_as_errored ()
151+
152+ span .set_attribute (
153+ SpanAttributes .HTTP_STATUS_CODE , int (response .status_code )
154+ )
155+ extract_custom_headers (span , response .headers , format = False )
156+ tracer = get_tracer ()
157+ tracer .inject (span .context , Format .HTTP_HEADERS , response .headers )
158+ except Exception :
159+ logger .debug (error_message , exc_info = True )
160+ finally :
161+ if span and span .is_recording ():
162+ span .end ()
163+ if set_flask_g_none :
164+ flask .g .span = None
165+
166+
167+ def teardown_request_with_instana (* argv : Union [Exception , Type [Exception ]]) -> None :
168+ """
169+ In the case of exceptions, after_request_with_instana isn't called
170+ so we capture those cases here.
171+ """
172+ if hasattr (flask .g , "span" ) and flask .g .span :
173+ if len (argv ) > 0 and argv [0 ]:
174+ span = flask .g .span
175+ span .record_exception (argv [0 ])
176+ if SpanAttributes .HTTP_STATUS_CODE not in span .attributes :
177+ span .set_attribute (SpanAttributes .HTTP_STATUS_CODE , 500 )
178+ if flask .g .span .is_recording ():
179+ flask .g .span .end ()
180+ flask .g .span = None
181+
182+ if hasattr (flask .g , "token" ) and flask .g .token :
183+ context .detach (flask .g .token )
184+ flask .g .token = None
0 commit comments