3
3
"""
4
4
5
5
import re
6
+ from contextlib import ExitStack
6
7
from unittest .mock import Mock , patch
7
8
8
9
import ddt
9
10
import ddtrace
10
11
from django .test import TestCase , override_settings
11
12
12
- from ..middleware import DETECT_ANOMALOUS_TRACE , LOG_ROOT_SPAN , DatadogDiagnosticMiddleware
13
+ from ..middleware import CLOSE_ANOMALOUS_SPANS , DETECT_ANOMALOUS_TRACE , LOG_ROOT_SPAN , DatadogDiagnosticMiddleware
13
14
14
15
15
16
def fake_view (_request ):
@@ -24,7 +25,7 @@ def make_middleware(self):
24
25
"""Make an instance of the middleware with current settings."""
25
26
return DatadogDiagnosticMiddleware (fake_view )
26
27
27
- def run_middleware (self , middleware = None ):
28
+ def run_middleware (self , middleware = None , check_error_state = True ):
28
29
"""Run the middleware using a fake request."""
29
30
if middleware is None :
30
31
middleware = self .make_middleware ()
@@ -36,6 +37,9 @@ def run_middleware(self, middleware=None):
36
37
37
38
middleware .process_view (request , None , None , None )
38
39
40
+ if check_error_state :
41
+ assert middleware .error is False
42
+
39
43
@patch ('edx_arch_experiments.datadog_diagnostics.middleware.log.error' )
40
44
def test_log_diagnostics_error_only_once (self , mock_log_error ):
41
45
"""
@@ -48,8 +52,9 @@ def test_log_diagnostics_error_only_once(self, mock_log_error):
48
52
bad_method = Mock (side_effect = lambda request : 1 / 0 )
49
53
middleware .log_diagnostics = bad_method
50
54
51
- self .run_middleware (middleware )
52
- self .run_middleware (middleware )
55
+ self .run_middleware (middleware , check_error_state = False )
56
+ self .run_middleware (middleware , check_error_state = False )
57
+ assert middleware .error is True
53
58
54
59
# Called twice
55
60
assert len (bad_method .call_args_list ) == 2
@@ -74,6 +79,7 @@ def test_log_diagnostics_error_only_once(self, mock_log_error):
74
79
def test_anomalous_trace (self , enabled , cause_anomaly , mock_log_warning ):
75
80
with (
76
81
patch .object (DETECT_ANOMALOUS_TRACE , 'is_enabled' , return_value = enabled ),
82
+ patch .object (CLOSE_ANOMALOUS_SPANS , 'is_enabled' , return_value = False ),
77
83
patch .object (LOG_ROOT_SPAN , 'is_enabled' , return_value = False ),
78
84
# Need at least two levels of spans in order to fake
79
85
# an anomaly. (Otherwise current_root_span returns None.)
@@ -108,6 +114,7 @@ def test_anomalous_trace_truncation(self, mock_log_warning):
108
114
"""
109
115
with (
110
116
patch .object (DETECT_ANOMALOUS_TRACE , 'is_enabled' , return_value = True ),
117
+ patch .object (CLOSE_ANOMALOUS_SPANS , 'is_enabled' , return_value = False ),
111
118
patch .object (LOG_ROOT_SPAN , 'is_enabled' , return_value = False ),
112
119
# Need at least two levels of spans in order to fake
113
120
# an anomaly. (Otherwise current_root_span returns None.)
@@ -134,6 +141,7 @@ def test_anomalous_trace_truncation(self, mock_log_warning):
134
141
def test_log_root_span (self , mock_log_info ):
135
142
with (
136
143
patch .object (DETECT_ANOMALOUS_TRACE , 'is_enabled' , return_value = False ),
144
+ patch .object (CLOSE_ANOMALOUS_SPANS , 'is_enabled' , return_value = False ),
137
145
patch .object (LOG_ROOT_SPAN , 'is_enabled' , return_value = True ),
138
146
# Need at least two levels of spans for interesting logging
139
147
ddtrace .tracer .trace ("local_root" ),
@@ -149,3 +157,87 @@ def test_log_root_span(self, mock_log_info):
149
157
r"current span = name='inner_span' .*" ,
150
158
log_msg
151
159
)
160
+
161
+ def run_close_with (self , * , enabled , anomalous , ancestors = None ):
162
+ """
163
+ Run a "close anomalous spans" scenario with supplied settings.
164
+
165
+ ancestors is a list of span operation names, defaulting to
166
+ something reasonable if not supplied.
167
+ """
168
+ with (
169
+ patch .object (DETECT_ANOMALOUS_TRACE , 'is_enabled' , return_value = False ),
170
+ patch .object (CLOSE_ANOMALOUS_SPANS , 'is_enabled' , return_value = enabled ),
171
+ patch .object (LOG_ROOT_SPAN , 'is_enabled' , return_value = False ),
172
+ ExitStack () as stack ,
173
+ ):
174
+ if ancestors is None :
175
+ ancestors = [
176
+ 'django.request' , 'django.view' ,
177
+ 'celery.apply' ,
178
+ # ^ will need to close some of these
179
+ 'django.request' , 'django.view' ,
180
+ ]
181
+ for ancestor_name in ancestors :
182
+ stack .enter_context (ddtrace .tracer .trace (ancestor_name ))
183
+ # make anomaly readily detectable
184
+ if anomalous :
185
+ ddtrace .tracer .current_root_span ().finish ()
186
+
187
+ self .run_middleware ()
188
+
189
+ @patch ('edx_arch_experiments.datadog_diagnostics.middleware.log.info' )
190
+ @patch ('edx_arch_experiments.datadog_diagnostics.middleware.log.error' )
191
+ def test_close_disabled (self , mock_log_error , mock_log_info ):
192
+ """
193
+ Confirm that nothing interesting happens when close-spans flag is disabled.
194
+ """
195
+ self .run_close_with (enabled = False , anomalous = True )
196
+
197
+ mock_log_error .assert_not_called ()
198
+ mock_log_info .assert_not_called ()
199
+
200
+ @patch ('edx_arch_experiments.datadog_diagnostics.middleware.log.info' )
201
+ @patch ('edx_arch_experiments.datadog_diagnostics.middleware.log.error' )
202
+ def test_close_applied (self , mock_log_error , mock_log_info ):
203
+ """
204
+ Confirm that anomalous spans are closed, at least for future requests.
205
+ """
206
+ self .run_close_with (enabled = True , anomalous = True )
207
+
208
+ mock_log_error .assert_not_called ()
209
+
210
+ # Expect to close celery.apply and the one above it (but we've
211
+ # already closed the root, above).
212
+ assert len (mock_log_info .call_args_list ) == 2
213
+ assert [call [0 ][0 ].split (' id=' )[0 ] for call in mock_log_info .call_args_list ] == [
214
+ "Closed span in anomalous trace: name=celery.apply" ,
215
+ "Closed span in anomalous trace: name=django.view" ,
216
+ ]
217
+
218
+ @patch ('edx_arch_experiments.datadog_diagnostics.middleware.log.info' )
219
+ @patch ('edx_arch_experiments.datadog_diagnostics.middleware.log.error' )
220
+ def test_close_not_needed (self , mock_log_error , mock_log_info ):
221
+ """
222
+ Confirm that no logging when anomalous trace not present.
223
+ """
224
+ self .run_close_with (enabled = True , anomalous = False )
225
+
226
+ mock_log_error .assert_not_called ()
227
+ mock_log_info .assert_not_called ()
228
+
229
+ @patch ('edx_arch_experiments.datadog_diagnostics.middleware.log.info' )
230
+ @patch ('edx_arch_experiments.datadog_diagnostics.middleware.log.error' )
231
+ def test_close_missing_request (self , mock_log_error , mock_log_info ):
232
+ """
233
+ Check that we look for the expected ancestor and only close above it.
234
+ """
235
+ self .run_close_with (enabled = True , anomalous = True , ancestors = [
236
+ # Artificial scenario standing in for something unexpected.
237
+ 'django.view' , 'celery.apply' , 'django.view' ,
238
+ ])
239
+
240
+ mock_log_error .assert_called_once_with (
241
+ "Did not find django.request span when walking anomalous trace to root. Not attempting a fix."
242
+ )
243
+ mock_log_info .assert_not_called ()
0 commit comments