python 多进程日志 logging

python的logging模块提供了灵活的标准模块,使得任何Python程序都可以使用这个第三方模块来实现日志记录。
  但是 python 中logging 并不支持多进程,所以会遇到不少麻烦。
  以 TimedRotatingFileHandler 这个类的问题作为例子。这个Handler本来的作用是:按天切割日志文件。(当天的文件是xxxx.log 昨天的文件是xxxx.log.2016-06-01)。这样的好处是,一来可以按天来查找日志,二来可以让日志文件不至于非常大, 过期日志也可以按天删除。
  但是问题来了,如果是用多进程来输出日志,则只有一个进程会切换,其他进程会在原来的文件中继续打,还有可能某些进程切换的时候早就有别的进程在新的日志文件里打入东西了,那么他会无情删掉之,再建立新的日志文件。反正将会很乱很乱,完全没法开心的玩耍。还会有一些其他莫名其妙的麻烦比如: os.rename(self.baseFilename, dfn)WindowsError: [Error 32] 错误 (进程无法访问文件,因为另一个程序正在使用此文件 是文件已经打开的错误,改名前没有关闭文件。就是一个进程在使用此文件,另一个进程想要修改文件名)
  so 我们需要改写一个 logging中的 handler 以使logging支持多进程
  重写FileHandler类(这个类是所有写入文件的Handler都需要继承的TimedRotatingFileHandler 就是继承的这个类;我们增加一些简单的判断和操作就可以。
  我们的逻辑是这样的:
 1. 判断当前时间戳是否与指向的文件名是同一个时间
 2. 如果不是,则切换 指向的文件即可
 3. 结束,是不是很简单的逻辑。
以下代码参考messud4312的博客 感谢这位大哥

#multiprocessloghandler.py
import os
import re
import datetime
import logging

try:
    import codecs
except ImportError:
    codecs = None
class MultiprocessHandler(logging.FileHandler):
    """支持多进程的TimedRotatingFileHandler"""
    def __init__(self,filename,when='D',backupCount=0,encoding=None,delay=False):
        """filename 日志文件名,when 时间间隔的单位,backupCount 保留文件个数
        delay 是否开启 OutSteam缓存
            True 表示开启缓存,OutStream输出到缓存,待缓存区满后,刷新缓存区,并输出缓存数据到文件。
            False表示不缓存,OutStrea直接输出到文件"""
        self.prefix = filename
        self.backupCount = backupCount
        self.when = when.upper()
     #正则匹配 年-月-日
        #正则写到这里就对了
    self.extMath = r"^\d{4}-\d{2}-\d{2}"

        # S 每秒建立一个新文件
        # M 每分钟建立一个新文件
        # H 每天建立一个新文件
        # D 每天建立一个新文件
        self.when_dict = {
            'S':"%Y-%m-%d-%H-%M-%S",
            'M':"%Y-%m-%d-%H-%M",
            'H':"%Y-%m-%d-%H",
            'D':"%Y-%m-%d"
        }
        #日志文件日期后缀
        self.suffix = self.when_dict.get(when)
        #源码中self.extMath写在这里
        #这个正则匹配不应该写到这里,不然非D模式下 会造成 self.extMath属性不存在的问题
        #不管是什么模式都是按照这个正则来搜索日志文件的。
        # if self.when == 'D':
        #    正则匹配 年-月-日
        #    self.extMath = r"^\d{4}-\d{2}-\d{2}"
        if not self.suffix:
            raise ValueError(u"指定的日期间隔单位无效: %s" % self.when)
        #拼接文件路径 格式化字符串
        self.filefmt = os.path.join("logs","%s.%s" % (self.prefix,self.suffix))
        #使用当前时间,格式化文件格式化字符串
        self.filePath = datetime.datetime.now().strftime(self.filefmt)
        #获得文件夹路径
        _dir = os.path.dirname(self.filefmt)
        try:
            #如果日志文件夹不存在,则创建文件夹
            if not os.path.exists(_dir):
                os.makedirs(_dir)
        except Exception:
            print u"创建文件夹失败"
            print u"文件夹路径:" + self.filePath
            pass
        if codecs is None:
            encoding = None
    #调用FileHandler
        logging.FileHandler.__init__(self,self.filePath,'a+',encoding,delay)

  def shouldChangeFileToWrite(self):
        """更改日志写入目的写入文件
        return True 表示已更改,False 表示未更改"""
        #以当前时间获得新日志文件路径
        _filePath = datetime.datetime.now().strftime(self.filefmt)
        #新日志文件日期 不等于 旧日志文件日期,则表示 已经到了日志切分的时候
        #   更换日志写入目的为新日志文件。
        #例如 按 天 (D)来切分日志
        #   当前新日志日期等于旧日志日期,则表示在同一天内,还不到日志切分的时候
        #   当前新日志日期不等于旧日志日期,则表示不在
        #同一天内,进行日志切分,将日志内容写入新日志内。
        if _filePath != self.filePath:
            self.filePath = _filePath
            return True
        return False

    def doChangeFile(self):
        """输出信息到日志文件,并删除多于保留个数的所有日志文件"""
        #日志文件的绝对路径
        self.baseFilename = os.path.abspath(self.filePath)
        #stream == OutStream
        #stream is not None 表示 OutStream中还有未输出完的缓存数据
        if self.stream:
            self.stream.flush()
            self.stream.close()
        #delay 为False 表示 不OutStream不缓存数据 直接输出
        #   所有,只需要关闭OutStream即可
        if not self.delay:
            self.stream.close()
           
        #删除多于保留个数的所有日志文件
        if self.backupCount > 0:
            for s in self.getFilesToDelete():
                #print s
                os.remove(s)
  def getFilesToDelete(self):
        """获得过期需要删除的日志文件"""
        #分离出日志文件夹绝对路径
        #split返回一个元组(absFilePath,fileName)
        #例如:split('I:\ScripPython\char4\mybook\util\logs\mylog.2017-03-19)
        #返回(I:\ScripPython\char4\mybook\util\logs, mylog.2017-03-19)
        # _ 表示占位符,没什么实际意义,
        dirName,_ = os.path.split(self.baseFilename)
        fileNames = os.listdir(dirName)
        result = []
        #self.prefix 为日志文件名 列如:mylog.2017-03-19 中的 mylog
        #加上 点号 . 方便获取点号后面的日期
        prefix = self.prefix + '.'
        plen = len(prefix)
        for fileName in fileNames:
            if fileName[:plen] == prefix:
                #日期后缀 mylog.2017-03-19 中的 2017-03-19
                suffix = fileName[plen:]
                #匹配符合规则的日志文件,添加到result列表中
                if re.compile(self.extMath).match(suffix):
                    result.append(os.path.join(dirName,fileName))
        result.sort()

        #返回  待删除的日志文件
        #   多于 保留文件个数 backupCount的所有前面的日志文件。
        if len(result) < self.backupCount:
            result = []
        else:
            result = result[:len(result) - self.backupCount]
        return result

    def emit(self, record):
        """发送一个日志记录
        覆盖FileHandler中的emit方法,logging会自动调用此方法"""
        try:
            if self.shouldChangeFileToWrite():
                self.doChangeFile()
            logging.FileHandler.emit(self,record)
        except (KeyboardInterrupt,SystemExit):
            raise
        except:
            self.handleError(record)

messud4312的博客 大哥的源代码是这个样子的,但是 经过我测试发现在使用中会造成一些I/O错误
下面我们来测试一下:

import sys
import time
import multiprocessing
from multiprocessloghandler import MultiprocessHandler

# 定义日志输出格式
formattler = '%(levelname)s - %(name)s - %(asctime)s - %(message)s'
fmt = logging.Formatter(formattler)

# 获得logger,默认获得root logger对象
# 设置logger级别 debug
# root logger默认的级别是warning级别。
# 不设置的话 只能发送 >= warning级别的日志
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# 设置handleer日志处理器,日志具体怎么处理都在日志处理器里面定义
# SteamHandler 流处理器,输出到控制台,输出方式为stdout
#   StreamHandler默认输出到sys.stderr
# 设置handler所处理的日志级别。
#   只能处理 >= 所设置handler级别的日志
# 设置日志输出格式
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setLevel(logging.DEBUG)
stream_handler.setFormatter(fmt)

# 使用我们写的多进程版Handler理器,定义日志输出到mylog.log文件内
#   文件打开方式默认为 a
#   按分钟进行日志切割
file_handler = MultiprocessHandler('mylog', when='M')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(fmt)

# 对logger增加handler日志处理器
logger.addHandler(stream_handler)
logger.addHandler(file_handler)


# 发送debug级别日志消息
def test(num):
    time.sleep(3)
    logger.debug('日志测试' + str(num))

if __name__ == '__main__':

    pool = multiprocessing.Pool(processes=10)

    for i in range(10):
        pool.apply_async(func=test, args=(i,))
    pool.close()
    pool.join()
    print '完毕'

测试结果如下:


image.png

image.png

这样则能正常的使用
下面重点来了:

def test(num):
    time.sleep(10)
    logger.debug('日志测试' + str(num))

if __name__ == '__main__':

    pool = multiprocessing.Pool(processes=3)

    for i in range(10):
        pool.apply_async(func=test, args=(i,))
    pool.close()
    pool.join()
    print '完毕'

运行结果如下:


image.png

  在休眠时间过长的情况下 会造成 对已关闭文件进行I/0操作的错误,也不是每次都出现。导致日志无法正确写入日志文件内。
  为什么会造成这个原因呢?
  在方法 doChangeFile中,我们每次输出完self.stream中的信息后,都把stream关闭了 self.stream.close():

 def doChangeFile(self):
        """输出信息到日志文件,并删除多于保留个数的所有日志文件"""
        #日志文件的绝对路径
        self.baseFilename = os.path.abspath(self.filePath)
        #stream == OutStream
        #stream is not None 表示 OutStream中还有未输出完的缓存数据
        if self.stream:
            self.stream.flush()
            self.stream.close()
        #delay 为False 表示 不OutStream不缓存数据 直接输出
        #   所有,只需要关闭OutStream即可
        if not self.delay:
            self.stream.close()

logging调用我们覆盖的emit方法
doChangeFile关闭了stream,
当 logging.FileHandler.emit(self,record)
的时候 stream其实已经关闭了。

    def emit(self, record):
        """发送一个日志记录
        覆盖FileHandler中的emit方法,logging会自动调用此方法"""
        try:
            if self.shouldChangeFileToWrite():
                self.doChangeFile()
            #此时 stram已经关闭
            logging.FileHandler.emit(self,record)
        except (KeyboardInterrupt,SystemExit):
            raise
        except:
            self.handleError(record)

我们看一下 logging.FileHandler.emit的源码:

    def emit(self, record):
        """
        Emit a record.

        If the stream was not opened because 'delay' was specified in the
        constructor, open it before calling the superclass's emit.
        """
        if self.stream is None:
            #打开stream
            self.stream = self._open()
        StreamHandler.emit(self, record)

logging.FileHandler.emit中 检查 当stream为 None的情况下 重新打开 steam
然而我们在doChangeFile中仅仅关闭了stream stram.close()但是并没有设置stream为 None。关闭的stream仍然还是 标准流对象,并不会成为None

#coding=utf-8
import sys
#stream 就是标准输出流,或者标准错误流,logging源码中默认的是标准错误流
#我们来看一下stream是什么东西
stream = sys.stdout
#可以看到是一个file对象
print type(stream)
#写入文件,刷新缓冲区(如果没有设置缓冲区,则可以不刷新)关闭流
stream.write('abc\n')
stream.flush()
stream.close()
#流关闭后,还会是file对象么
#是的 关闭后仍然是file对象
print type(stream)
#可以看到 报错信息为 对已经关闭的文件对象file进行io操作,说明sream关闭后仍然是file对象。
#所以说我们需要 将已经关闭的stream设置为None,srteam = None
# 避免对已关闭的文件对象进行i0操作。
```
![image.png](http://upload-images.jianshu.io/upload_images/4131789-0ff3830bc4ce6dba.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
找到问题所在 那么久好办咯:
在doChangeFile中将关闭后的stream 重新设置为 None即可
```
        if self.stream:
            #flush close 都会刷新缓冲区,flush不会关闭stream,close则关闭stream
            #self.stream.flush()
            self.stream.close()
            #关闭stream后必须重新设置stream为None,否则会造成对已关闭文件进行IO操作。
            self.stream = None
        #delay 为False 表示 不OutStream不缓存数据 直接输出
        #   所有,只需要关闭OutStream即可
        if not self.delay:
            #这个地方如果关闭colse那么就会造成进程往已关闭的文件中写数据,从而造成IO错误
            #delay == False 表示的就是 不缓存直接写入磁盘
            #我们需要重新在打开一次stream
            #self.stream.close()
            self.stream = self._open()
```
if not self.delay中为甚要打开stream内
![image.png](http://upload-images.jianshu.io/upload_images/4131789-5033b4310e35461b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
在这里我们可以看到 
delay为False的时候 需要打开stream
FileHandler_init我们在 我们写的多进程版Handler_init中已经提前初始化了。多进程后面使用中可能会造成stream关闭。所以再打开一次。

这样就好了 
改正后完整的代码如下:
```
#coding=utf-8
import os
import re
import datetime
import logging

try:
    import codecs
except ImportError:
    codecs = None

class MultiprocessHandler(logging.FileHandler):
    """支持多进程的TimedRotatingFileHandler"""
    def __init__(self,filename,when='D',backupCount=0,encoding=None,delay=False):
        """filename 日志文件名,when 时间间隔的单位,backupCount 保留文件个数
        delay 是否开启 OutSteam缓存
            True 表示开启缓存,OutStream输出到缓存,待缓存区满后,刷新缓存区,并输出缓存数据到文件。
            False表示不缓存,OutStrea直接输出到文件"""
        self.prefix = filename
        self.backupCount = backupCount
        self.when = when.upper()
        # 正则匹配 年-月-日
        self.extMath = r"^\d{4}-\d{2}-\d{2}"

        # S 每秒建立一个新文件
        # M 每分钟建立一个新文件
        # H 每天建立一个新文件
        # D 每天建立一个新文件
        self.when_dict = {
            'S':"%Y-%m-%d-%H-%M-%S",
            'M':"%Y-%m-%d-%H-%M",
            'H':"%Y-%m-%d-%H",
            'D':"%Y-%m-%d"
        }
        #日志文件日期后缀
        self.suffix = self.when_dict.get(when)
        if not self.suffix:
            raise ValueError(u"指定的日期间隔单位无效: %s" % self.when)
        #拼接文件路径 格式化字符串
        self.filefmt = os.path.join("logs","%s.%s" % (self.prefix,self.suffix))
        #使用当前时间,格式化文件格式化字符串
        self.filePath = datetime.datetime.now().strftime(self.filefmt)
        #获得文件夹路径
        _dir = os.path.dirname(self.filefmt)
        try:
            #如果日志文件夹不存在,则创建文件夹
            if not os.path.exists(_dir):
                os.makedirs(_dir)
        except Exception:
            print u"创建文件夹失败"
            print u"文件夹路径:" + self.filePath
            pass

        if codecs is None:
            encoding = None

        logging.FileHandler.__init__(self,self.filePath,'a+',encoding,delay)

    def shouldChangeFileToWrite(self):
        """更改日志写入目的写入文件
        :return True 表示已更改,False 表示未更改"""
        #以当前时间获得新日志文件路径
        _filePath = datetime.datetime.now().strftime(self.filefmt)
        #新日志文件日期 不等于 旧日志文件日期,则表示 已经到了日志切分的时候
        #   更换日志写入目的为新日志文件。
        #例如 按 天 (D)来切分日志
        #   当前新日志日期等于旧日志日期,则表示在同一天内,还不到日志切分的时候
        #   当前新日志日期不等于旧日志日期,则表示不在
        #同一天内,进行日志切分,将日志内容写入新日志内。
        if _filePath != self.filePath:
            self.filePath = _filePath
            return True
        return False

    def doChangeFile(self):
        """输出信息到日志文件,并删除多于保留个数的所有日志文件"""
        #日志文件的绝对路径
        self.baseFilename = os.path.abspath(self.filePath)
        #stream == OutStream
        #stream is not None 表示 OutStream中还有未输出完的缓存数据
        if self.stream:
            #flush close 都会刷新缓冲区,flush不会关闭stream,close则关闭stream
            #self.stream.flush()
            self.stream.close()
            #关闭stream后必须重新设置stream为None,否则会造成对已关闭文件进行IO操作。
            self.stream = None
        #delay 为False 表示 不OutStream不缓存数据 直接输出
        #   所有,只需要关闭OutStream即可
        if not self.delay:
            #这个地方如果关闭colse那么就会造成进程往已关闭的文件中写数据,从而造成IO错误
            #delay == False 表示的就是 不缓存直接写入磁盘
            #我们需要重新在打开一次stream
            #self.stream.close()
            self.stream = self._open()
        #删除多于保留个数的所有日志文件
        if self.backupCount > 0:
            print '删除日志'
            for s in self.getFilesToDelete():
                print s
                os.remove(s)

    def getFilesToDelete(self):
        """获得过期需要删除的日志文件"""
        #分离出日志文件夹绝对路径
        #split返回一个元组(absFilePath,fileName)
        #例如:split('I:\ScripPython\char4\mybook\util\logs\mylog.2017-03-19)
        #返回(I:\ScripPython\char4\mybook\util\logs, mylog.2017-03-19)
        # _ 表示占位符,没什么实际意义,
        dirName,_ = os.path.split(self.baseFilename)
        fileNames = os.listdir(dirName)
        result = []
        #self.prefix 为日志文件名 列如:mylog.2017-03-19 中的 mylog
        #加上 点号 . 方便获取点号后面的日期
        prefix = self.prefix + '.'
        plen = len(prefix)
        for fileName in fileNames:
            if fileName[:plen] == prefix:
                #日期后缀 mylog.2017-03-19 中的 2017-03-19
                suffix = fileName[plen:]
                #匹配符合规则的日志文件,添加到result列表中
                if re.compile(self.extMath).match(suffix):
                    result.append(os.path.join(dirName,fileName))
        result.sort()

        #返回  待删除的日志文件
        #   多于 保留文件个数 backupCount的所有前面的日志文件。
        if len(result) < self.backupCount:
            result = []
        else:
            result = result[:len(result) - self.backupCount]
        return result

    def emit(self, record):
        """发送一个日志记录
        覆盖FileHandler中的emit方法,logging会自动调用此方法"""
        try:
            if self.shouldChangeFileToWrite():
                self.doChangeFile()
            logging.FileHandler.emit(self,record)
        except (KeyboardInterrupt,SystemExit):
            raise
        except:
            self.handleError(record)
```
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • 本篇文章主要对 python logging 的介绍加深理解。更主要是 讨论在多进程环境下如何使用logging ...
    doudou0o阅读 40,949评论 52 42
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • Python logging 模块 参考 http://blog.csdn.net/zyz511919766/ar...
    ktide阅读 885评论 0 2
  • 前言 在自动化测试实践过程中,必不可少的就是进行日志管理,方便调试和生产问题追踪,python提供了logg...
    苦叶子阅读 805评论 0 0
  • 本文翻译自logging howto 基础教程 日志是跟踪软件运行时发生事件的一种手段。Python开发者在代码中...
    大蟒传奇阅读 4,246评论 0 17