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

自定义CSVTimedRotatingFileHandler无法触发日志滚动问题求助

自定义CSVTimedRotatingFileHandler无法触发日志滚动问题求助

我们的Python (v3.11.7) + Django (v3.2.23) 应用有个需求:把特定安全相关事件记录到CSV文件,要求每小时自动滚动日志,生成的文件名格式类似audit_logs20250130_0800-0900.csv。

后端跑在Docker容器里,启动命令如下:

gunicorn  wsgi:application --bind 0.0.0.0:8000 --workers 4 --threads 4

为了实现这个需求,我们继承logging.handlers.TimedRotatingFileHandler自定义了CSVTimedRotatingFileHandler,代码如下:

import logging
import os
from datetime import datetime, timedelta
from logging.handlers import TimedRotatingFileHandler

import pytz
import redis
from django.conf import settings

REDIS_KEY = 'CSVTimedRotatingFileHandler_RolloverAt'


class CSVTimedRotatingFileHandler(TimedRotatingFileHandler):
    def __init__(self, filename, when, interval, backup_count, encoding=None, delay=False, headers=None):
        super().__init__(filename, when=when, interval=interval, backupCount=backup_count,
                         encoding=encoding, delay=delay, utc=False, atTime=None, errors=None)
        self.headers = headers

    def emit(self, record):
        try:
            last_rollover_at = self.get_redis_rollover_at_value()

            # Check if a rollover happened and refresh the stream if needed (for multiple workers)
            if self.rolloverAt != last_rollover_at:
                self.rolloverAt = last_rollover_at
                if self.stream and not self.stream.closed:
                    self.stream.close()
                self.stream = self._open()

            if self.shouldRollover(record):
                self.doRollover()

            # If the stream is still closed or None, open it again
            if self.stream is None or self.stream.closed:
                self.stream = self._open()

            # Write headers if the file is empty
            if self.stream.tell() == 0 and self.headers:
                self.stream.write(','.join(self.headers) + self.terminator)
                self.flush()

            logging.FileHandler.emit(self, record)
        except Exception:
            self.handleError(record)

    @staticmethod
    def get_redis_rollover_at_value():
        r = redis.StrictRedis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=0)
        redis_rollover_value = r.get(REDIS_KEY)
        if redis_rollover_value:
            formatted_redis_rollover_value = int(redis_rollover_value.decode('utf-8'))
        else:
            formatted_redis_rollover_value = 0
        return formatted_redis_rollover_value

    @staticmethod
    def set_redis_rollover_at_value(value):
        r = redis.StrictRedis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=0)
        r.set(REDIS_KEY, value)

    def computeRollover(self, currentTime):
        redis_rollover_value = self.get_redis_rollover_at_value()
        if currentTime > redis_rollover_value:
            utc_timezone = pytz.utc
            date = datetime.fromtimestamp(currentTime, utc_timezone)
            updated_date = date.replace(minute=0, second=0, microsecond=0) + timedelta(seconds=self.interval)
            updated_timestamp = int(updated_date.timestamp())
            self.set_redis_rollover_at_value(updated_timestamp)
            return updated_timestamp
        return redis_rollover_value

    def rotation_filename(self, filename):
        file = os.path.basename(self.baseFilename)
        directory_path = os.path.dirname(self.baseFilename)
        name = file.split('.')[0]
        extension = file.split('.')[1]
        dot = '.'
        separator = '_'
        rollover_date = datetime.fromtimestamp(self.rolloverAt, tz=pytz.timezone('UTC'))
        date = rollover_date.strftime('%Y%m%d')
        from_hour = (rollover_date - timedelta(hours=1)).hour
        from_min = (rollover_date - timedelta(hours=1)).minute
        to_hour = rollover_date.hour
        to_minute = rollover_date.minute
        hours_delta = f'{from_hour:02}{from_min:02}-{to_hour:02}{to_minute:02}'
        return os.path.join(directory_path, f'{name}{date}{separator}{hours_delta}{dot}{extension}')

    def getFilesToDelete(self):
        result = []
        directory_path = os.path.dirname(self.baseFilename)
        files = [os.path.join(directory_path, f) for f in os.listdir(directory_path)
                 if os.path.isfile(os.path.join(directory_path, f)) and f != self.baseFilename]
        if os.path.join(directory_path, self.baseFilename) in files:
            files.remove(os.path.join(directory_path, self.baseFilename))
        files.sort(key=os.path.getctime)
        if len(files) > self.backupCount:
            result = files[:len(files) - self.backupCount]
        return result

    def flush(self):
        if self.stream and hasattr(self.stream, "flush"):
            self.stream.flush()

现在遇到的问题是:日志事件能正常写入CSV文件,但完全不会触发日志滚动——所有日志一直写到基础文件audit_logs.csv里,永远不会生成按小时命名的滚动文件。

我们做了手动测试:进入容器的python manage.py shell后,

  • 调用logging.handlers.TimedRotatingFileHandler.shouldRollover()能返回正确结果
  • 手动执行logging.handlers.TimedRotatingFileHandler.doRollover()也能正常完成滚动操作

有没有朋友能帮忙分析下,为什么实际运行时(多worker场景下)滚动逻辑不生效?

备注:内容来源于stack exchange,提问作者Kyriakos

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 08:44:33