如何在Django日志中包含IP地址/用户名?附生产环境相关疑问
我希望在Django日志里不仅记录消息,还要记录请求的关键信息,比如IP地址和用户。
我知道可以用logger的extra属性传入request相关信息,比如logger.info("my message", extra={'ip': request.ip, 'user': request.user},但这种方法要在每一处日志调用都加这段代码,太繁琐,后续新增属性也不好维护。
我理解LogAdapter只适合静态额外上下文的场景,所以参考思路实现了自定义中间件,把request对象存到线程本地存储,再通过自定义过滤器读取,代码如下:
import logging import threading local = threading.local() class RequestFilter(logging.Filter): def filter(self, record): request = getattr(local, 'request', None) if request: record.ip = request.META.get('REMOTE_ADDR') record.user = request.user.username else: record.ip = '-' record.user = '-' return True class LogRequestMiddleware: """ A middleware that stores request object into the thread local storage, so that logging filter can access it and populate necessary information to the logs """ def __init__(self, get_response): self.get_response = get_response def __call__(self, request): setattr(local, 'request', request) return self.get_response(request)
- 我用
thread.local的方法能正常工作,但在nginx+gunicorn+django的生产环境里这个方案安全吗?会不会出现线程本地存储被其他线程覆盖的风险? - 我配置了如下LOGGING把日志写入文件,会不会出现多个线程同时写文件的竞态条件?
- 我觉得这类需求在Django开发里应该很常见,但相关资料不多,有没有更优的实现方案?
LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { "simple": { "format": "{levelname} {asctime} {ip} {funcName} {user} {message}", "style": "{", }, }, 'filters': { 'ip_filter': { '()': 'EngMemo.logging.RequestFilter', } }, 'handlers': { 'file': { 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', 'filename': LOGS_DIR + 'api.log', 'maxBytes': 1024 * 1024, # 1 MB 'backupCount': 5, # Number of backup log files to keep 'formatter': 'simple', 'filters': ['ip_filter'], }, }, 'loggers': { 'api_log': { 'handlers': ['file'], 'level': 'INFO', 'propagate': True, }, }, }
问题1:线程本地存储在生产环境的安全性
在gunicorn默认的同步worker模式下,每个请求由独立线程处理,线程本地存储的数据是线程隔离的,不会被其他线程覆盖,这个方案是安全的。
如果使用gunicorn的异步worker模式(比如gevent),线程本地存储可能因协程切换出现数据混乱——多个协程会复用同一个线程。这种情况下需要改用协程本地存储(比如gevent的gevent.local.local())。
另外你的中间件有个小问题:请求处理完成后没有清理线程本地存储的request对象。如果线程被线程池复用,下一个请求可能拿到上一个请求的残留数据,建议修改中间件用try-finally清理:
def __call__(self, request): try: setattr(local, 'request', request) return self.get_response(request) finally: delattr(local, 'request')
问题2:日志文件写入的竞态条件
Python标准库的RotatingFileHandler内部自带锁机制,多个线程同时写入时会自动加锁避免竞态,不会出现日志内容混乱的情况。
但要注意:如果gunicorn开了多个worker进程,不同进程之间的锁是不共享的,同时写同一个文件会出现日志错乱。这种情况建议让每个worker写单独的日志文件,或者用ELK、fluentd这类专门的日志收集服务统一处理。
问题3:更优的实现方案
除了线程本地存储的方式,还有两种常用的更简洁方案:
方案1:用Django内置的current_request(Django 3.2+)
Django 3.2及以上版本提供了django.utils.module_loading.current_request,可以直接在过滤器里获取当前请求,不需要自己维护线程本地存储:
from django.utils.module_loading import current_request class RequestFilter(logging.Filter): def filter(self, record): request = current_request() if request: record.ip = request.META.get('REMOTE_ADDR') record.user = request.user.username if request.user.is_authenticated else '-' else: record.ip = '-' record.user = '-' return True
这种方式不需要自定义中间件,Django已经处理了线程/协程的隔离问题,更简洁可靠。
方案2:结合LoggerAdapter封装请求上下文
如果需要更灵活的上下文注入,可以封装LoggerAdapter,在请求开始时创建带上下文的适配器,存到request对象里:
class RequestLoggerMiddleware: def __init__(self, get_response): self.get_response = get_response self.logger = logging.getLogger('api_log') def __call__(self, request): context = { 'ip': request.META.get('REMOTE_ADDR'), 'user': request.user.username if request.user.is_authenticated else '-' } request.logger = logging.LoggerAdapter(self.logger, context) return self.get_response(request)
之后在视图里直接用request.logger.info("my message")即可,不需要手动传extra,适合需要动态调整上下文的场景。
内容的提问来源于stack exchange,提问作者Hao

