Skip to content
This repository was archived by the owner on Jun 13, 2023. It is now read-only.

Commit 857cf86

Browse files
authored
feat(fastapi): Adding custom api routes support (#347)
1 parent d58e848 commit 857cf86

3 files changed

Lines changed: 55 additions & 40 deletions

File tree

epsagon/modules/fastapi.py

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,13 @@
44

55
from __future__ import absolute_import
66
import wrapt
7-
from fastapi.routing import APIRoute
87
from ..wrappers.fastapi import (
9-
TracingAPIRoute,
108
exception_handler_wrapper,
119
server_call_wrapper,
10+
route_class_wrapper,
1211
)
13-
from ..utils import is_lambda_env, print_debug
12+
from ..utils import is_lambda_env
1413

15-
def _wrapper(wrapped, _instance, args, kwargs):
16-
"""
17-
Adds TracingRoute into APIRouter (FastAPI).
18-
:param wrapped: wrapt's wrapped
19-
:param instance: wrapt's instance
20-
:param args: wrapt's args
21-
:param kwargs: wrapt's kwargs
22-
"""
23-
24-
# Skip on Lambda environment since it's not relevant and might be duplicate
25-
if is_lambda_env():
26-
return wrapped(*args, **kwargs)
27-
route_class = kwargs.get('route_class', APIRoute)
28-
if route_class != APIRoute:
29-
# custom routes are not supported
30-
print_debug(
31-
f'Custom FastAPI route {route_class.__name__} is not supported'
32-
)
33-
return wrapped(*args, **kwargs)
34-
kwargs['route_class'] = TracingAPIRoute
35-
return wrapped(*args, **kwargs)
3614

3715
def _exception_handler_wrapper(wrapped, _instance, args, kwargs):
3816
"""
@@ -58,9 +36,9 @@ def patch():
5836
:return: None
5937
"""
6038
wrapt.wrap_function_wrapper(
61-
'fastapi',
62-
'APIRouter.__init__',
63-
_wrapper
39+
'fastapi.routing',
40+
'APIRoute.__init__',
41+
route_class_wrapper
6442
)
6543
wrapt.wrap_function_wrapper(
6644
'starlette.applications',

epsagon/wrappers/fastapi.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import asyncio
99

1010
import warnings
11-
from fastapi.routing import APIRoute
1211
from fastapi import Request, Response
1312
from starlette.requests import ClientDisconnect
1413
from starlette.concurrency import run_in_threadpool
@@ -248,21 +247,27 @@ def wrapped_handler(*args, **kwargs):
248247
dependant.call = wrapped_handler
249248

250249

251-
class TracingAPIRoute(APIRoute):
250+
def route_class_wrapper(wrapped, instance, args, kwargs):
252251
"""
253-
Custom tracing route - traces each route request & response
252+
Route class wrapper - traces each route request & response.
253+
:param wrapped: wrapt's wrapped
254+
:param instance: wrapt's instance
255+
:param args: wrapt's args
256+
:param kwargs: wrapt's kwargs
254257
"""
258+
result = wrapped(*args, **kwargs)
259+
# Skip on Lambda environment since it's not relevant and might be duplicate
260+
if not is_lambda_env() and instance:
261+
try:
262+
if instance.dependant and instance.dependant.call:
263+
_wrap_handler(
264+
instance.dependant,
265+
kwargs.get('status_code', DEFAULT_SUCCESS_STATUS_CODE)
266+
)
267+
except Exception: # pylint: disable=broad-except
268+
pass
255269

256-
def __init__(self, *args, **kwargs):
257-
"""
258-
wraps the route endpoint with Epsagon wrapper
259-
"""
260-
super().__init__(*args, **kwargs)
261-
if self.dependant and self.dependant.call:
262-
_wrap_handler(
263-
self.dependant,
264-
kwargs.pop('status_code', DEFAULT_SUCCESS_STATUS_CODE)
265-
)
270+
return result
266271

267272

268273
def exception_handler_wrapper(original_handler):

tests/wrappers/test_fastapi_wrapper.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from httpx import AsyncClient
1010
from pydantic import BaseModel
1111
from fastapi import FastAPI, APIRouter, Request
12+
from fastapi.routing import APIRoute
1213
from fastapi.responses import JSONResponse
1314
from fastapi.encoders import jsonable_encoder
1415
from epsagon import trace_factory
@@ -22,9 +23,12 @@
2223

2324
RETURN_VALUE = 'testresponsedata'
2425
ROUTER_RETURN_VALUE = 'router-endpoint-return-data'
26+
CUSTOM_ROUTE_RETURN_VALUE = 'custom-route-endpoint-return-data'
2527
REQUEST_OBJ_PATH = '/given_request'
2628
TEST_ROUTER_PREFIX = '/test-router-path'
2729
TEST_ROUTER_PATH = '/test-router'
30+
TEST_CUSTOM_ROUTE_PATH = '/atest-custom-route'
31+
TEST_CUSTOM_ROUTE_PREFIX = '/test-custom-route-main'
2832
MULTIPLE_THREADS_KEY = "multiple_threads"
2933
MULTIPLE_THREADS_ROUTE = f'/{MULTIPLE_THREADS_KEY}'
3034
MULTIPLE_THREADS_RETURN_VALUE = MULTIPLE_THREADS_KEY
@@ -44,6 +48,9 @@
4448
class CustomBaseModel(BaseModel):
4549
data: List[str]
4650

51+
class CustomRouteClass(APIRoute):
52+
pass
53+
4754
def _get_response_data(key):
4855
return {key: key}
4956

@@ -87,6 +94,9 @@ def handle_b():
8794
def handle_router_endpoint():
8895
return _get_response(ROUTER_RETURN_VALUE)
8996

97+
def handle_custom_route_endpoint():
98+
return _get_response(CUSTOM_ROUTE_RETURN_VALUE)
99+
90100
def multiple_threads_route():
91101
multiple_threads_handler()
92102
return _get_response(MULTIPLE_THREADS_RETURN_VALUE)
@@ -162,6 +172,9 @@ def _build_fastapi_app():
162172
router = APIRouter()
163173
router.add_api_route(TEST_ROUTER_PATH, handle_router_endpoint)
164174
app.include_router(router, prefix=TEST_ROUTER_PREFIX)
175+
router_with_custom_route = APIRouter(route_class=CustomRouteClass)
176+
router_with_custom_route.add_api_route(TEST_CUSTOM_ROUTE_PATH, handle_custom_route_endpoint)
177+
app.include_router(router_with_custom_route, prefix=TEST_CUSTOM_ROUTE_PREFIX)
165178
return app
166179

167180
@pytest.fixture(scope='function', autouse=False)
@@ -343,6 +356,25 @@ async def test_fastapi_custom_router(trace_transport, fastapi_app):
343356
assert response_data == expected_response_data
344357

345358

359+
@pytest.mark.asyncio
360+
async def test_fastapi_custom_api_route(trace_transport, fastapi_app):
361+
"""Custom api route sanity test."""
362+
full_route_path= f'{TEST_CUSTOM_ROUTE_PREFIX}{TEST_CUSTOM_ROUTE_PATH}'
363+
async with AsyncClient(app=fastapi_app, base_url="http://test") as ac:
364+
response = await ac.get(full_route_path)
365+
response_data = response.json()
366+
runner = trace_transport.last_trace.events[0]
367+
assert isinstance(runner, FastapiRunner)
368+
assert runner.resource['name'].startswith('127.0.0.1')
369+
assert runner.resource['metadata']['Path'] == full_route_path
370+
assert runner.resource['metadata']['status_code'] == DEFAULT_SUCCESS_STATUS_CODE
371+
expected_response_data = _get_response_data(CUSTOM_ROUTE_RETURN_VALUE)
372+
assert runner.resource['metadata']['Response Data'] == (
373+
expected_response_data
374+
)
375+
assert response_data == expected_response_data
376+
377+
346378
@pytest.mark.asyncio
347379
async def test_fastapi_exception(trace_transport, fastapi_app):
348380
"""Test when the handler raises an exception."""

0 commit comments

Comments
 (0)