You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 05:37:10