Django 多进程日志写入日志大小模式与时间切割模式
django logging 的实现,是基于python logging模块实现,而logging模块仅仅是线程安全的;而使用uwsgi启动项目时,是启动多个进程的;logging模块并没有保证多进程安全。
1.使用 concurrent-log-handler 包记录日志
这个包通过加锁的方式实现了多进程安全,并且可以在日志文件达到特定大小时,分割文件,但是不支持按时间分割。如果使用日志大小来分割日志文件的话,就可以使用concurrent-log-handler。
安装:
pip install concurrent-log-handler
以下为django配置文件settings.py下的配置:
import concurrent_log_handler # 日志配置 只要将注释打开相应的日志就会写入到logs/demo.log 里面 LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { # 详细 'verbose': { # 错误等级 错误时间戳 错误魔抗 错误行 错误信息 'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s' }, # 简单 'simple': { 'format': '%(levelname)s:(%(asctime)s:%(module)s: %(filename)s:%(lineno)d):%(message)s' }, # 错误日志格式 'err_info': { 'format': '%(levelname)s:(%(asctime)s:%(module)s: %(filename)s:%(lineno)d):%(message)s' } }, # 日志过滤器 'filters': { 'require_debug_false': { '()': 'django.utils.log.RequireDebugFalse' }, 'require_debug_true': { '()': 'django.utils.log.RequireDebugTrue', }, }, 'handlers': { 'console': { # 过滤错误模式 'level': 'DEBUG', 'filters': ['require_debug_true'], 'class': 'logging.StreamHandler', 'formatter': 'simple' }, 'file': { # 记录日志的错误等级 'level': 'INFO', # logging.handlers.RotatingFileHandler 'class': 'concurrent_log_handler.ConcurrentRotatingFileHandler', # 日志位置,日志文件名,日志保存目录必须手动创建 'filename': os.path.join(BASE_DIR, 'logs/demo.log'), # 日志文件的最大值,这里我们设置100M 'maxBytes': 1 * 1024 * 1024, # 日志文件的数量,设置最大日志数量为20 'backupCount': 10, # 日志格式:详细格式 'formatter': 'verbose' }, # 输出info日志 'info': { 'level': 'INFO', 'class': 'concurrent_log_handler.ConcurrentRotatingFileHandler', 'filename': os.path.join(BASE_DIR, 'logs/demo.log'), 'maxBytes': 100 * 1024 * 1024, 'backupCount': 10, 'formatter': 'verbose', # 'standard', 'encoding': 'utf-8', # 设置默认编码 }, # 专门用来记错误日志 'error': { 'level': 'DEBUG', 'class': 'concurrent_log_handler.ConcurrentRotatingFileHandler', # 保存到文件,自动切 'filters': ['require_debug_false'], 'filename': os.path.join(BASE_DIR, "logs/demo.log"), # 日志文件 'maxBytes': 1024 * 1024 * 50, # 日志大小 50M 'backupCount': 10, 'formatter': 'err_info', 'encoding': 'utf-8', }, }, # 日志对象 'loggers': { 'django': { 'handlers': ['console', 'file', 'error'], # 'propagate': True, # 是否让日志信息继续冒泡给其他的日志处理系统,一般为Ture ,不保证其他第三方模块有记录 }, 'log': { 'handlers': ['info', 'console'], 'level': 'INFO', 'propagate': True }, 'error': { 'handlers': ['console', 'file', 'error'], 'level': 'DEBUG', 'propagate': True }, } }
应用的demo:
import logging logger = logging.getLogger('error') logger.error("12345678")
这样访问这个接口,就会写入日志文件,并且messages为12345678
2.使用 TimedRotatingFileHandler记录日志
以下为django配置文件settings.py下的配置:
LOGGING = { 'version': 1, 'disable_existing_logger': False, 'formatters': { 'verbose': { 'format': '%(asctime)s \"%(pathname)s:%(module)s:%(funcName)s:%(lineno)d\" [%(levelname)s]-%(message)s' }, }, # 处理器 'handlers': { # 输出控制台 'console': { 'level': 'INFO', 'class': 'logging.StreamHandler', 'formatter': 'verbose' }, # 输出文件 'file': { 'level': 'DEBUG', 'class': 'logging.handlers.TimedRotatingFileHandler', 'filename': 'logs/blog.log', 'formatter': 'verbose', # 每分钟切割一次日志 'when': 'M', # 时间间隔 'interval': 1, # 保留5份日志 'backupCount': 5, 'encoding': 'utf-8' }, }, # 记录器 'loggers': { 'django': { 'handlers': ['file'], # 'console', 'level': 'INFO', 'propagete': True, }, } }
这个可以实现,按时间的间隔进行日志切割。
3.使用CommonTimedRotatingFileHandler记录日志(暂时未成功)
重写 TimedRotatingFileHandler
通过上面的分析可以知道,出问题的点就是发生在日志分割时,一是删文件,二是没有及时更新写入句柄。
所以针对这两点,我的对策就是:一是去掉删文件的逻辑,二是在切割文件时,及时将写入句柄更新到最新。
代码如下:
# 解决多进程日志写入混乱问题 import os import time from logging.handlers import TimedRotatingFileHandler class CommonTimedRotatingFileHandler(TimedRotatingFileHandler): @property def dfn(self): currentTime = int(time.time()) # get the time that this sequence started at and make it a TimeTuple dstNow = time.localtime(currentTime)[-1] t = self.rolloverAt - self.interval if self.utc: timeTuple = time.gmtime(t) else: timeTuple = time.localtime(t) dstThen = timeTuple[-1] if dstNow != dstThen: if dstNow: addend = 3600 else: addend = -3600 timeTuple = time.localtime(t + addend) dfn = self.rotation_filename(self.baseFilename + "." + time.strftime(self.suffix, timeTuple)) return dfn def shouldRollover(self, record): """ 是否应该执行日志滚动操作: 1、存档文件已存在时,执行滚动操作 2、当前时间 >= 滚动时间点时,执行滚动操作 """ dfn = self.dfn t = int(time.time()) if t >= self.rolloverAt or os.path.exists(dfn): return 1 return 0 def doRollover(self): """ 执行滚动操作 1、文件句柄更新 2、存在文件处理 3、备份数处理 4、下次滚动时间点更新 """ if self.stream: self.stream.close() self.stream = None # get the time that this sequence started at and make it a TimeTuple dfn = self.dfn # 存档log 已存在处理 if not os.path.exists(dfn): self.rotate(self.baseFilename, dfn) # 备份数控制 if self.backupCount > 0: for s in self.getFilesToDelete(): os.remove(s) # 延迟处理 if not self.delay: self.stream = self._open() # 更新滚动时间点 currentTime = int(time.time()) newRolloverAt = self.computeRollover(currentTime) while newRolloverAt <= currentTime: newRolloverAt = newRolloverAt + self.interval # If DST changes and midnight or weekly rollover, adjust for this. if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc: dstAtRollover = time.localtime(newRolloverAt)[-1] dstNow = time.localtime(currentTime)[-1] if dstNow != dstAtRollover: if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour addend = -3600 else: # DST bows out before next rollover, so we need to add an hour addend = 3600 newRolloverAt += addend self.rolloverAt = newRolloverAt