Python 多进程 日志

  最近需要给多进程加日志  但是在加的过程中出现了诸多问题,比如日志丢失,代码报错等一切问题。经过一番查询找到了大牛的解决方案,可行,在这里进行记录以及帮助大家。

        目前这个还是丢日志  后期加了我自己的解决方法

# coding=utf-8
import os
import sys
import re
import datetime
import time
import logging
import multiprocessing

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)


def init_logging_handler():
    # 定义日志输出格式
    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)
    return logger


logger = init_logging_handler()


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('完毕')

  借鉴地址:https://www.jianshu.com/p/d874a05edf19

日志不丢失的代码:

# coding=utf-8
import os
import sys
import re
import datetime
import time
import logging
import multiprocessing

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)


def init_logging_handler():
    # 定义日志输出格式
    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='D')
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(fmt)

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


logger = init_logging_handler()


def test(num, m_queue):
    time.sleep(2)
    m_queue.put('日志测试' + str(num))


def write_logging(m_queue):
    while True:
        data = m_queue.get()
        if data:
            logger.debug(data)


if __name__ == '__main__':
    m_queue = multiprocessing.Manager().Queue(maxsize=1000)
    multiprocessing.Manager().Queue()
    pool = multiprocessing.Pool(processes=10)

    p1 = multiprocessing.Process(target=write_logging, args=(m_queue,))
    p1.start()

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

  解决方法  再加一个进程,专门加一个日志管理进程。

 

 

在测试的过程中上面的代码因为监控日志所以是死循环 程序不能结束 可以将里面的函数替换下面代码  10秒没有新日志 就退出程序  这里的信号可以大家自己设置 比如监控进程 或者有特殊信号什么的  大家根据自己的需求而定就行

 

def write_logging(m_queue):
    num = 0
    while True:
        if m_queue.qsize() != 0:
            num = 0
            data = m_queue.get()
            logger.debug(data)
        else:
            time.sleep(1)
            num += 1

        if num == 10:
            break

  

 

posted @ 2020-11-11 11:45  IT小平  阅读(849)  评论(0)    收藏  举报