Skip to content

Commit 859e030

Browse files
committed
FastAPI backend including tests
1 parent a1057e3 commit 859e030

File tree

6 files changed

+404
-1
lines changed

6 files changed

+404
-1
lines changed

example/fastapi_sample_app.py

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import logging
2+
3+
import fastapi
4+
5+
import json_logging
6+
7+
app = fastapi.FastAPI()
8+
9+
# init the logger as usual
10+
logger = logging.getLogger(__name__)
11+
12+
@app.get('/')
13+
async def home():
14+
logger.info("test log statement")
15+
logger.info("test log statement with extra props", extra={'props': {"extra_property": 'extra_value'}})
16+
correlation_id = json_logging.get_correlation_id()
17+
return "hello world" \
18+
"\ncorrelation_id : " + correlation_id
19+
20+
21+
@app.get('/exception')
22+
def exception():
23+
try:
24+
raise RuntimeError
25+
except BaseException as e:
26+
logger.error("Error occurred", exc_info=e)
27+
logger.exception("Error occurred", exc_info=e)
28+
return "Error occurred, check log for detail"
29+
30+
31+
@app.get('/exclude_from_request_instrumentation')
32+
def exclude_from_request_instrumentation():
33+
return "this request wont log request instrumentation information"
34+
35+
36+
if __name__ == "__main__":
37+
import uvicorn
38+
logging_config = {
39+
'version': 1,
40+
'disable_existing_loggers': False,
41+
'handlers': {
42+
'default_handler': {
43+
'class': 'logging.StreamHandler',
44+
'level': 'DEBUG',
45+
},
46+
},
47+
'loggers': {
48+
'': {
49+
'handlers': ['default_handler'],
50+
}
51+
}
52+
}
53+
json_logging.init_fastapi(enable_json=True)
54+
json_logging.init_request_instrument(app, exclude_url_patterns=[r'^/exclude_from_request_instrumentation'])
55+
uvicorn.run(app, host='0.0.0.0', port=5000, log_level="debug", log_config=logging_config)

json_logging/__init__.py

+12
Original file line numberDiff line numberDiff line change
@@ -395,3 +395,15 @@ def init_quart(custom_formatter=None, enable_json=False):
395395

396396
def init_connexion(custom_formatter=None, enable_json=False):
397397
__init(framework_name='connexion', custom_formatter=custom_formatter, enable_json=enable_json)
398+
399+
400+
# register FastAPI support
401+
import json_logging.framework.fastapi as fastapi_support
402+
403+
register_framework_support('fastapi', app_configurator=None,
404+
app_request_instrumentation_configurator=fastapi_support.FastAPIAppRequestInstrumentationConfigurator,
405+
request_adapter_class=fastapi_support.FastAPIRequestAdapter,
406+
response_adapter_class=fastapi_support.FastAPIResponseAdapter)
407+
408+
def init_fastapi(custom_formatter=None, enable_json=False):
409+
__init(framework_name='fastapi', custom_formatter=custom_formatter, enable_json=enable_json)
+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# coding=utf-8
2+
import logging
3+
4+
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
5+
from starlette.requests import Request
6+
from starlette.responses import Response
7+
from starlette.types import ASGIApp
8+
9+
import json_logging
10+
import json_logging.framework
11+
from json_logging.framework_base import AppRequestInstrumentationConfigurator, RequestAdapter, ResponseAdapter
12+
13+
from json_logging.util import is_not_match_any_pattern
14+
15+
16+
def is_fastapi_present():
17+
# noinspection PyPep8,PyBroadException
18+
try:
19+
import fastapi
20+
import starlette
21+
return True
22+
except:
23+
return False
24+
25+
26+
if is_fastapi_present():
27+
import fastapi
28+
import starlette.requests
29+
import starlette.responses
30+
31+
32+
class JSONLoggingASGIMiddleware(BaseHTTPMiddleware):
33+
def __init__(self, app: ASGIApp, exclude_url_patterns=tuple()) -> None:
34+
super().__init__(app)
35+
self.request_logger = logging.getLogger('fastapi-request-logger')
36+
self.exclude_url_patterns = exclude_url_patterns
37+
logging.getLogger("uvicorn.access").propagate = False
38+
39+
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
40+
log_request = is_not_match_any_pattern(request.url.path, self.exclude_url_patterns)
41+
42+
if not log_request:
43+
return await call_next(request)
44+
45+
request_info = json_logging.RequestInfo(request)
46+
response = await call_next(request)
47+
request_info.update_response_status(response)
48+
self.request_logger.info(
49+
"", extra={"request_info": request_info, "type": "request"}
50+
)
51+
return response
52+
53+
54+
class FastAPIAppRequestInstrumentationConfigurator(AppRequestInstrumentationConfigurator):
55+
def config(self, app, exclude_url_patterns=tuple()):
56+
if not is_fastapi_present():
57+
raise RuntimeError("fastapi is not available in system runtime")
58+
if not isinstance(app, fastapi.FastAPI):
59+
raise RuntimeError("app is not a valid fastapi.FastAPI instance")
60+
61+
# Disable standard logging
62+
logging.getLogger('uvicorn.access').disabled = True
63+
64+
# noinspection PyAttributeOutsideInit
65+
self.request_logger = logging.getLogger('fastapi-request-logger')
66+
67+
app.add_middleware(JSONLoggingASGIMiddleware, exclude_url_patterns=exclude_url_patterns)
68+
69+
70+
class FastAPIRequestAdapter(RequestAdapter):
71+
@staticmethod
72+
def get_request_class_type():
73+
return starlette.requests.Request
74+
75+
@staticmethod
76+
def support_global_request_object():
77+
return False
78+
79+
@staticmethod
80+
def get_current_request():
81+
raise NotImplementedError
82+
83+
def get_remote_user(self, request: starlette.requests.Request):
84+
try:
85+
return request.user
86+
except AssertionError:
87+
return json_logging.EMPTY_VALUE
88+
89+
def get_http_header(self, request: starlette.requests.Request, header_name, default=None):
90+
try:
91+
if header_name in request.headers:
92+
return request.headers.get(header_name)
93+
except:
94+
pass
95+
return default
96+
97+
def set_correlation_id(self, request_, value):
98+
request_.state.correlation_id = value
99+
100+
def get_correlation_id_in_request_context(self, request: starlette.requests.Request):
101+
try:
102+
return request.state.correlation_id
103+
except AttributeError:
104+
return None
105+
106+
def get_protocol(self, request: starlette.requests.Request):
107+
protocol = str(request.scope.get('type', ''))
108+
http_version = str(request.scope.get('http_version', ''))
109+
if protocol.lower() == 'http' and http_version:
110+
return protocol.upper() + "/" + http_version
111+
return json_logging.EMPTY_VALUE
112+
113+
def get_path(self, request: starlette.requests.Request):
114+
return request.url.path
115+
116+
def get_content_length(self, request: starlette.requests.Request):
117+
return request.headers.get('content-length', json_logging.EMPTY_VALUE)
118+
119+
def get_method(self, request: starlette.requests.Request):
120+
return request.method
121+
122+
def get_remote_ip(self, request: starlette.requests.Request):
123+
return request.client.host
124+
125+
def get_remote_port(self, request: starlette.requests.Request):
126+
return request.client.port
127+
128+
129+
class FastAPIResponseAdapter(ResponseAdapter):
130+
def get_status_code(self, response: starlette.responses.Response):
131+
return response.status_code
132+
133+
def get_response_size(self, response: starlette.responses.Response):
134+
return response.headers.get('content-length', json_logging.EMPTY_VALUE)
135+
136+
def get_content_type(self, response: starlette.responses.Response):
137+
return response.headers.get('content-type', json_logging.EMPTY_VALUE)

test-requirements.txt

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ flask
33
connexion[swagger-ui]
44
quart
55
sanic
6+
fastapi
7+
uvicorn
8+
requests
69
flake8
710
pytest
811
-e .

0 commit comments

Comments
 (0)