Python模块学习--logging

前言:

      许多应用程序中都会有日志模块,用于记录系统在运行过程中的一些关键信息,以便于对系统的运行状况进行跟踪。在.NET平台中,有非常著名的第三方开源日志组件log4net,c++中,有人们熟悉的log4cpp,而在python中,我们不需要第三方的日志组件,因为它已经为我们提供了简单易用、且功能强大的日志模块:logging。logging模块支持将日志信息保存到不同的目标域中,如:保存到日志文件中;以邮件的形式发送日志信息;以http get或post的方式提交日志到web服务器;以windows事件的形式记录等等。这些日志保存方式可以组合使用,每种方式可以设置自己的日志级别以及日志格式。


先看一个比较简单的例子,让我们对logging模块有个感性的认识:

# -*- coding: utf-8 -*-
import logging,os
logging.basicConfig(filename = os.path.join(os.getcwd(), 'log.txt'), level = logging.DEBUG)
logging.debug('this is a message')
运行上面例子的代码,将会在程序的根目录下创建一个log.txt文件,打开该文件,里面有一条日志记录:”DEBUG:root:this is a message”。


4个主要的组件
logger: 提供了应用程序可以直接使用的接口;
handler: 将(logger创建的)日志记录发送到合适的目的输出;
filter: 提供了细度设备来决定输出哪条日志记录;
formatter:决定日志记录的最终输出格式。


模块级函数
logging.getLogger([name]):返回一个logger对象,如果没有指定名字将返回root logger
logging.notset()、logging.debug()、logging.info()、logging.warning()、logging.error()、logging.critical():设定root logger的日志级别

logging.basicConfig():用默认Formatter为日志系统建立一个StreamHandler,设置基础配置并加到root logger中。kwargs支持如下几个关键字参数:详细的格式介绍就查看官方文档
  filename-->日志文件的保存路径。如果配置了些参数,将自动创建一个FileHandler作为Handler;
  filemode-->日志文件的打开模式。 默认值为’a’,表示日志消息以追加的形式添加到日志文件中。如果设为’w’, 那么每次程序启动的时候都会创建一个新的日志文件;
  format-->设置日志输出格式;
  datefmt-->定义日期格式;
  level-->设置日志的级别.对低于该级别的日志消息将被忽略;
  stream-->设置特定的流用于初始化StreamHandler;


Logger
每个程序在输出信息之前都要获得一个Logger。Logger通常对应了程序的模块名,比如聊天工具的图形界面模块可以这样获得它的Logger:
Logger=logging.getLogger(”chat.gui”)
而核心模块可以这样:
Logger=logging.getLogger(”chat.kernel”)
Logger.setLevel(lel):指定最低的日志级别,低于lel的级别将被忽略。debug是最低的内置级别,critical为最高
Logger.addFilter(filt)、Logger.removeFilter(filt):添加或删除指定的filter
Logger.addHandler(hdlr)、Logger.removeHandler(hdlr):增加或删除指定的handler
Logger.debug()、Logger.info()、Logger.warning()、Logger.error()、Logger.critical():设置logger的level
level有以下几个级别:


NOTSET < DEBUG < INFO < WARNING < ERROR < CRITICAL
注意:如果把Looger的级别设置为INFO,那么小于INFO级别的日志都不输出,大于等于INFO级别的日志都输出。这样的好处, 就是在项目开发时debug用的log,在产品release阶段不用一一注释,只需要调整logger的级别就可以了,很方便。


Handlers
handler对象负责发送相关的信息到指定目的地。Python的日志系统有多种Handler可以使用。有些Handler可以把信息输出到控制台,有些Logger可以把信息输出到文件,还有些 Handler可以把信息发送到网络上。如果觉得不够用,还可以编写自己的Handler。可以通过addHandler()方法添加多个多handler
Handler.setLevel(lel):指定被处理的信息级别,低于lel级别的信息将被忽略
Handler.setFormatter():给这个handler选择一个格式
Handler.addFilter(filt)、Handler.removeFilter(filt):新增或删除一个filter对象


Formatters

Formatter对象设置日志信息最后的规则、结构和内容,默认的时间格式为%Y-%m-%d %H:%M:%S,下面是Formatter常用的一些信息

%(name)s    Logger的名字
%(levelno)s    数字形式的日志级别
%(levelname)s    文本形式的日志级别
%(pathname)s    调用日志输出函数的模块的完整路径名,可能没有
%(filename)s    调用日志输出函数的模块的文件名
%(module)s    调用日志输出函数的模块名
%(funcName)s    调用日志输出函数的函数名
%(lineno)d    调用日志输出函数的语句所在的代码行
%(created)f    当前时间,用UNIX标准的表示时间的浮 点数表示
%(relativeCreated)d    输出日志信息时的,自Logger创建以 来的毫秒数
%(asctime)s    字符串形式的当前时间。默认格式是 “2003-07-08 16:49:45,896”。逗号后面的是毫秒
%(thread)d    线程ID。可能没有
%(threadName)s    线程名。可能没有
%(process)d    进程ID。可能没有
%(message)s    用户输出的消息
注:一个Handler只能拥有一个Formatter,因此如果要实现多种格式的输出只能用多个Handler来实现。可以给日志对象(Logger Instance)设置日志级别,低于该级别的日志消息将会被忽略,也可以给Hanlder设置日志级别,对于低于该级别的日志消息, Handler也会忽略。


设置过滤器
细心的朋友一定会发现前文调用logging.getLogger()时参数的格式类似于“A.B.C”。采取这样的格式其实就是为了可以配置过滤器。看一下这段代码:
LOG=logging.getLogger(”chat.gui.statistic”)
console = logging.StreamHandler()
console.setLevel(logging.INFO)
formatter = logging.Formatter(’%(asctime)s %(levelname)s %(message)s’)
console.setFormatter(formatter)
filter=logging.Filter(”chat.gui”)
console.addFilter(filter)
LOG.addHandler(console)
和前面不同的是我们在Handler上添加了一个过滤器。现在我们输出日志信息的时候就会经过过滤器的处理。名为“A.B”的过滤器只让名字带有 “A.B”前缀的Logger输出信息。可以添加多个过滤器,只要有一个过滤器拒绝,日志信息就不会被输出。另外,在Logger中也可以添加过滤器。


每个Logger可以附加多个Handler。接下来我们就来介绍一些常用的Handler:
(1)logging.StreamHandler
使用这个Handler可以向类似与sys.stdout或者sys.stderr的任何文件对象(file object)输出信息。它的构造函数是:
StreamHandler([strm])
其中strm参数是一个文件对象。默认是sys.stderr
(2)logging.FileHandler
和StreamHandler类似,用于向一个文件输出日志信息。不过FileHandler会帮你打开这个文件。它的构造函数是:
FileHandler(filename[,mode])
filename是文件名,必须指定一个文件名。
mode是文件的打开方式。参见Python内置函数open()的用法。默认是’a',即添加到文件末尾。
(3)logging.handlers.RotatingFileHandler
这个Handler类似于上面的FileHandler,但是它可以管理文件大小。当文件达到一定大小之后,它会自动将当前日志文件改名,然后创建 一个新的同名日志文件继续输出。比如日志文件是chat.log。当chat.log达到指定的大小之后,RotatingFileHandler自动把 文件改名为chat.log.1。不过,如果chat.log.1已经存在,会先把chat.log.1重命名为chat.log.2。。。最后重新创建 chat.log,继续输出日志信息。它的构造函数是:
RotatingFileHandler( filename[, mode[, maxBytes[, backupCount]]])
其中filename和mode两个参数和FileHandler一样。
maxBytes用于指定日志文件的最大文件大小。如果maxBytes为0,意味着日志文件可以无限大,这时上面描述的重命名过程就不会发生。
backupCount用于指定保留的备份文件的个数。比如,如果指定为2,当上面描述的重命名过程发生时,原有的chat.log.2并不会被更名,而是被删除。
(4)logging.handlers.TimedRotatingFileHandler
这个Handler和RotatingFileHandler类似,不过,它没有通过判断文件大小来决定何时重新创建日志文件,而是间隔一定时间就 自动创建新的日志文件。重命名的过程与RotatingFileHandler类似,不过新的文件不是附加数字,而是当前时间。它的构造函数是:
TimedRotatingFileHandler( filename [,when [,interval [,backupCount]]])
其中filename参数和backupCount参数和RotatingFileHandler具有相同的意义。
interval是时间间隔。
when参数是一个字符串。表示时间间隔的单位,不区分大小写。它有以下取值:
S 秒
M 分
H 小时
D 天
W 每星期(interval==0时代表星期一)
midnight 每天凌晨
(5)logging.handlers.SocketHandler
(6)logging.handlers.DatagramHandler
以上两个Handler类似,都是将日志信息发送到网络。不同的是前者使用TCP协议,后者使用UDP协议。它们的构造函数是:
Handler(host, port)
其中host是主机名,port是端口名
(7)logging.handlers.SysLogHandler
(8)logging.handlers.NTEventLogHandler
(9)logging.handlers.SMTPHandler
(10)logging.handlers.MemoryHandler
(11)logging.handlers.HTTPHandler


Configuration配置方法
logging的配置大致有下面几种方式。
1.通过代码进行完整配置,参考开头的例子,主要是通过getLogger方法实现。
2.通过代码进行简单配置,下面有例子,主要是通过basicConfig方法实现。
3.通过配置文件,下面有例子,主要是通过 logging.config.fileConfig(filepath)
logging.basicConfig
basicConfig()提供了非常便捷的方式让你配置logging模块并马上开始使用,可以参考下面的例子。

import logging
 
logging.basicConfig(filename='example.log',level=logging.DEBUG)
logging.debug('This message should go to the log file')
 
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
logging.debug('This message should appear on the console')
 
logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
logging.warning('is when this event was logged.')
备注:其实你甚至可以什么都不配置直接使用默认值在控制台中打log,用这样的方式替换print语句对日后项目维护会有很大帮助。


通过文件配置logging

如果你希望通过配置文件来管理logging,可以参考这个官方文档。在log4net或者log4j中这是很常见的方式。

# logging.conf
[loggers]
keys=root
 
[logger_root]
level=DEBUG
handlers=consoleHandler
#,timedRotateFileHandler,errorTimedRotateFileHandler
 
#################################################
[handlers]
keys=consoleHandler,timedRotateFileHandler,errorTimedRotateFileHandler
 
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)
 
[handler_timedRotateFileHandler]
class=handlers.TimedRotatingFileHandler
level=DEBUG
formatter=simpleFormatter
args=('debug.log', 'H')
 
[handler_errorTimedRotateFileHandler]
class=handlers.TimedRotatingFileHandler
level=WARN
formatter=simpleFormatter
args=('error.log', 'H')
 
#################################################
[formatters]
keys=simpleFormatter, multiLineFormatter
 
[formatter_simpleFormatter]
format= %(levelname)s %(threadName)s %(asctime)s:   %(message)s
datefmt=%H:%M:%S
 
[formatter_multiLineFormatter]
format= ------------------------- %(levelname)s -------------------------
 Time:      %(asctime)s
 Thread:    %(threadName)s
 File:      %(filename)s(line %(lineno)d)
 Message:
 %(message)s
 
datefmt=%Y-%m-%d %H:%M:%S
假设以上的配置文件放在和模块相同的目录,代码中的调用如下。

import os
filepath = os.path.join(os.path.dirname(__file__), 'logging.conf')
logging.config.fileConfig(filepath)
return logging.getLogger()
下面的代码展示了logging最基本的用法:
例子1:
# -*- coding: utf-8 -*-

import logging
import sys

# 获取logger实例,如果参数为空则返回root logger
logger = logging.getLogger("AppName")

# 指定logger输出格式
formatter = logging.Formatter('%(asctime)s %(levelname)-8s: %(message)s')
 
# 文件日志
file_handler = logging.FileHandler("test.log")
file_handler.setFormatter(formatter)  # 可以通过setFormatter指定输出格式

# 控制台日志
console_handler = logging.StreamHandler(sys.stdout)
console_handler.formatter = formatter  # 也可以直接给formatter赋值

# 为logger添加的日志处理器
logger.addHandler(file_handler)
logger.addHandler(console_handler)

# 指定日志的输出级别,默认为WARN级别
logger.setLevel(logging.INFO)

# 输出不同级别的log
logger.debug('this is debug info')  #被忽略
logger.info('this is information')
logger.warn('this is warning message')  #写成logger.warning也可以
logger.error('this is error message')
logger.fatal('this is fatal message, it is same as logger.critical')
logger.critical('this is critical message')

# 移除一些日志处理器
logger.removeHandler(file_handler)
运行代码:python log.py
输出结果:
2018-01-03 11:48:19,714 INFO    : this is information
2018-01-03 11:48:19,714 WARNING : this is warning message
2018-01-03 11:48:19,714 ERROR   : this is error message
2018-01-03 11:48:19,714 CRITICAL: this is fatal message, it is same as logger.critical
2018-01-03 11:48:19,714 CRITICAL: this is critical message


例子2:

# -*- coding: utf-8 -*-

import logging
import random

class OddFilter(logging.Filter):
    def __init__(self):
        self.count = 0

    def filter(self, record):
        self.count += 1
        if record.args[0] & 1:
            record.count = self.count  # 给 record 增加了 count 属性
            return True  # 为 True 的记录才输出
        return False

root_logger = logging.getLogger()
logging.basicConfig(
    level=logging.NOTSET,
    format='%(asctime)s %(message)s (total: %(count)d)',
    datefmt='%a, %d %b %Y %H:%M:%S',
    filename='log.test',
    filemode='w'
)
root_logger.level = logging.ERROR
root_logger.addFilter(OddFilter())

for i in xrange(10):
    logging.error('number: %d', random.randint(0, 100))
查看生成的log.test文件:
Wed, 03 Jan 2018 13:08:27 number: 75 (total: 3)
Wed, 03 Jan 2018 13:08:27 number: 19 (total: 6)
Wed, 03 Jan 2018 13:08:27 number: 97 (total: 7)
Wed, 03 Jan 2018 13:08:27 number: 55 (total: 10)


除了这些基本用法,还有一些常见的小技巧可以分享一下:
格式化输出日志

service_name = "Booking"
logger.error('%s service is down!' % service_name)  # 使用python自带的字符串格式化,不推荐
logger.error('%s service is down!', service_name)  # 使用logger的格式化,推荐
logger.error('%s service is %s!', service_name, 'down')  # 多参数格式化
logger.error('{} service is {}'.format(service_name, 'down')) # 使用format函数,推荐
 
# 2018-01-03 11:48:19,714 ERROR   : Booking service is down!
记录异常信息
当你使用logging模块记录异常信息时,不需要传入该异常对象,只要你直接调用logger.error() 或者 logger.exception()就可以将当前异常记录下来。
try:
    1 / 0
except:
    # 等同于error级别,但是会额外记录当前抛出的异常堆栈信息
    logger.exception('this is an exception message')
 
# 2018-01-03 11:48:19,714 ERROR   : this is an exception message
# Traceback (most recent call last):
#   File "D:/Git/py_labs/demo/use_logging.py", line 45, in 
#     1 / 0
# ZeroDivisionError: integer division or modulo by zero
logging配置要点
GetLogger()方法
这是最基本的入口,该方法参数可以为空,默认的logger名称是root,如果在同一个程序中一直都使用同名的logger,其实会拿到同一个实例,使用这个技巧就可以跨模块调用同样的logger来记录日志。
另外你也可以通过日志名称来区分同一程序的不同模块,比如这个例子。
logger = logging.getLogger("App.UI")
logger = logging.getLogger("App.Service")


logging是线程安全的么?
是的,handler内部使用了threading.RLock()来保证同一时间只有一个线程能够输出。
但是,在使用logging.FileHandler时,多进程同时写一个日志文件是不支持的


logging遇到多进程
python中由于某种历史原因,多线程的性能基本可以无视。所以一般情况下python要实现并行操作或者并行计算的时候都是使用多进程。但是python中logging并不支持多进程,所以会遇到不少麻烦。
本次就以TimedRotatingFileHandler这个类的问题作为例子。这个Handler本来的作用是:按天切割日志文件。(当天的文件是xxxx.log昨天的文件是xxxx.log.2016-06-01)。这样的好处是,一来可以按天来查找日志,二来可以让日志文件不至于非常大, 过期日志也可以按天删除。
但是问题来了,如果是用多进程来输出日志,则只有一个进程会切换,其他进程会在原来的文件中继续打,还有可能某些进程切换的时候早就有别的进程在新的日志文件里打入东西了,那么他会无情删掉之,再建立新的日志文件。反正将会很乱很乱,完全没法开心的玩耍。
所以这里就想了几个办法来解决多进程logging问题

原因:在解决之前,我们先看看为什么会导致这样的原因。
先将 TimedRotatingFileHandler 的源代码贴上来,这部分是切换时所作的操作:

    def doRollover(self):
        """
        do a rollover; in this case, a date/time stamp is appended to the filename
        when the rollover happens.  However, you want the file to be named for the
        start of the interval, not the current time.  If there is a backup count,
        then we have to get a list of matching filenames, sort them and remove
        the one with the oldest suffix.
        """
        if self.stream:
            self.stream.close()
            self.stream = None
        # get the time that this sequence started at and make it a TimeTuple
        currentTime = int(time.time())
        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.baseFilename + "." + time.strftime(self.suffix, timeTuple)
        if os.path.exists(dfn):
            os.remove(dfn)
        # Issue 18940: A file may not have been created if delay is True.
        if os.path.exists(self.baseFilename):
            os.rename(self.baseFilename, dfn)
        if self.backupCount > 0:
            for s in self.getFilesToDelete():
                os.remove(s)
        if not self.delay:
            self.stream = self._open()
        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]
            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
我们观察 if os.path.exists(dfn) 这一行开始,这里的逻辑是如果 dfn 这个文件存在,则要先删除掉它,然后将 baseFilename 这个文件重命名为 dfn 文件。然后再重新打开 baseFilename这个文件开始写入东西。那么这里的逻辑就很清楚了
1.假设当前日志文件名为 current.log 切分后的文件名为 current.log.2016-06-01
2.判断 current.log.2016-06-01 是否存在,如果存在就删除
3.将当前的日志文件名 改名为current.log.2016-06-01
4.重新打开新文件(我观察到源代码中默认是”a” 模式打开,之前据说是”w”)
于是在多进程的情况下,一个进程切换了,其他进程的句柄还在 current.log.2016-06-01 还会继续往里面写东西。又或者一个进程执行切换了,会把之前别的进程重命名的 current.log.2016-06-01 文件直接删除。又或者还有一个情况,当一个进程在写东西,另一个进程已经在切换了,会造成不可预估的情况发生。还有一种情况两个进程同时在切文件,第一个进程正在执行第3步,第二进程刚执行完第2步,然后第一个进程 完成了重命名但还没有新建一个新的 current.log 第二个进程开始重命名,此时第二个进程将会因为找不到 current 发生错误。如果第一个进程已经成功创建了 current.log 第二个进程会将这个空文件另存为 current.log.2016-06-01。那么不仅删除了日志文件,而且,进程一认为已经完成过切分了不会再切,而事实上他的句柄指向的是current.log.2016-06-01。
好了这里看上去很复杂,实际上就是因为对于文件操作时,没有对多进程进行一些约束,而导致的问题。
那么如何优雅地解决这个问题呢。我提出了两种方案,当然我会在下面提出更多可行的方案供大家尝试。


解决方案1
先前我们发现 TimedRotatingFileHandler 中逻辑的缺陷。我们只需要稍微修改一下逻辑即可:
1.判断切分后的文件 current.log.2016-06-01 是否存在,如果不存在则进行重命名。(如果存在说明有其他进程切过了,我不用切了,换一下句柄即可)
2.以”a”模式 打开 current.log
发现修改后就这么简单~
talking is cheap show me the code:

class SafeRotatingFileHandler(TimedRotatingFileHandler):
 def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False):
     TimedRotatingFileHandler.__init__(self, filename, when, interval, backupCount, encoding, delay, utc)
 """
 Override doRollover
 lines commanded by "##" is changed by cc
 """
 def doRollover(self):
     """
     do a rollover; in this case, a date/time stamp is appended to the filename
     when the rollover happens.  However, you want the file to be named for the
     start of the interval, not the current time.  If there is a backup count,
     then we have to get a list of matching filenames, sort them and remove
     the one with the oldest suffix.

     Override,   1. if dfn not exist then do rename
                 2. _open with "a" model
     """
     if self.stream:
         self.stream.close()
         self.stream = None
     # get the time that this sequence started at and make it a TimeTuple
     currentTime = int(time.time())
     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.baseFilename + "." + time.strftime(self.suffix, timeTuple)
##        if os.path.exists(dfn):
##            os.remove(dfn)

     # Issue 18940: A file may not have been created if delay is True.
##        if os.path.exists(self.baseFilename):
     if not os.path.exists(dfn) and os.path.exists(self.baseFilename):
         os.rename(self.baseFilename, dfn)
     if self.backupCount > 0:
         for s in self.getFilesToDelete():
             os.remove(s)
     if not self.delay:
         self.mode = "a"
         self.stream = self._open()
     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]
         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
    不要以为代码那么长,其实修改部分就是 “##” 注释的地方而已,其他都是照抄源代码。这个类继承了 TimedRotatingFileHandler 重写了这个切分的过程。这个解决方案十分优雅,改换的地方非常少,也十分有效。但有网友提出,这里有一处地方依然不完美,就是rename的那一步,如果就是这么巧,同时两个或者多个进程进入了 if 语句,先后开始 rename 那么依然会发生删除掉日志的情况。确实这种情况确实会发生,由于切分文件一天才一次,正好切分的时候同时有两个Handler在操作,又正好同时走到这里,也是蛮巧的,但是为了完美,可以加上一个文件锁,if 之后加锁,得到锁之后再判断一次,再进行rename这种方式就完美了。代码就不贴了,涉及到锁代码,影响美观。

解决方案2

我认为最简单有效的解决方案。重写FileHandler类(这个类是所有写入文件的Handler都需要继承的TimedRotatingFileHandler 就是继承的这个类;我们增加一些简单的判断和操作就可以。
我们的逻辑是这样的:
1.判断当前时间戳是否与指向的文件名是同一个时间
2.如果不是,则切换 指向的文件即可
结束,是不是很简单的逻辑。
talking is cheap show me the code:

class SafeFileHandler(FileHandler):
 def __init__(self, filename, mode, encoding=None, delay=0):
     """
     Use the specified filename for streamed logging
     """
     if codecs is None:
         encoding = None
     FileHandler.__init__(self, filename, mode, encoding, delay)
     self.mode = mode
     self.encoding = encoding
     self.suffix = "%Y-%m-%d"
     self.suffix_time = ""

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

     Always check time 
     """
     try:
         if self.check_baseFilename(record):
             self.build_baseFilename()
         FileHandler.emit(self, record)
     except (KeyboardInterrupt, SystemExit):
         raise
     except:
         self.handleError(record)

 def check_baseFilename(self, record):
     """
     Determine if builder should occur.

     record is not used, as we are just comparing times, 
     but it is needed so the method signatures are the same
     """
     timeTuple = time.localtime()

     if self.suffix_time != time.strftime(self.suffix, timeTuple) or not os.path.exists(self.baseFilename+'.'+self.suffix_time):
         return 1
     else:
         return 0
 def build_baseFilename(self):
     """
     do builder; in this case, 
     old time stamp is removed from filename and
     a new time stamp is append to the filename
     """
     if self.stream:
         self.stream.close()
         self.stream = None

     # remove old suffix
     if self.suffix_time != "":
         index = self.baseFilename.find("."+self.suffix_time)
         if index == -1:
             index = self.baseFilename.rfind(".")
         self.baseFilename = self.baseFilename[:index]

     # add new suffix
     currentTimeTuple = time.localtime()
     self.suffix_time = time.strftime(self.suffix, currentTimeTuple)
     self.baseFilename  = self.baseFilename + "." + self.suffix_time

     self.mode = 'a'
     if not self.delay:
         self.stream = self._open()
       check_baseFilename 就是执行逻辑1判断;build_baseFilename 就是执行逻辑2换句柄。就这么简单完成了。
这种方案与之前不同的是,当前文件就是 current.log.2016-06-01 ,到了明天当前文件就是current.log.2016-06-02 没有重命名的情况,也没有删除的情况。十分简洁优雅。也能解决多进程的logging问题。


解决方案其他
      当然还有其他的解决方案,例如由一个logging进程统一打日志,其他进程将所有的日志内容打入logging进程管道由它来打理。还有将日志打入网络socket当中也是同样的道理。


logging的流程是怎样的?
这里有张流程图可以参考:https://docs.python.org/2/howto/logging.html#logging-flow

参考:
http://python.jobbole.com/81521/?utm_source=blog.jobbole.com&utm_medium=relatedPosts
http://python.jobbole.com/86887/?utm_source=blog.jobbole.com&utm_medium=relatedPosts
http://python.jobbole.com/84092/
https://my.oschina.net/leejun2005/blog/126713
http://python.jobbole.com/87300/?utm_source=blog.jobbole.com&utm_medium=relatedPosts


猜你喜欢

转载自blog.csdn.net/m0_37739193/article/details/79219489