上线一个 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 输出是否已同步北京时间——这是所有日志轮转的前提。