2222from .. import trace_utils
2323from ...constants import ANALYTICS_SAMPLE_RATE_KEY
2424from ...constants import SPAN_MEASURED_KEY
25- from ...contrib .wsgi .wsgi import _DDWSGIMiddlewareBase
2625from ...ext import SpanTypes
2726from ...internal .compat import maybe_stringify
2827from ...internal .logger import get_logger
@@ -90,103 +89,6 @@ class RequestWithJson(werkzeug.Request, JSONMixin):
9089flask_version = parse_version (flask_version_str )
9190
9291
93- class _FlaskWSGIMiddleware (_DDWSGIMiddlewareBase ):
94- _request_span_name = "flask.request"
95- _application_span_name = "flask.application"
96- _response_span_name = "flask.response"
97-
98- def _traced_start_response (self , start_response , span , status_code , headers , exc_info = None ):
99- code , _ , _ = status_code .partition (" " )
100- # If values are accessible, set the resource as `<method> <path>` and add other request tags
101- _set_request_tags (span )
102-
103- # Override root span resource name to be `<method> 404` for 404 requests
104- # DEV: We do this because we want to make it easier to see all unknown requests together
105- # Also, we do this to reduce the cardinality on unknown urls
106- # DEV: If we have an endpoint or url rule tag, then we don't need to do this,
107- # we still want `GET /product/<int:product_id>` grouped together,
108- # even if it is a 404
109- if not span .get_tag (FLASK_ENDPOINT ) and not span .get_tag (FLASK_URL_RULE ):
110- span .resource = u" " .join ((flask .request .method , code ))
111-
112- trace_utils .set_http_meta (span , config .flask , status_code = code , response_headers = headers )
113-
114- return start_response (status_code , headers )
115-
116- def _request_span_modifier (self , span , environ ):
117- # Create a werkzeug request from the `environ` to make interacting with it easier
118- # DEV: This executes before a request context is created
119- request = _RequestType (environ )
120-
121- # Default resource is method and path:
122- # GET /
123- # POST /save
124- # We will override this below in `traced_dispatch_request` when we have a `
125- # RequestContext` and possibly a url rule
126- span .resource = u" " .join ((request .method , request .path ))
127-
128- span .set_tag (SPAN_MEASURED_KEY )
129- # set analytics sample rate with global config enabled
130- sample_rate = config .flask .get_analytics_sample_rate (use_global_config = True )
131- if sample_rate is not None :
132- span .set_tag (ANALYTICS_SAMPLE_RATE_KEY , sample_rate )
133-
134- span ._set_str_tag (FLASK_VERSION , flask_version_str )
135-
136- req_body = None
137- if config ._appsec_enabled and request .method in _BODY_METHODS :
138- content_type = request .content_type
139- wsgi_input = environ .get ("wsgi.input" , "" )
140-
141- # Copy wsgi input if not seekable
142- try :
143- seekable = wsgi_input .seekable ()
144- except AttributeError :
145- seekable = False
146- if not seekable :
147- body = wsgi_input .read ()
148- environ ["wsgi.input" ] = BytesIO (body )
149-
150- try :
151- if content_type == "application/json" :
152- if _HAS_JSON_MIXIN and hasattr (request , "json" ):
153- req_body = request .json
154- else :
155- req_body = json .loads (request .data .decode ("UTF-8" ))
156- elif content_type in ("application/xml" , "text/xml" ):
157- req_body = xmltodict .parse (request .get_data ())
158- elif hasattr (request , "values" ):
159- req_body = request .values .to_dict ()
160- elif hasattr (request , "args" ):
161- req_body = request .args .to_dict ()
162- elif hasattr (request , "form" ):
163- req_body = request .form .to_dict ()
164- else :
165- req_body = request .get_data ()
166- except (AttributeError , RuntimeError , TypeError , BadRequest , ValueError , JSONDecodeError ):
167- log .warning ("Failed to parse werkzeug request body" , exc_info = True )
168- finally :
169- # Reset wsgi input to the beginning
170- if seekable :
171- wsgi_input .seek (0 )
172- else :
173- environ ["wsgi.input" ] = BytesIO (body )
174-
175- trace_utils .set_http_meta (
176- span ,
177- config .flask ,
178- method = request .method ,
179- url = request .base_url ,
180- raw_uri = request .url ,
181- query = request .query_string ,
182- parsed_query = request .args ,
183- request_headers = request .headers ,
184- request_cookies = request .cookies ,
185- request_body = req_body ,
186- peer_ip = request .remote_addr ,
187- )
188-
189-
19092def patch ():
19193 """
19294 Patch `flask` module for tracing
@@ -387,6 +289,32 @@ def unpatch():
387289 _u (obj , prop )
388290
389291
292+ # Wrap the `start_response` handler to extract response code
293+ # DEV: We tried using `Flask.finalize_request`, which seemed to work, but gave us hell during tests
294+ # DEV: The downside to using `start_response` is we do not have a `Flask.Response` object here,
295+ # only `status_code`, and `headers` to work with
296+ # On the bright side, this works in all versions of Flask (or any WSGI app actually)
297+ def _wrap_start_response (func , span , request ):
298+ def traced_start_response (status_code , headers ):
299+ code , _ , _ = status_code .partition (" " )
300+ # If values are accessible, set the resource as `<method> <path>` and add other request tags
301+ _set_request_tags (span )
302+
303+ # Override root span resource name to be `<method> 404` for 404 requests
304+ # DEV: We do this because we want to make it easier to see all unknown requests together
305+ # Also, we do this to reduce the cardinality on unknown urls
306+ # DEV: If we have an endpoint or url rule tag, then we don't need to do this,
307+ # we still want `GET /product/<int:product_id>` grouped together,
308+ # even if it is a 404
309+ if not span .get_tag (FLASK_ENDPOINT ) and not span .get_tag (FLASK_URL_RULE ):
310+ span .resource = u" " .join ((request .method , code ))
311+
312+ trace_utils .set_http_meta (span , config .flask , status_code = code , response_headers = headers )
313+ return func (status_code , headers )
314+
315+ return traced_start_response
316+
317+
390318@with_instance_pin
391319def traced_wsgi_app (pin , wrapped , instance , args , kwargs ):
392320 """
@@ -397,8 +325,88 @@ def traced_wsgi_app(pin, wrapped, instance, args, kwargs):
397325 # DEV: This is safe before this is the args for a WSGI handler
398326 # https://www.python.org/dev/peps/pep-3333/
399327 environ , start_response = args
400- middleware = _FlaskWSGIMiddleware (wrapped , pin .tracer , config .flask , pin )
401- return middleware (environ , start_response )
328+
329+ # Create a werkzeug request from the `environ` to make interacting with it easier
330+ # DEV: This executes before a request context is created
331+ request = _RequestType (environ )
332+
333+ # Configure distributed tracing
334+ trace_utils .activate_distributed_headers (pin .tracer , int_config = config .flask , request_headers = request .headers )
335+
336+ # Default resource is method and path:
337+ # GET /
338+ # POST /save
339+ # We will override this below in `traced_dispatch_request` when we have a `RequestContext` and possibly a url rule
340+ resource = u" " .join ((request .method , request .path ))
341+ with pin .tracer .trace (
342+ "flask.request" ,
343+ service = trace_utils .int_service (pin , config .flask ),
344+ resource = resource ,
345+ span_type = SpanTypes .WEB ,
346+ ) as span :
347+ span .set_tag (SPAN_MEASURED_KEY )
348+ # set analytics sample rate with global config enabled
349+ sample_rate = config .flask .get_analytics_sample_rate (use_global_config = True )
350+ if sample_rate is not None :
351+ span .set_tag (ANALYTICS_SAMPLE_RATE_KEY , sample_rate )
352+
353+ span ._set_str_tag (FLASK_VERSION , flask_version_str )
354+ start_response = _wrap_start_response (start_response , span , request )
355+
356+ req_body = None
357+ if config ._appsec_enabled and request .method in _BODY_METHODS :
358+ content_type = request .content_type
359+ wsgi_input = environ .get ("wsgi.input" , "" )
360+
361+ # Copy wsgi input if not seekable
362+ try :
363+ seekable = wsgi_input .seekable ()
364+ except AttributeError :
365+ seekable = False
366+ if not seekable :
367+ body = wsgi_input .read ()
368+ environ ["wsgi.input" ] = BytesIO (body )
369+
370+ try :
371+ if content_type == "application/json" :
372+ if _HAS_JSON_MIXIN and hasattr (request , "json" ):
373+ req_body = request .json
374+ else :
375+ req_body = json .loads (request .data .decode ("UTF-8" ))
376+ elif content_type in ("application/xml" , "text/xml" ):
377+ req_body = xmltodict .parse (request .get_data ())
378+ elif hasattr (request , "values" ):
379+ req_body = request .values .to_dict ()
380+ elif hasattr (request , "args" ):
381+ req_body = request .args .to_dict ()
382+ elif hasattr (request , "form" ):
383+ req_body = request .form .to_dict ()
384+ else :
385+ req_body = request .get_data ()
386+ except (AttributeError , RuntimeError , TypeError , BadRequest , ValueError , JSONDecodeError ):
387+ log .warning ("Failed to parse werkzeug request body" , exc_info = True )
388+ finally :
389+ # Reset wsgi input to the beginning
390+ if seekable :
391+ wsgi_input .seek (0 )
392+ else :
393+ environ ["wsgi.input" ] = BytesIO (body )
394+
395+ trace_utils .set_http_meta (
396+ span ,
397+ config .flask ,
398+ method = request .method ,
399+ url = request .base_url ,
400+ raw_uri = request .url ,
401+ query = request .query_string ,
402+ parsed_query = request .args ,
403+ request_headers = request .headers ,
404+ request_cookies = request .cookies ,
405+ request_body = req_body ,
406+ peer_ip = request .remote_addr ,
407+ )
408+
409+ return wrapped (environ , start_response )
402410
403411
404412def traced_blueprint_register (wrapped , instance , args , kwargs ):
0 commit comments