-
-
Notifications
You must be signed in to change notification settings - Fork 60
Fastapi backend #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fastapi backend #65
Changes from all commits
29f7403
0497f34
a1057e3
859e030
9a4c312
9eda892
15db3c3
94bcdc7
0fd9df0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import logging | ||
|
||
import fastapi | ||
|
||
import json_logging | ||
|
||
app = fastapi.FastAPI() | ||
|
||
# init the logger as usual | ||
logger = logging.getLogger(__name__) | ||
logger.setLevel(logging.DEBUG) | ||
|
||
@app.get('/') | ||
async def home(): | ||
logger.info("test log statement") | ||
logger.info("test log statement with extra props", extra={'props': {"extra_property": 'extra_value'}}) | ||
correlation_id = json_logging.get_correlation_id() | ||
return "hello world" \ | ||
"\ncorrelation_id : " + correlation_id | ||
|
||
|
||
@app.get('/exception') | ||
def exception(): | ||
try: | ||
raise RuntimeError | ||
except BaseException as e: | ||
logger.error("Error occurred", exc_info=e) | ||
logger.exception("Error occurred", exc_info=e) | ||
return "Error occurred, check log for detail" | ||
|
||
|
||
@app.get('/exclude_from_request_instrumentation') | ||
def exclude_from_request_instrumentation(): | ||
return "this request wont log request instrumentation information" | ||
|
||
|
||
if __name__ == "__main__": | ||
import uvicorn | ||
logging_config = { | ||
'version': 1, | ||
'disable_existing_loggers': False, | ||
'handlers': { | ||
'default_handler': { | ||
'class': 'logging.StreamHandler', | ||
'level': 'DEBUG', | ||
}, | ||
}, | ||
'loggers': { | ||
'': { | ||
'handlers': ['default_handler'], | ||
} | ||
} | ||
} | ||
json_logging.init_fastapi(enable_json=True) | ||
json_logging.init_request_instrument(app, exclude_url_patterns=[r'^/exclude_from_request_instrumentation']) | ||
uvicorn.run(app, host='0.0.0.0', port=5000, log_level="debug", log_config=logging_config) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
|
||
def is_fastapi_present(): | ||
# noinspection PyPep8,PyBroadException | ||
try: | ||
import fastapi | ||
import starlette | ||
return True | ||
except: | ||
return False | ||
|
||
|
||
if is_fastapi_present(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this method is declared twice There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, the second one is an if statement using the function. This pattern I adopted from other backends. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. its total fine, somehow i misunderstood it LOL |
||
from .implementation import FastAPIAppRequestInstrumentationConfigurator, FastAPIRequestAdapter, FastAPIResponseAdapter |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import logging | ||
|
||
import json_logging | ||
import json_logging.framework | ||
from json_logging.framework_base import AppRequestInstrumentationConfigurator, RequestAdapter, ResponseAdapter | ||
|
||
from json_logging.util import is_not_match_any_pattern | ||
|
||
import fastapi | ||
import starlette.requests | ||
import starlette.responses | ||
|
||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint | ||
from starlette.requests import Request | ||
from starlette.responses import Response | ||
from starlette.types import ASGIApp | ||
|
||
|
||
class JSONLoggingASGIMiddleware(BaseHTTPMiddleware): | ||
def __init__(self, app: ASGIApp, exclude_url_patterns=tuple()) -> None: | ||
super().__init__(app) | ||
self.request_logger = logging.getLogger('fastapi-request-logger') | ||
self.exclude_url_patterns = exclude_url_patterns | ||
logging.getLogger("uvicorn.access").propagate = False | ||
|
||
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: | ||
log_request = is_not_match_any_pattern(request.url.path, self.exclude_url_patterns) | ||
|
||
if not log_request: | ||
return await call_next(request) | ||
|
||
request_info = json_logging.RequestInfo(request) | ||
response = await call_next(request) | ||
request_info.update_response_status(response) | ||
self.request_logger.info( | ||
"", extra={"request_info": request_info, "type": "request"} | ||
) | ||
return response | ||
|
||
|
||
class FastAPIAppRequestInstrumentationConfigurator(AppRequestInstrumentationConfigurator): | ||
def config(self, app, exclude_url_patterns=tuple()): | ||
if not isinstance(app, fastapi.FastAPI): | ||
raise RuntimeError("app is not a valid fastapi.FastAPI instance") | ||
|
||
# Disable standard logging | ||
logging.getLogger('uvicorn.access').disabled = True | ||
|
||
# noinspection PyAttributeOutsideInit | ||
self.request_logger = logging.getLogger('fastapi-request-logger') | ||
|
||
app.add_middleware(JSONLoggingASGIMiddleware, exclude_url_patterns=exclude_url_patterns) | ||
|
||
|
||
class FastAPIRequestAdapter(RequestAdapter): | ||
@staticmethod | ||
def get_request_class_type(): | ||
return starlette.requests.Request | ||
|
||
@staticmethod | ||
def support_global_request_object(): | ||
return False | ||
|
||
@staticmethod | ||
def get_current_request(): | ||
raise NotImplementedError | ||
|
||
def get_remote_user(self, request: starlette.requests.Request): | ||
try: | ||
return request.user | ||
except AssertionError: | ||
return json_logging.EMPTY_VALUE | ||
|
||
def get_http_header(self, request: starlette.requests.Request, header_name, default=None): | ||
try: | ||
if header_name in request.headers: | ||
return request.headers.get(header_name) | ||
except: | ||
pass | ||
return default | ||
|
||
def set_correlation_id(self, request_, value): | ||
request_.state.correlation_id = value | ||
|
||
def get_correlation_id_in_request_context(self, request: starlette.requests.Request): | ||
try: | ||
return request.state.correlation_id | ||
except AttributeError: | ||
return None | ||
|
||
def get_protocol(self, request: starlette.requests.Request): | ||
protocol = str(request.scope.get('type', '')) | ||
http_version = str(request.scope.get('http_version', '')) | ||
if protocol.lower() == 'http' and http_version: | ||
return protocol.upper() + "/" + http_version | ||
return json_logging.EMPTY_VALUE | ||
|
||
def get_path(self, request: starlette.requests.Request): | ||
return request.url.path | ||
|
||
def get_content_length(self, request: starlette.requests.Request): | ||
return request.headers.get('content-length', json_logging.EMPTY_VALUE) | ||
|
||
def get_method(self, request: starlette.requests.Request): | ||
return request.method | ||
|
||
def get_remote_ip(self, request: starlette.requests.Request): | ||
return request.client.host | ||
|
||
def get_remote_port(self, request: starlette.requests.Request): | ||
return request.client.port | ||
|
||
|
||
class FastAPIResponseAdapter(ResponseAdapter): | ||
def get_status_code(self, response: starlette.responses.Response): | ||
return response.status_code | ||
|
||
def get_response_size(self, response: starlette.responses.Response): | ||
return response.headers.get('content-length', json_logging.EMPTY_VALUE) | ||
|
||
def get_content_type(self, response: starlette.responses.Response): | ||
return response.headers.get('content-type', json_logging.EMPTY_VALUE) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,9 @@ flask | |
connexion[swagger-ui] | ||
quart | ||
sanic | ||
fastapi | ||
uvicorn | ||
requests | ||
flake8 | ||
pytest | ||
-e . |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# Organization of the test folder | ||
|
||
``` | ||
├───helpers Shared functionality for all tests | ||
├───smoketests A test script to run all API examples and see if they work | ||
│ ├───fastapi | ||
│ ├───flask | ||
│ ├───quart | ||
│ └───sanic | ||
└───test_*.py Unit tests | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
"""Helper functions related to module imports""" | ||
import sys | ||
|
||
|
||
def undo_imports_from_package(package: str): | ||
"""Removes all imported modules from the given package from sys.modules""" | ||
for k in sorted(sys.modules.keys(), key=lambda s: len(s), reverse=True): | ||
if k == package or k.startswith(package + '.'): | ||
del sys.modules[k] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import datetime, logging, sys, json_logging, fastapi, uvicorn | ||
|
||
app = fastapi.FastAPI() | ||
json_logging.init_fastapi(enable_json=True) | ||
json_logging.init_request_instrument(app) | ||
|
||
# init the logger as usual | ||
logger = logging.getLogger("test-logger") | ||
logger.setLevel(logging.DEBUG) | ||
logger.addHandler(logging.StreamHandler(sys.stdout)) | ||
|
||
@app.get('/') | ||
def home(): | ||
logger.info("test log statement") | ||
logger.info("test log statement with extra props", extra={'props': {"extra_property": 'extra_value'}}) | ||
correlation_id = json_logging.get_correlation_id() | ||
return "Hello world : " + str(datetime.datetime.now()) | ||
|
||
if __name__ == "__main__": | ||
uvicorn.run(app, host='0.0.0.0', port=5000) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
fastapi | ||
uvicorn | ||
requests | ||
pytest | ||
-e . |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import datetime, logging, sys, json_logging, flask | ||
|
||
app = flask.Flask(__name__) | ||
json_logging.init_flask(enable_json=True) | ||
json_logging.init_request_instrument(app) | ||
|
||
# init the logger as usual | ||
logger = logging.getLogger("test-logger") | ||
logger.setLevel(logging.DEBUG) | ||
logger.addHandler(logging.StreamHandler(sys.stdout)) | ||
|
||
@app.route('/') | ||
def home(): | ||
logger.info("test log statement") | ||
logger.info("test log statement with extra props", extra={'props': {"extra_property": 'extra_value'}}) | ||
correlation_id = json_logging.get_correlation_id() | ||
return "Hello world : " + str(datetime.datetime.now()) | ||
|
||
if __name__ == "__main__": | ||
app.run(host='0.0.0.0', port=int(5000), use_reloader=False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you're missing the logger.setLevel call
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually not necessary. All what's needed is configured further down with a config dict.
I'm doing it that way, so that I can pass this configuration to uvicorn which configures logging itself. When I tried it locally in the script as for the other backends it didn't work out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a bit weird, when i run the sample directly from terminal the logging statement didnt emit anything, only works with setLevel call
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, when you copy the code directly into an IPython shell, you mean? I also tested it and indeed that didn't work without
logger.setLevel()
. I only tested to run the script as dedicated python process (python fastapi_sample_app.py
).But right, with an additional
logger.SetLevel()
it works under all circumstances, so I just added that.