Skip to content

Commit 3f823ff

Browse files
feat(middleware): implement request logging middleware with process time tracking
1 parent d00b75e commit 3f823ff

File tree

4 files changed

+99
-15
lines changed

4 files changed

+99
-15
lines changed

app/__init__.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
from fastapi import FastAPI, Request, status
66
from fastapi.encoders import jsonable_encoder
77
from fastapi.exceptions import RequestValidationError
8-
from fastapi.middleware.cors import CORSMiddleware
98
from fastapi.responses import JSONResponse
109
from fastapi.routing import APIRoute
11-
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
1210

11+
from app.middlewares import setup_middleware
1312
from app.utils.logger import get_logger
14-
from config import ALLOWED_ORIGINS, DOCS, SUBSCRIPTION_PATH
13+
from config import DOCS, SUBSCRIPTION_PATH
1514

1615
__version__ = "1.8.1"
1716

@@ -70,16 +69,7 @@ async def lifespan(app: FastAPI):
7069
scheduler = AsyncIOScheduler(job_defaults={"max_instances": 30}, timezone="UTC")
7170
logger = get_logger()
7271

73-
74-
app.add_middleware(
75-
CORSMiddleware,
76-
allow_origins=ALLOWED_ORIGINS,
77-
allow_credentials=True,
78-
allow_methods=["*"],
79-
allow_headers=["*"],
80-
)
81-
82-
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*")
72+
setup_middleware(app)
8373

8474
from app import routers, telegram, jobs # noqa
8575
from app.routers import api_router # noqa

app/middlewares/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from fastapi import FastAPI
2+
from fastapi.middleware.cors import CORSMiddleware
3+
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
4+
5+
from app.utils.logger import get_logger
6+
from config import ALLOWED_ORIGINS
7+
8+
from .request_logging import RequestProcessTimeLoggingMiddleware
9+
10+
11+
def setup_middleware(app: FastAPI):
12+
app.add_middleware(
13+
CORSMiddleware,
14+
allow_origins=ALLOWED_ORIGINS,
15+
allow_credentials=True,
16+
allow_methods=["*"],
17+
allow_headers=["*"],
18+
)
19+
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*")
20+
app.add_middleware(RequestProcessTimeLoggingMiddleware, access_logger=get_logger("uvicorn.access"))

app/middlewares/request_logging.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import logging
2+
from time import perf_counter
3+
4+
from starlette.middleware.base import BaseHTTPMiddleware
5+
from starlette.requests import Request
6+
7+
8+
class RequestProcessTimeLoggingMiddleware(BaseHTTPMiddleware):
9+
def __init__(self, app, access_logger: logging.Logger):
10+
super().__init__(app)
11+
self.access_logger = access_logger
12+
13+
async def dispatch(self, request: Request, call_next):
14+
start_time = perf_counter()
15+
status_code = 500
16+
17+
try:
18+
response = await call_next(request)
19+
status_code = response.status_code
20+
return response
21+
finally:
22+
process_time_ms = (perf_counter() - start_time) * 1000
23+
path = request.url.path
24+
if request.url.query:
25+
path = f"{path}?{request.url.query}"
26+
http_version = request.scope.get("http_version", "1.1")
27+
client_addr = request.client.host if request.client else "-"
28+
29+
self.access_logger.info(
30+
'%s - "%s %s HTTP/%s" %d',
31+
client_addr,
32+
request.method,
33+
path,
34+
http_version,
35+
status_code,
36+
extra={"process_time": f"{process_time_ms:.2f}ms"},
37+
)

app/utils/logger.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import click
66
from uvicorn.config import LOGGING_CONFIG
7-
from uvicorn.logging import DefaultFormatter
7+
from uvicorn.logging import AccessFormatter, ColourizedFormatter, DefaultFormatter
88

99
from config import (
1010
ECHO_SQL_QUERIES,
@@ -26,6 +26,37 @@ def formatMessage(self, record: logging.LogRecord) -> str:
2626
return super().formatMessage(recordcopy)
2727

2828

29+
class CustomAccessFormatter(AccessFormatter):
30+
def formatMessage(self, record: logging.LogRecord) -> str:
31+
recordcopy = copy(record)
32+
33+
try:
34+
client_addr, method, full_path, http_version, status_code = recordcopy.args # type: ignore[misc]
35+
except Exception:
36+
return super().formatMessage(record)
37+
38+
status_code = self.get_status_code(int(status_code)) # type: ignore[arg-type]
39+
request_line = f"{method} {full_path} HTTP/{http_version}"
40+
if self.use_colors:
41+
request_line = click.style(request_line, bold=True)
42+
43+
recordcopy.__dict__.update(
44+
{
45+
"client_addr": client_addr,
46+
"request_line": request_line,
47+
"status_code": status_code,
48+
"process_time": getattr(recordcopy, "process_time", "-"),
49+
}
50+
)
51+
52+
return ColourizedFormatter.formatMessage(self, recordcopy)
53+
54+
55+
class RequireProcessTimeFilter(logging.Filter):
56+
def filter(self, record: logging.LogRecord) -> bool:
57+
return getattr(record, "process_time", None) is not None
58+
59+
2960
LOGGING_CONFIG["formatters"]["custom"] = {
3061
"()": CustomLoggingFormatter,
3162
"fmt": "%(levelprefix)s %(asctime)s - %(nameprefix)s - %(message)s",
@@ -39,8 +70,14 @@ def formatMessage(self, record: logging.LogRecord) -> str:
3970

4071
LOGGING_CONFIG["formatters"]["default"]["fmt"] = "%(levelprefix)s %(asctime)s - %(message)s"
4172
LOGGING_CONFIG["formatters"]["access"]["fmt"] = (
42-
'%(levelprefix)s %(asctime)s - %(client_addr)s - "%(request_line)s" %(status_code)s'
73+
'%(levelprefix)s %(asctime)s - %(client_addr)s - "%(request_line)s" %(status_code)s - %(process_time)s'
4374
)
75+
LOGGING_CONFIG["formatters"]["access"]["()"] = CustomAccessFormatter
76+
77+
LOGGING_CONFIG.setdefault("filters", {})
78+
LOGGING_CONFIG["filters"]["require_process_time"] = {"()": RequireProcessTimeFilter}
79+
LOGGING_CONFIG["loggers"]["uvicorn.access"].setdefault("filters", [])
80+
LOGGING_CONFIG["loggers"]["uvicorn.access"]["filters"].append("require_process_time")
4481

4582
LOGGING_CONFIG["loggers"]["uvicorn"]["level"] = LOG_LEVEL
4683
LOGGING_CONFIG["loggers"]["uvicorn.error"]["level"] = LOG_LEVEL

0 commit comments

Comments
 (0)