Skip to content

Commit 4b00543

Browse files
committed
feat: Add error logger
1 parent 4366caa commit 4b00543

File tree

9 files changed

+106
-29
lines changed

9 files changed

+106
-29
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,6 @@ dmypy.json
134134

135135
# Ruff cache
136136
.ruff_cache
137+
138+
# Logger
139+
errors.log

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
Related documentation and links about this template.
88

9-
[FastAPI](https://fastapi.tiangolo.com/)
9+
[FastAPI](https://fastapi.tiangolo.com/): Modern web framework for building APIs with Python.
1010

11-
[Pydantic](https://pydantic-docs.helpmanual.io/)
11+
[Pydantic](https://pydantic-docs.helpmanual.io/): Data validation library for Python.
1212

13-
[Motor](https://motor.readthedocs.io/en/stable/): Asynchronous Python driver for MongoDB
13+
[Motor](https://motor.readthedocs.io/en/stable/): Asynchronous Python driver for MongoDB.
1414

1515
## Deployment with docker
1616

app/controllers/student_controller.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
@router.post('/student', response_description='Create student', response_model=StudentModel)
1818
async def create_student_data(student: StudentModel) -> JSONResponse:
1919
student_json = jsonable_encoder(student)
20-
if student_json['age'] == 3:
21-
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
2220
new_student = await create_student(student_json)
2321
return JSONResponse(
2422
status_code=status.HTTP_201_CREATED,

app/exception_handlers.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import sys
2+
3+
from fastapi import Request
4+
from fastapi.exception_handlers import http_exception_handler as _http_exception_handler
5+
from fastapi.exception_handlers import (
6+
request_validation_exception_handler as _request_validation_exception_handler,
7+
)
8+
from fastapi.exceptions import HTTPException, RequestValidationError
9+
from fastapi.responses import JSONResponse, PlainTextResponse, Response
10+
11+
from app.logger import logger
12+
13+
14+
async def request_validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
15+
body = await request.body()
16+
query_params = request.query_params._dict
17+
detail = {"errors": exc.errors(), "body": body.decode(), "query_params": query_params}
18+
logger.info(detail)
19+
return await _request_validation_exception_handler(request, exc)
20+
21+
22+
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse | Response:
23+
return await _http_exception_handler(request, exc)
24+
25+
26+
async def unhandled_exception_handler(request: Request, exc: Exception) -> PlainTextResponse:
27+
host = getattr(getattr(request, "client", None), "host", None)
28+
port = getattr(getattr(request, "client", None), "port", None)
29+
url = f"{request.url.path}?{request.query_params}" if request.query_params else request.url.path
30+
exception_type, exception_value, exception_traceback = sys.exc_info()
31+
exception_name = getattr(exception_type, "__name__", None)
32+
logger.error(
33+
f'{host}:{port} - "{request.method} {url}" 500 Internal Server Error <{exception_name}: {exception_value}>'
34+
)
35+
return PlainTextResponse(str(exc), status_code=500)

app/logger.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import logging
2+
3+
uvicorn_access = logging.getLogger("uvicorn.access")
4+
uvicorn_access.disabled = True
5+
6+
logger = logging.getLogger("uvicorn")
7+
logger.setLevel(logging.getLevelName(logging.DEBUG))
8+
9+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
10+
error_file_handler = logging.FileHandler("errors.log")
11+
error_file_handler.setLevel(logging.DEBUG)
12+
error_file_handler.setFormatter(formatter)
13+
logger.addHandler(error_file_handler)

app/main.py

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
from fastapi import FastAPI, Request
2-
from fastapi.exception_handlers import (
3-
http_exception_handler,
4-
request_validation_exception_handler,
5-
)
1+
from contextlib import asynccontextmanager
2+
from typing import Any
3+
4+
from fastapi import FastAPI
65
from fastapi.exceptions import RequestValidationError
76
from starlette.exceptions import HTTPException
87
from starlette.middleware.cors import CORSMiddleware
9-
from starlette.responses import Response
108

9+
from app.exception_handlers import (
10+
http_exception_handler,
11+
request_validation_exception_handler,
12+
unhandled_exception_handler,
13+
)
14+
from app.middleware import log_request_middleware
1115
from app.router import api_router
1216
from app.services.populate_service import students_bulkwrite
1317

@@ -25,34 +29,29 @@
2529
[Motor](https://motor.readthedocs.io/en/stable/): Asynchronous Python driver for MongoDB.
2630
'''
2731

32+
@asynccontextmanager
33+
async def lifespan(app: FastAPI) -> Any:
34+
await students_bulkwrite()
35+
yield
36+
2837
app = FastAPI(
2938
title="FastAPI/MongoDB Template",
3039
description=DESCRIPTION,
31-
version="1.4.0",
40+
version="1.5.0",
3241
contact={
3342
"name": "Antonio Germán Márquez Trujillo",
3443
"url": "https://github.com/GermanMT",
3544
"email": "[email protected]",
36-
}
45+
},
46+
license_info={
47+
"name": "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
48+
"url": "https://www.gnu.org/licenses/gpl-3.0.html",
49+
},
50+
lifespan=lifespan,
3751
)
3852

3953

40-
@app.exception_handler(HTTPException)
41-
async def custom_http_exception_handler(request: Request, exc: HTTPException) -> Response:
42-
return await http_exception_handler(request, exc)
43-
44-
45-
@app.exception_handler(RequestValidationError)
46-
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> Response:
47-
return await request_validation_exception_handler(request, exc)
48-
49-
50-
@app.on_event("startup")
51-
async def startup_event() -> None:
52-
await students_bulkwrite()
53-
54-
55-
# Set all CORS enabled origins
54+
app.middleware("http")(log_request_middleware)
5655
app.add_middleware(
5756
CORSMiddleware,
5857
allow_origins=[],
@@ -62,4 +61,9 @@ async def startup_event() -> None:
6261
)
6362

6463

64+
app.add_exception_handler(RequestValidationError, request_validation_exception_handler)
65+
app.add_exception_handler(HTTPException, http_exception_handler)
66+
app.add_exception_handler(Exception, unhandled_exception_handler)
67+
68+
6569
app.include_router(api_router)

app/middleware.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import http
2+
import time
3+
4+
from fastapi import Request
5+
6+
from app.logger import logger
7+
8+
9+
async def log_request_middleware(request: Request, call_next):
10+
url = f"{request.url.path}?{request.query_params}" if request.query_params else request.url.path
11+
start_time = time.time()
12+
response = await call_next(request)
13+
process_time = (time.time() - start_time) * 1000
14+
formatted_process_time = f"{process_time:.2f}"
15+
host = getattr(getattr(request, "client", None), "host", None)
16+
port = getattr(getattr(request, "client", None), "port", None)
17+
try:
18+
status_phrase = http.HTTPStatus(response.status_code).phrase
19+
except ValueError:
20+
status_phrase=""
21+
logger.info(f'{host}:{port} - "{request.method} {url}" {response.status_code} {status_phrase} {formatted_process_time}ms')
22+
return response

app/services/db/database.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from app.config import settings
66

7+
78
@lru_cache
89
def get_collection(collection_name: str) -> AsyncIOMotorCollection:
910
client = AsyncIOMotorClient(settings.DB_URI)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
fastapi==0.115.5
22
uvicorn==0.32.0
3+
email-validator==2.2.0
34
pydantic-settings==2.6.1
45
motor==3.6.0
56
python-dotenv==1.0.1

0 commit comments

Comments
 (0)