|
13 | 13 | # limitations under the License.
|
14 | 14 |
|
15 | 15 | import unittest
|
| 16 | +from timeit import default_timer |
16 | 17 | from unittest.mock import patch
|
17 | 18 |
|
18 | 19 | import fastapi
|
|
22 | 23 | import opentelemetry.instrumentation.fastapi as otel_fastapi
|
23 | 24 | from opentelemetry import trace
|
24 | 25 | from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
|
| 26 | +from opentelemetry.sdk.metrics.export import ( |
| 27 | + HistogramDataPoint, |
| 28 | + NumberDataPoint, |
| 29 | +) |
25 | 30 | from opentelemetry.sdk.resources import Resource
|
26 | 31 | from opentelemetry.semconv.trace import SpanAttributes
|
27 | 32 | from opentelemetry.test.globals_test import reset_trace_globals
|
28 | 33 | from opentelemetry.test.test_base import TestBase
|
29 | 34 | from opentelemetry.util.http import (
|
30 | 35 | OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
|
31 | 36 | OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
|
| 37 | + _active_requests_count_attrs, |
| 38 | + _duration_attrs, |
32 | 39 | get_excluded_urls,
|
33 | 40 | )
|
34 | 41 |
|
| 42 | +_expected_metric_names = [ |
| 43 | + "http.server.active_requests", |
| 44 | + "http.server.duration", |
| 45 | +] |
| 46 | +_recommended_attrs = { |
| 47 | + "http.server.active_requests": _active_requests_count_attrs, |
| 48 | + "http.server.duration": _duration_attrs, |
| 49 | +} |
| 50 | + |
35 | 51 |
|
36 | 52 | class TestFastAPIManualInstrumentation(TestBase):
|
37 | 53 | def _create_app(self):
|
@@ -161,6 +177,124 @@ def test_fastapi_excluded_urls_not_env(self):
|
161 | 177 | spans = self.memory_exporter.get_finished_spans()
|
162 | 178 | self.assertEqual(len(spans), 0)
|
163 | 179 |
|
| 180 | + def test_fastapi_metrics(self): |
| 181 | + self._client.get("/foobar") |
| 182 | + self._client.get("/foobar") |
| 183 | + self._client.get("/foobar") |
| 184 | + metrics_list = self.memory_metrics_reader.get_metrics_data() |
| 185 | + number_data_point_seen = False |
| 186 | + histogram_data_point_seen = False |
| 187 | + self.assertTrue(len(metrics_list.resource_metrics) == 1) |
| 188 | + for resource_metric in metrics_list.resource_metrics: |
| 189 | + self.assertTrue(len(resource_metric.scope_metrics) == 1) |
| 190 | + for scope_metric in resource_metric.scope_metrics: |
| 191 | + self.assertTrue(len(scope_metric.metrics) == 2) |
| 192 | + for metric in scope_metric.metrics: |
| 193 | + self.assertIn(metric.name, _expected_metric_names) |
| 194 | + data_points = list(metric.data.data_points) |
| 195 | + self.assertEqual(len(data_points), 1) |
| 196 | + for point in data_points: |
| 197 | + if isinstance(point, HistogramDataPoint): |
| 198 | + self.assertEqual(point.count, 3) |
| 199 | + histogram_data_point_seen = True |
| 200 | + if isinstance(point, NumberDataPoint): |
| 201 | + number_data_point_seen = True |
| 202 | + for attr in point.attributes: |
| 203 | + self.assertIn( |
| 204 | + attr, _recommended_attrs[metric.name] |
| 205 | + ) |
| 206 | + self.assertTrue(number_data_point_seen and histogram_data_point_seen) |
| 207 | + |
| 208 | + def test_basic_metric_success(self): |
| 209 | + start = default_timer() |
| 210 | + self._client.get("/foobar") |
| 211 | + duration = max(round((default_timer() - start) * 1000), 0) |
| 212 | + expected_duration_attributes = { |
| 213 | + "http.method": "GET", |
| 214 | + "http.host": "testserver", |
| 215 | + "http.scheme": "http", |
| 216 | + "http.flavor": "1.1", |
| 217 | + "http.server_name": "testserver", |
| 218 | + "net.host.port": 80, |
| 219 | + "http.status_code": 200, |
| 220 | + } |
| 221 | + expected_requests_count_attributes = { |
| 222 | + "http.method": "GET", |
| 223 | + "http.host": "testserver", |
| 224 | + "http.scheme": "http", |
| 225 | + "http.flavor": "1.1", |
| 226 | + "http.server_name": "testserver", |
| 227 | + } |
| 228 | + metrics_list = self.memory_metrics_reader.get_metrics_data() |
| 229 | + for metric in ( |
| 230 | + metrics_list.resource_metrics[0].scope_metrics[0].metrics |
| 231 | + ): |
| 232 | + for point in list(metric.data.data_points): |
| 233 | + if isinstance(point, HistogramDataPoint): |
| 234 | + self.assertDictEqual( |
| 235 | + expected_duration_attributes, |
| 236 | + dict(point.attributes), |
| 237 | + ) |
| 238 | + self.assertEqual(point.count, 1) |
| 239 | + self.assertAlmostEqual(duration, point.sum, delta=20) |
| 240 | + if isinstance(point, NumberDataPoint): |
| 241 | + self.assertDictEqual( |
| 242 | + expected_requests_count_attributes, |
| 243 | + dict(point.attributes), |
| 244 | + ) |
| 245 | + self.assertEqual(point.value, 0) |
| 246 | + |
| 247 | + def test_basic_post_request_metric_success(self): |
| 248 | + start = default_timer() |
| 249 | + self._client.post("/foobar") |
| 250 | + duration = max(round((default_timer() - start) * 1000), 0) |
| 251 | + metrics_list = self.memory_metrics_reader.get_metrics_data() |
| 252 | + for metric in ( |
| 253 | + metrics_list.resource_metrics[0].scope_metrics[0].metrics |
| 254 | + ): |
| 255 | + for point in list(metric.data.data_points): |
| 256 | + if isinstance(point, HistogramDataPoint): |
| 257 | + self.assertEqual(point.count, 1) |
| 258 | + self.assertAlmostEqual(duration, point.sum, delta=30) |
| 259 | + if isinstance(point, NumberDataPoint): |
| 260 | + self.assertEqual(point.value, 0) |
| 261 | + |
| 262 | + def test_metric_uninstruemnt_app(self): |
| 263 | + self._client.get("/foobar") |
| 264 | + self._instrumentor.uninstrument_app(self._app) |
| 265 | + self._client.get("/foobar") |
| 266 | + metrics_list = self.memory_metrics_reader.get_metrics_data() |
| 267 | + for metric in ( |
| 268 | + metrics_list.resource_metrics[0].scope_metrics[0].metrics |
| 269 | + ): |
| 270 | + for point in list(metric.data.data_points): |
| 271 | + if isinstance(point, HistogramDataPoint): |
| 272 | + self.assertEqual(point.count, 1) |
| 273 | + if isinstance(point, NumberDataPoint): |
| 274 | + self.assertEqual(point.value, 0) |
| 275 | + |
| 276 | + def test_metric_uninstrument(self): |
| 277 | + # instrumenting class and creating app to send request |
| 278 | + self._instrumentor.instrument() |
| 279 | + app = self._create_fastapi_app() |
| 280 | + client = TestClient(app) |
| 281 | + client.get("/foobar") |
| 282 | + # uninstrumenting class and creating the app again |
| 283 | + self._instrumentor.uninstrument() |
| 284 | + app = self._create_fastapi_app() |
| 285 | + client = TestClient(app) |
| 286 | + client.get("/foobar") |
| 287 | + |
| 288 | + metrics_list = self.memory_metrics_reader.get_metrics_data() |
| 289 | + for metric in ( |
| 290 | + metrics_list.resource_metrics[0].scope_metrics[0].metrics |
| 291 | + ): |
| 292 | + for point in list(metric.data.data_points): |
| 293 | + if isinstance(point, HistogramDataPoint): |
| 294 | + self.assertEqual(point.count, 1) |
| 295 | + if isinstance(point, NumberDataPoint): |
| 296 | + self.assertEqual(point.value, 0) |
| 297 | + |
164 | 298 | @staticmethod
|
165 | 299 | def _create_fastapi_app():
|
166 | 300 | app = fastapi.FastAPI()
|
@@ -274,6 +408,14 @@ def test_request(self):
|
274 | 408 | self.assertEqual(span.resource.attributes["key1"], "value1")
|
275 | 409 | self.assertEqual(span.resource.attributes["key2"], "value2")
|
276 | 410 |
|
| 411 | + def test_mulitple_way_instrumentation(self): |
| 412 | + self._instrumentor.instrument_app(self._app) |
| 413 | + count = 0 |
| 414 | + for middleware in self._app.user_middleware: |
| 415 | + if middleware.cls is OpenTelemetryMiddleware: |
| 416 | + count += 1 |
| 417 | + self.assertEqual(count, 1) |
| 418 | + |
277 | 419 | def tearDown(self):
|
278 | 420 | self._instrumentor.uninstrument()
|
279 | 421 | super().tearDown()
|
|
0 commit comments