日志轮转总比北京时间晚8小时?一招搞定时区坑

上线一个 Python 服务,用 RotatingFileHandler日志轮转,结果发现每天凌晨 16 点(即 UTC 0 点)就切新日志——明明服务器时间是北京时间(CST,UTC+8),可日志文件名却标着 app-2024-05-20.log,而实际才刚过午夜 0 点不到两小时。这事儿不是 bug,是时区没对齐。

为什么日志轮转会“错时”?

很多日志轮转逻辑(比如 Python 的 TimedRotatingFileHandler、Linux 的 logrotate、Nginx 的 access_log 切分)默认依赖系统本地时间或 UTC 时间,但没显式声明时区。一旦你的应用跑在 Docker 容器里、或者服务器 locale 没设好、又或者 Python 解释器启动时没加载时区环境,就容易出现:日志按 UTC 切,但你盯着 CST 看,自然觉得“提前了 8 小时”。

Python 日志轮转的时区修复

如果你用的是 TimedRotatingFileHandler,它底层靠 time.localtime(),而这个函数受 TZ 环境变量或系统时区影响。最稳的办法是显式指定时区:

import logging
from logging.handlers import TimedRotatingFileHandler
import pytz
from datetime import datetime

class TimeZoneRotatingHandler(TimedRotatingFileHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.tz = pytz.timezone('Asia/Shanghai')

    def getFilesToDelete(self):
        # 覆盖父类方法,确保按本地时区判断保留天数
        return super().getFilesToDelete()

    def shouldRollover(self, record):
        # 强制使用北京时间判断是否该轮转
        now = datetime.now(self.tz)
        t = now.timestamp()
        if self.stream is None:
            self.stream = self._open()
        if self._shouldRollover(t):
            return True
        return False

    def _shouldRollover(self, t):
        if self.when == 'D':
            dt_now = datetime.fromtimestamp(t, self.tz)
            dt_last = datetime.fromtimestamp(self.rolloverAt, self.tz)
            return dt_now.date() != dt_last.date()
        return super()._shouldRollover(t)

初始化 handler 时直接用它替代原生类:

handler = TimeZoneRotatingHandler(
    'app.log',
    when='D',
    interval=1,
    backupCount=7
)

logrotate 的时区处理

Linux 下常用 logrotate,它本身不支持时区参数,但可以通过 dateformat + 系统时区联动解决。确认你的服务器时区已设为上海:

sudo timedatectl set-timezone Asia/Shanghai
sudo systemctl restart rsyslog

然后在 /etc/logrotate.d/myapp 中写:

/var/log/myapp/*.log {
    daily
    dateext
    dateformat -%Y-%m-%d
    rotate 30
    missingok
    notifempty
    create 0644 www-data www-data
    sharedscripts
}

关键点:确保 dateformat 生成的日期和系统当前 date 命令输出一致。执行 date +%Y-%m-%d 看是不是你预期的“今天”。如果不是,先 fix 系统时区。

Docker 场景别漏这步

容器里跑应用,镜像默认是 UTC。加一行环境变量就能救:

ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

或者运行时挂载:

docker run -e TZ=Asia/Shanghai -v /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro ...

再检查容器内 date 输出是否已同步北京时间——这是所有日志轮转的前提。