如何在Uvicorn/FastAPI的访问日志中添加User-Agent等自定义变量
如何在Uvicorn访问日志中添加User-Agent等请求头信息
核心问题
Uvicorn默认的AccessFormatter拿到的日志记录(record对象)里没有请求头数据,因为uvicorn.access日志的默认生成逻辑未传递这些信息。要实现需求,有两种可行方案:
方案一:自定义中间件记录访问日志(推荐)
通过FastAPI/Starlette的中间件拦截请求与响应,直接获取请求头信息并构造自定义格式的访问日志,无需修改Uvicorn的日志组件逻辑。
示例代码:
import logging from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import Response from datetime import datetime access_logger = logging.getLogger("uvicorn.access") class CustomAccessLogMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next) -> Response: response = await call_next(request) # 提取日志所需字段 client_addr = request.client.host if request.client else "-" request_line = f"{request.method} {request.url.path} HTTP/{request.scope.get('http_version', '1.1')}" status_code = response.status_code user_agent = request.headers.get("user-agent", "-") x_forwarded_for = request.headers.get("x-forwarded-for", "-") # 构造并写入日志 log_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") log_message = f'[{log_time}] - {client_addr} - "{request_line}" {status_code} "{user_agent}" "{x_forwarded_for}"' access_logger.info(log_message) return response
在FastAPI应用中注册中间件:
from fastapi import FastAPI app = FastAPI() app.add_middleware(CustomAccessLogMiddleware)
方案二:重写Uvicorn的AccessLogger与AccessFormatter
直接修改Uvicorn生成访问日志的逻辑,把请求头信息注入到日志记录对象中,让Formatter可以直接调用这些字段。
- 自定义AccessLogger,扩展日志字段:
from uvicorn.logging import AccessLogger class CustomAccessLogger(AccessLogger): def log_message(self, message: str, scope, status_code: int, response_length: int) -> None: # 从请求scope中解析请求头 headers = dict(scope.get("headers", [])) user_agent = headers.get(b"user-agent", b"-").decode("utf-8") x_forwarded_for = headers.get(b"x-forwarded-for", b"-").decode("utf-8") # 将自定义字段传入日志记录的extra参数 extra = { "client_addr": self.get_client_addr(scope), "request_line": message, "status_code": status_code, "user_agent": user_agent, "x_forwarded_for": x_forwarded_for } self.logger.info(message, extra=extra)
- 自定义AccessFormatter,支持新字段:
from uvicorn.logging import AccessFormatter class CustomAccessFormatter(AccessFormatter): def formatMessage(self, record): # 确保自定义字段存在,避免报错 record.user_agent = getattr(record, "user_agent", "-") record.x_forwarded_for = getattr(record, "x_forwarded_for", "-") return super().formatMessage(record)
- 更新日志配置,使用自定义组件:
import logging from logging.config import dictConfig log_config = { "version": 1, "disable_existing_loggers": False, "formatters": { "default": { "()": "uvicorn.logging.DefaultFormatter", "fmt": "[%(asctime)s] - %(levelname)s - %(message)s", "datefmt": "%Y-%m-%d %H:%M:%S", }, "access": { "()": "my_app.logger.CustomAccessFormatter", # 替换为你的类实际路径 "fmt": '[%(asctime)s] - %(client_addr)s - "%(request_line)s" %(status_code)s "%(user_agent)s" "%(x_forwarded_for)s"', "datefmt": "%Y-%m-%d %H:%M:%S", "use_colors": False, }, }, "handlers": { "default": { "formatter": "default", "class": "logging.StreamHandler", "stream": "ext://sys.stderr", }, "access": {"class": "logging.StreamHandler", "formatter": "access", "stream": "ext://sys.stdout"}, }, "loggers": { "my_app": {"handlers": ["default"], "level": "DEBUG"}, "uvicorn": {"handlers": ["default"], "level": "DEBUG", "propagate": True}, "uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False}, "uvicorn.error": {"level": "INFO", "propagate": False}, }, } dictConfig(log_config)
- 启动Uvicorn时指定自定义AccessLogger:
uvicorn my_app.app:app --host 0.0.0.0 --port 8000 --proxy-headers --access-logger-class my_app.logger.CustomAccessLogger
注意事项
- 若启用
--proxy-headers,客户端真实IP需从X-Forwarded-For请求头获取,而非request.client.host(后者是代理服务器IP)。 - 方案一兼容性更强,无需依赖Uvicorn内部实现;方案二更贴近原生日志逻辑,但需注意Uvicorn版本更新可能带来的API变动。
内容的提问来源于stack exchange,提问作者PATAPOsha
相关产品推荐
相关产品推荐

