|
13 | 13 | # limitations under the License. |
14 | 14 |
|
15 | 15 |
|
| 16 | +import asyncio |
16 | 17 | from timeit import default_timer |
17 | 18 |
|
| 19 | +import tornado.testing |
| 20 | + |
18 | 21 | from opentelemetry.instrumentation.tornado import TornadoInstrumentor |
19 | 22 | from opentelemetry.sdk.metrics.export import HistogramDataPoint |
20 | 23 |
|
@@ -165,6 +168,78 @@ def test_basic_metrics(self): |
165 | 168 | ), |
166 | 169 | ) |
167 | 170 |
|
| 171 | + @tornado.testing.gen_test |
| 172 | + async def test_metrics_concurrent_requests(self): |
| 173 | + """ |
| 174 | + Test that metrics can handle concurrent requests and calculate in an async-safe way. |
| 175 | + """ |
| 176 | + req1 = self.http_client.fetch(self.get_url("/slow?duration=1.0")) |
| 177 | + req2 = self.http_client.fetch(self.get_url("/async")) |
| 178 | + await asyncio.gather(req1, req2) |
| 179 | + |
| 180 | + metrics = self.get_sorted_metrics() |
| 181 | + self.assertEqual(len(metrics), 7) |
| 182 | + |
| 183 | + client_duration = metrics[0] |
| 184 | + server_duration = metrics[4] |
| 185 | + self.assertEqual(client_duration.name, "http.client.duration") |
| 186 | + self.assertEqual(server_duration.name, "http.server.duration") |
| 187 | + |
| 188 | + # Calculating duration requires tracking state via `_HANDLER_STATE_KEY`, so we want to make sure |
| 189 | + # duration is calculated properly per request, and doesn't affect concurrent requests. |
| 190 | + req1_client_duration_data_point = next( |
| 191 | + dp |
| 192 | + for dp in client_duration.data.data_points |
| 193 | + if "/slow" in dp.attributes.get("http.url") |
| 194 | + ) |
| 195 | + req1_server_duration_data_point = next( |
| 196 | + dp |
| 197 | + for dp in server_duration.data.data_points |
| 198 | + if "/slow" in dp.attributes.get("http.target") |
| 199 | + ) |
| 200 | + req2_client_duration_data_point = next( |
| 201 | + dp |
| 202 | + for dp in client_duration.data.data_points |
| 203 | + if "/async" in dp.attributes.get("http.url") |
| 204 | + ) |
| 205 | + req2_server_duration_data_point = next( |
| 206 | + dp |
| 207 | + for dp in server_duration.data.data_points |
| 208 | + if "/async" in dp.attributes.get("http.target") |
| 209 | + ) |
| 210 | + |
| 211 | + # Server and client durations should be similar (adjusting for msecs vs secs) |
| 212 | + self.assertAlmostEqual( |
| 213 | + abs( |
| 214 | + req1_server_duration_data_point.sum / 1000.0 |
| 215 | + - req1_client_duration_data_point.sum |
| 216 | + ), |
| 217 | + 0.0, |
| 218 | + delta=0.01, |
| 219 | + ) |
| 220 | + self.assertAlmostEqual( |
| 221 | + abs( |
| 222 | + req2_server_duration_data_point.sum / 1000.0 |
| 223 | + - req2_client_duration_data_point.sum |
| 224 | + ), |
| 225 | + 0.0, |
| 226 | + delta=0.01, |
| 227 | + ) |
| 228 | + |
| 229 | + # Make sure duration is roughly equivalent to expected (req1/slow) should be around 1 second |
| 230 | + self.assertAlmostEqual( |
| 231 | + req1_server_duration_data_point.sum / 1000.0, |
| 232 | + 1.0, |
| 233 | + delta=0.1, |
| 234 | + msg="Should have been about 1 second", |
| 235 | + ) |
| 236 | + self.assertAlmostEqual( |
| 237 | + req2_server_duration_data_point.sum, |
| 238 | + 0.0, |
| 239 | + delta=0.1, |
| 240 | + msg="Should have been really short", |
| 241 | + ) |
| 242 | + |
168 | 243 | def test_metric_uninstrument(self): |
169 | 244 | self.fetch("/") |
170 | 245 | TornadoInstrumentor().uninstrument() |
|
0 commit comments