22
22
import fastapi
23
23
from fastapi .middleware .httpsredirect import HTTPSRedirectMiddleware
24
24
from fastapi .responses import JSONResponse
25
+ from fastapi .routing import APIRoute
25
26
from fastapi .testclient import TestClient
27
+ from starlette .routing import Match
28
+ from starlette .types import Receive , Scope , Send
26
29
27
30
import opentelemetry .instrumentation .fastapi as otel_fastapi
28
31
from opentelemetry import trace
38
41
from opentelemetry .instrumentation .auto_instrumentation ._load import (
39
42
_load_instrumentors ,
40
43
)
41
- from opentelemetry .instrumentation .dependencies import (
42
- DependencyConflict ,
43
- )
44
+ from opentelemetry .instrumentation .dependencies import DependencyConflict
44
45
from opentelemetry .sdk .metrics .export import (
45
46
HistogramDataPoint ,
46
47
NumberDataPoint ,
123
124
)
124
125
125
126
127
+ class CustomMiddleware :
128
+ def __init__ (self , app : fastapi .FastAPI ) -> None :
129
+ self .app = app
130
+
131
+ async def __call__ (
132
+ self , scope : Scope , receive : Receive , send : Send
133
+ ) -> None :
134
+ scope ["nonstandard_field" ] = "here"
135
+ await self .app (scope , receive , send )
136
+
137
+
138
+ class CustomRoute (APIRoute ):
139
+ def matches (self , scope : Scope ) -> tuple [Match , Scope ]:
140
+ assert "nonstandard_field" in scope
141
+ return super ().matches (scope )
142
+
143
+
126
144
class TestBaseFastAPI (TestBase ):
127
145
def _create_app (self ):
128
146
app = self ._create_fastapi_app ()
@@ -183,6 +201,7 @@ def setUp(self):
183
201
self ._instrumentor = otel_fastapi .FastAPIInstrumentor ()
184
202
self ._app = self ._create_app ()
185
203
self ._app .add_middleware (HTTPSRedirectMiddleware )
204
+ self ._app .add_middleware (CustomMiddleware )
186
205
self ._client = TestClient (self ._app , base_url = "https://testserver:443" )
187
206
# run the lifespan, initialize the middleware stack
188
207
# this is more in-line with what happens in a real application when the server starts up
@@ -202,6 +221,7 @@ def tearDown(self):
202
221
def _create_fastapi_app ():
203
222
app = fastapi .FastAPI ()
204
223
sub_app = fastapi .FastAPI ()
224
+ custom_router = fastapi .APIRouter (route_class = CustomRoute )
205
225
206
226
@sub_app .get ("/home" )
207
227
async def _ ():
@@ -227,6 +247,12 @@ async def _():
227
247
async def _ ():
228
248
raise UnhandledException ("This is an unhandled exception" )
229
249
250
+ @custom_router .get ("/success" )
251
+ async def _ ():
252
+ return None
253
+
254
+ app .include_router (custom_router , prefix = "/custom-router" )
255
+
230
256
app .mount ("/sub" , app = sub_app )
231
257
232
258
return app
@@ -304,6 +330,14 @@ def test_sub_app_fastapi_call(self):
304
330
span .attributes [HTTP_URL ],
305
331
)
306
332
333
+ def test_custom_api_router (self ):
334
+ """
335
+ This test is to ensure that custom API routers the OpenTelemetryMiddleware does not cause issues with
336
+ custom API routers that depend on non-standard fields on the ASGI scope.
337
+ """
338
+ resp = self ._client .get ("/custom-router/success" )
339
+ self .assertEqual (resp .status_code , 200 )
340
+
307
341
308
342
class TestBaseAutoFastAPI (TestBaseFastAPI ):
309
343
@classmethod
@@ -988,6 +1022,7 @@ def test_metric_uninstrument(self):
988
1022
def _create_fastapi_app ():
989
1023
app = fastapi .FastAPI ()
990
1024
sub_app = fastapi .FastAPI ()
1025
+ custom_router = fastapi .APIRouter (route_class = CustomRoute )
991
1026
992
1027
@sub_app .get ("/home" )
993
1028
async def _ ():
@@ -1013,6 +1048,12 @@ async def _():
1013
1048
async def _ ():
1014
1049
raise UnhandledException ("This is an unhandled exception" )
1015
1050
1051
+ @custom_router .get ("/success" )
1052
+ async def _ ():
1053
+ return None
1054
+
1055
+ app .include_router (custom_router , prefix = "/custom-router" )
1056
+
1016
1057
app .mount ("/sub" , app = sub_app )
1017
1058
1018
1059
return app
0 commit comments