Skip to content

Commit

Permalink
Add log config and custom API logger to log requests (#6)
Browse files Browse the repository at this point in the history
uvicorn log access of API like

```sh
api-server-1  | INFO:     192.168.65.1:41988 - "GET /swagger HTTP/1.1" 200 OK
```

but it's not helping, because it does not log the request body.

- Following this issue encode/uvicorn#491, I
made a custom log config to add logger name in log and adjust log format
- Add a `middleware` of fastapi to log the request in a more detailed
way.
- Override HTTPExceptionHandler to create error log when API is not
responding 200.
https://fastapi.tiangolo.com/tutorial/handling-errors/#override-the-httpexception-error-handler
- Hide uvicorn access log 

Now the log looks like:
```sh
api-server-1  | 2024-12-09 09:35:13,363 - INFO     - app.api_logger       - [request_id: 7608958e-36a6-428e-8427-83ced6b9d7c3, method:POST, path: http://0.0.0.0:8000/login/] Request body: {"e_mail":"123","password":"string"}.
api-server-1  | 2024-12-09 09:35:13,367 - INFO     - app.routers.login    - e_mail='123' password='string'
api-server-1  | 2024-12-09 09:35:13,367 - ERROR    - app.http_exc_handler - [request_id: 7608958e-36a6-428e-8427-83ced6b9d7c3, method:POST, path: http://0.0.0.0:8000/login/] Failed to handle request. status_code=404, detail=asdasdas
```


**What can be improved in the future:**
- Instead of only outputting logs to console, we should also keep logs
as files in disk and use logstash to send logs to es like real-world
application does.
- Response is not logged, but we may want to add it. APILoggerMiddleware
has to build a dummy Response from the outgoing bytestreams, which is
not good. So we may want to find another way to do so.
  • Loading branch information
kun98-liu authored Dec 10, 2024
1 parent db68665 commit 8c3af66
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 7 deletions.
2 changes: 1 addition & 1 deletion api-server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ FROM python:3.9-slim
WORKDIR /app
COPY .. .
RUN pip install -r requirements.txt
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--log-config", "log_conf.yaml"]
9 changes: 9 additions & 0 deletions api-server/app/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from fastapi import FastAPI
from starlette.exceptions import HTTPException

from app.routers import student, login, user
from app.server.api_logger_middleware import APILoggerMiddleware
from app.server.http_exception_handler import http_exception_handler

app = FastAPI(
docs_url="/swagger",
Expand All @@ -9,3 +13,8 @@
app.include_router(student.router)
app.include_router(login.router)
app.include_router(user.router)

app.add_middleware(APILoggerMiddleware)

app.add_exception_handler(HTTPException, http_exception_handler)

10 changes: 6 additions & 4 deletions api-server/app/routers/login.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import logging
from fastapi import APIRouter

from app.schema.loginschema import LoginRequestSchema, LoginResponseSchema, SigninResponseSchema, SigninRequestSchema

router = APIRouter(prefix="/login", tags=["login"])
logger = logging.getLogger(__name__)


@router.post("/", response_model=LoginResponseSchema)
async def login(login_request: LoginRequestSchema):
print(f"{login_request}")
logger.info(f"{login_request}")
return LoginResponseSchema(token="nidetoken", user_id="niyeye")


@router.post("/signin", response_model=SigninResponseSchema)
async def signin(signin_request: SigninRequestSchema):
print(f"{signin_request}")
return SigninResponseSchema(success=True)
logger.info(f"{signin_request}")
return SigninResponseSchema(success=True)
5 changes: 3 additions & 2 deletions api-server/app/routers/user.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import logging
from fastapi import APIRouter

from app.schema.userschema import UpdateUserinfoRequestSchema, UpdateUserinfoResponseSchema, GetUserinfoResponseSchema

router = APIRouter(prefix="/userinfo", tags=["userinfo"])
logger = logging.getLogger(__name__)


@router.post("/", response_model=UpdateUserinfoResponseSchema)
async def update_user(userinfo_request: UpdateUserinfoRequestSchema):
print(f"{userinfo_request}")
logger.info(f"{userinfo_request}")
return UpdateUserinfoResponseSchema(success=True)


@router.get("/{user_id}", response_model=GetUserinfoResponseSchema)
async def get_user(user_id: str):
return GetUserinfoResponseSchema(display_name="lulu", tel="23", address="tutu")

Empty file.
35 changes: 35 additions & 0 deletions api-server/app/server/api_logger_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import json
import logging
import uuid
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
import time

# Initialize the logger
logger = logging.getLogger("app.api_logger")


# Custom Middleware for Logging on API request
class APILoggerMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Generate a unique request_id
request_id = str(uuid.uuid4())
request.state.request_id = request_id # Attach to the request state

# Log the incoming request
start_time = time.time()
body = await request.body()
try:
# Try to load JSON and log it in compact form
body_json = json.loads(body)
compact_body = json.dumps(body_json, separators=(",", ":"))
except json.JSONDecodeError:
# Fallback to plain text if not valid JSON
compact_body = body.decode("utf-8")
logger.info(
f"[request_id: {request_id}, method:{request.method}, path: {request.url}] "
f"Request body: {compact_body}."
)

response = await call_next(request)
return response
18 changes: 18 additions & 0 deletions api-server/app/server/http_exception_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import logging

from fastapi.responses import JSONResponse

logger = logging.getLogger("app.http_exc_handler")


async def http_exception_handler(request, exc):
logger.error(
f"[request_id: {request.state.request_id}, method:{request.method}, path: {request.url}] "
f"Failed to handle request. "
f"status_code={exc.status_code}, detail={exc.detail} "
)
# Return the original response to the client
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
)
34 changes: 34 additions & 0 deletions api-server/log_conf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
version: 1
disable_existing_loggers: False
formatters:
default:
# "()": uvicorn.logging.DefaultFormatter
format: '%(asctime)s - %(levelname)-8s - %(name)-20s - %(message)s'
access:
# "()": uvicorn.logging.AccessFormatter
format: '%(asctime)s - %(levelname)-8s - %(name)-20s - %(message)s'
handlers:
default:
formatter: default
class: logging.StreamHandler
stream: ext://sys.stderr
access:
formatter: access
class: logging.StreamHandler
stream: ext://sys.stdout
loggers:
uvicorn.error:
level: INFO
handlers:
- default
propagate: no
uvicorn.access:
level: WARN # only log when access log is WARN level to reduce noise
handlers:
- access
propagate: no
root:
level: INFO
handlers:
- default
propagate: no
1 change: 1 addition & 0 deletions api-server/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ fastapi~=0.115.5
pydantic~=2.10.2
motor
uvicorn
PyYAML
pytest

0 comments on commit 8c3af66

Please sign in to comment.