11"""
2- Datadog trace code for flask.
2+ Datadog tracing code for flask.
33
4- Requires a modern version of flask and the `blinker` library (which is a
5- dependency of flask signals) .
4+ Installing the blinker library will allow the tracing middleware to collect
5+ more exception info .
66"""
77
88# stdlib
2121log = logging .getLogger (__name__ )
2222
2323
24+ SPAN_NAME = 'flask.request'
25+
26+
2427class TraceMiddleware (object ):
2528
2629 def __init__ (self , app , tracer , service = "flask" , use_signals = True , distributed_tracing = False ):
2730 self .app = app
2831 self .app .logger .info ("initializing trace middleware" )
2932
30- # save our traces.
3133 self ._tracer = tracer
3234 self ._service = service
3335 self ._use_distributed_tracing = distributed_tracing
36+ self .use_signals = use_signals
3437
3538 self ._tracer .set_service_info (
3639 service = service ,
3740 app = "flask" ,
3841 app_type = AppTypes .web ,
3942 )
4043
41- # warn the user if signals are unavailable (because blinker isn't
42- # installed) if they are asking to use them.
44+ # Install hooks which time requests.
45+ self .app .before_request (self ._before_request )
46+ self .app .after_request (self ._after_request )
47+ self .app .teardown_request (self ._teardown_request )
48+
49+ # Add exception handling signals. This will annotate exceptions that
50+ # are caught and handled in custom user code.
51+ # See https://github.com/DataDog/dd-trace-py/issues/390
4352 if use_signals and not signals .signals_available :
4453 self .app .logger .info (_blinker_not_installed_msg )
4554 self .use_signals = use_signals and signals .signals_available
46-
47- # our signal receivers
48- self ._receivers = []
49-
50- # instrument request timings
5155 timing_signals = {
52- 'request_started' : self ._request_started ,
53- 'request_finished' : self ._request_finished ,
5456 'got_request_exception' : self ._request_exception ,
5557 }
58+ self ._receivers = []
5659 if self .use_signals and _signals_exist (timing_signals ):
5760 self ._connect (timing_signals )
58- else :
59- # Fallback to request hooks. Won't catch exceptions.
60- # handle exceptions.
61- self .app .before_request (self ._before_request )
62- self .app .after_request (self ._after_request )
6361
6462 _patch_render (tracer )
6563
66- def _flask_signals_exist (self , names ):
67- """ Return true if the current version of flask has all of the given
68- signals.
69- """
70- return all (getattr (signals , n , None ) for n in names )
71-
7264 def _connect (self , signal_to_handler ):
7365 connected = True
7466 for name , handler in signal_to_handler .items ():
@@ -85,7 +77,34 @@ def _connect(self, signal_to_handler):
8577 self ._receivers .append (handler )
8678 return connected
8779
88- # common methods
80+ def _before_request (self ):
81+ """ Starts tracing the current request and stores it in the global
82+ request object.
83+ """
84+ self ._start_span ()
85+
86+ def _after_request (self , response ):
87+ """ Runs after the server can process a response. """
88+ try :
89+ self ._process_response (response )
90+ except Exception :
91+ self .app .logger .exception ("error tracing response" )
92+ return response
93+
94+ def _teardown_request (self , exception ):
95+ """ Runs at the end of a request. If there's an unhandled exception, it
96+ will be passed in.
97+ """
98+ # when we teardown the span, ensure we have a clean slate.
99+ span = getattr (g , 'flask_datadog_span' , None )
100+ setattr (g , 'flask_datadog_span' , None )
101+ if not span :
102+ return
103+
104+ try :
105+ self ._finish_span (span , exception = exception )
106+ except Exception :
107+ self .app .logger .exception ("error finishing span" )
89108
90109 def _start_span (self ):
91110 if self ._use_distributed_tracing :
@@ -96,85 +115,76 @@ def _start_span(self):
96115 self ._tracer .context_provider .activate (context )
97116 try :
98117 g .flask_datadog_span = self ._tracer .trace (
99- "flask.request" ,
118+ SPAN_NAME ,
100119 service = self ._service ,
101120 span_type = http .TYPE ,
102121 )
103122 except Exception :
104123 self .app .logger .exception ("error tracing request" )
105124
106- def _finish_span (self , response = None , exception = None ):
107- """ Close and finish the active span if it exists. """
125+ def _process_response (self , response ):
108126 span = getattr (g , 'flask_datadog_span' , None )
109- if span :
110- if span .sampled :
111- error = 0
112- code = response .status_code if response else None
113- method = request .method if request else None
114-
115- # if we didn't get a response, but we did get an exception, set
116- # codes accordingly.
117- if not response and exception :
118- code = 500
119- # The 3 next lines might not be strictly required, since `set_traceback`
120- # also get the exception from the sys.exc_info (and fill the error meta).
121- # Since we aren't sure it always work/for insuring no BC break, keep
122- # these lines which get overridden anyway.
123- error = 1
124- span .set_tag (errors .ERROR_TYPE , type (exception ))
125- span .set_tag (errors .ERROR_MSG , exception )
126- # The provided `exception` object doesn't have a stack trace attached,
127- # so attach the stack trace with `set_traceback`.
128- span .set_traceback ()
129-
130- # the endpoint that matched the request is None if an exception
131- # happened so we fallback to a common resource
132- resource = code if not request .endpoint else request .endpoint
133- span .resource = compat .to_unicode (resource ).lower ()
134- span .set_tag (http .URL , compat .to_unicode (request .base_url or '' ))
135- span .set_tag (http .STATUS_CODE , code )
136- span .set_tag (http .METHOD , method )
137- span .error = error
138- span .finish ()
139- # Clear our span just in case.
140- g .flask_datadog_span = None
141-
142- # Request hook methods
127+ if not (span and span .sampled ):
128+ return
143129
144- def _before_request (self ):
145- """ Starts tracing the current request and stores it in the global
146- request object.
147- """
148- self ._start_span ()
130+ code = response .status_code if response else ''
131+ span .set_tag (http .STATUS_CODE , code )
149132
150- def _after_request (self , response ):
151- """ handles a successful response. """
152- try :
153- self ._finish_span (response = response )
154- except Exception :
155- self .app .logger .exception ("error finishing trace" )
156- finally :
157- return response
158-
159- # signal handling methods
160-
161- def _request_started (self , sender ):
162- self ._start_span ()
133+ def _request_exception (self , * args , ** kwargs ):
134+ exception = kwargs .get ("exception" , None )
135+ span = getattr (g , 'flask_datadog_span' , None )
136+ if span and exception :
137+ _set_error_on_span (span , exception )
163138
164- def _request_finished (self , sender , response , ** kwargs ):
165- try :
166- self ._finish_span (response = response )
167- except Exception :
168- self .app .logger .exception ("error finishing trace" )
169- return response
139+ def _finish_span (self , span , exception = None ):
140+ if not span or not span .sampled :
141+ return
170142
171- def _request_exception (self , * args , ** kwargs ):
172- """ handles an error response. """
173- exception = kwargs .pop ("exception" , None )
143+ code = span .get_tag (http .STATUS_CODE ) or 0
174144 try :
175- self . _finish_span ( exception = exception )
145+ code = int ( code )
176146 except Exception :
177- self .app .logger .exception ("error tracing error" )
147+ code = 0
148+
149+ if exception :
150+ # if the request has already had a code set, don't override it.
151+ code = code or 500
152+ _set_error_on_span (span , exception )
153+
154+ # the endpoint that matched the request is None if an exception
155+ # happened so we fallback to a common resource
156+ span .error = 0 if code < 500 else 1
157+
158+ # the request isn't guaranteed to exist here, so only use it carefully.
159+ method = ''
160+ endpoint = ''
161+ url = ''
162+ if request :
163+ method = request .method
164+ endpoint = request .endpoint or code
165+ url = request .base_url or ''
166+
167+ # Let users specify their own resource in middleware if they so desire.
168+ # See case https://github.com/DataDog/dd-trace-py/issues/353
169+ if span .resource == SPAN_NAME :
170+ resource = endpoint or code
171+ span .resource = compat .to_unicode (resource ).lower ()
172+
173+ span .set_tag (http .URL , compat .to_unicode (url ))
174+ span .set_tag (http .STATUS_CODE , code )
175+ span .set_tag (http .METHOD , method )
176+ span .finish ()
177+
178+ def _set_error_on_span (span , exception ):
179+ # The 3 next lines might not be strictly required, since `set_traceback`
180+ # also get the exception from the sys.exc_info (and fill the error meta).
181+ # Since we aren't sure it always work/for insuring no BC break, keep
182+ # these lines which get overridden anyway.
183+ span .set_tag (errors .ERROR_TYPE , type (exception ))
184+ span .set_tag (errors .ERROR_MSG , exception )
185+ # The provided `exception` object doesn't have a stack trace attached,
186+ # so attach the stack trace with `set_traceback`.
187+ span .set_traceback ()
178188
179189def _patch_render (tracer ):
180190 """ patch flask's render template methods with the given tracer. """
0 commit comments