python 跨模块实现按照文件大小,日期实现日志分割,反转
笔者的一个自动化测试平台项目,采用了python作为后端服务器语言。项目基于快速成型目的,写了一个极其简陋的日志记录功能,支持日志记录到文件和支持根据日志级别在终端打印不同颜色的log。但随着测试平台上线运行,发现日志文件大小急剧膨胀,运行一段时间,往往一个log能有几个G大小,而且也不能根据日期查看日志内容。基于根据文件大小和日志实现日志分割,在下查阅了不少前辈的资料,不断尝试,终于得出一个可以用的demo,在此分享也做个记录,不足之处,还望指正。
这是本人工作前辈的初始版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | #!/usr/bin/python # -*- coding: utf-8 -*- import os import time import logging import platform if platform.system() = = 'Windows' : from ctypes import windll, c_ulong def color_text_decorator(function): def real_func( self , string): windll.Kernel32.GetStdHandle.restype = c_ulong h = windll.Kernel32.GetStdHandle(c_ulong( 0xfffffff5 )) if function.__name__.upper() = = 'ERROR' : windll.Kernel32.SetConsoleTextAttribute(h, 12 ) elif function.__name__.upper() = = 'WARN' : windll.Kernel32.SetConsoleTextAttribute(h, 13 ) elif function.__name__.upper() = = 'INFO' : windll.Kernel32.SetConsoleTextAttribute(h, 14 ) elif function.__name__.upper() = = 'DEBUG' : windll.Kernel32.SetConsoleTextAttribute(h, 15 ) else : windll.Kernel32.SetConsoleTextAttribute(h, 15 ) function( self , string) windll.Kernel32.SetConsoleTextAttribute(h, 15 ) return real_func else : def color_text_decorator(function): def real_func( self , string): if function.__name__.upper() = = 'ERROR' : self .stream.write( '\033[0;31;40m' ) elif function.__name__.upper() = = 'WARN' : self .stream.write( '\033[0;35;40m' ) elif function.__name__.upper() = = 'INFO' : self .stream.write( '\033[0;33;40m' ) elif function.__name__.upper() = = 'DEBUG' : self .stream.write( '\033[0;37;40m' ) else : self .stream.write( '\033[0;37;40m' ) function( self , string) self .stream.write( '\033[0m' ) return real_func FORMAT = '[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s' class Logger( object ): DEBUG_MODE = True LOG_LEVEL = 5 GLOBAL_FILENAME = 'static/testlog/syslog/atc.log' def __init__( self , name, filename = None ): current_path = os.path.join(os.path.dirname( os.path.abspath(__file__)), 'static' , 'testlog/syslog' ) if not os.path.exists(current_path): os.makedirs(current_path) # baseconfig logging.basicConfig() self .logger = logging.getLogger(name) self .logger.setLevel(logging.DEBUG) formatter = logging.Formatter( FORMAT ) # output to terminal sh = logging.StreamHandler() sh.setFormatter(formatter) sh.setLevel(logging.DEBUG if self .DEBUG_MODE else logging.INFO) self .logger.addHandler(sh) self .stream = sh.stream # output to global file if self .GLOBAL_FILENAME: fh_all = logging.FileHandler( self .GLOBAL_FILENAME, 'a' ) #fh_all = logging.handlers.TimedRotatingFileHandler(self.GLOBAL_FILENAME,'M',1,0) #fh_all.suffix ="_%Y_%m_%d-%H_%M.log" fh_all.setFormatter(formatter) fh_all.setLevel(logging.DEBUG) self .logger.addHandler(fh_all) self .logger.propagate = 0 # output to user define file if filename is not None : fh = logging.FileHandler(filename, 'a' ) fh.setFormatter(formatter) fh.setLevel(logging.DEBUG) self .logger.addHandler(fh) self .logger.propagate = 0 @color_text_decorator def hint( self , string): # 去除多余连续空格 strTmp = str (string) strTmp = ' ' .join(strTmp.split()) if self .LOG_LEVEL > = 5 : return self .logger.debug(strTmp) else : pass @color_text_decorator def debug( self , string): # 去除多余连续空格 strTmp = str (string) strTmp = ' ' .join(strTmp.split()) if self .LOG_LEVEL > = 4 : return self .logger.debug(strTmp) else : pass @color_text_decorator def info( self , string): # 去除多余连续空格 strTmp = str (string) strTmp = ' ' .join(strTmp.split()) if self .LOG_LEVEL > = 3 : return self .logger.info(strTmp) else : pass @color_text_decorator def warn( self , string): # 去除多余连续空格 strTmp = str (string) strTmp = ' ' .join(strTmp.split()) if self .LOG_LEVEL > = 2 : return self .logger.warn(strTmp) else : pass @color_text_decorator def error( self , string): # 去除多余连续空格 strTmp = str (string) strTmp = ' ' .join(strTmp.split()) if self .LOG_LEVEL > = 1 : return self .logger.error(strTmp) else : pass class TestLogModule( object ): def __init__( self ): pass def runtest( self ): logger = Logger( 'TEST' ) iCount = 0 while True : iCount = iCount + 1 logger.error( str (iCount)) logger.debug( '1 22 333 4444 55555 666666' ) logger.info( '1 22 333 4444 55555 666666' ) logger.warn( '1 22 333 4444 55555 666666' ) logger.error( '1 22 333 4444 55555 666666' ) time.sleep( 1 ) if iCount > = 120 : break # for a in xrange(10): # logger.debug('1 22 333 4444 55555 666666') # logger.info('1 22 333 4444 55555 666666') # logger.warn('1 22 333 4444 55555 666666') # logger.error('1 22 333 4444 55555 666666') # time.sleep(1) if __name__ = = '__main__' : TestLogModule().runtest() |
我们的需求可以用logging.handlers实现,具体方法为logging.handlers.TimedRotatingFileHandler和logging.handlers.RotatingFileHandler。
class logging.handlers.RotatingFileHandler(filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=0) 返回RotatingFileHandler类的实例。指明的文件被打开,并被用作日志流。如果没有指明mode,使用'a'。如果encoding不为None,使用指定的编码来打开文件。如果delay为真,只到第一次调用emit()的时候才打开文件。默认情况下,文件会一直增长。 可以使用maxBytes 和 backupCount 来让文件在预定义的尺寸发生翻转。当文件大小大概要超出时,文件被关闭,新文件被打开用来输出。当文件大小接近于maxBytes长度时,翻转会发生;如果maxBytes为0,翻转永不发生。如果backupCount不为0,系统将保存老的日志文件,在文件名后加上‘.1’, ‘.2’这样的扩展名。例如如果backupCount是5,基本的文件名是app.log,将会得到app.log, app.log.1, app.log.2到 app.log.5。总是写到文件app.log中。当文件被填满,文件被关闭并重命名为app.log.1,而已存的app.log.1, app.log.2等文件被重命名为app.log.2, app.log.3等。 改变于版本2.6:新增了delay。 doRollover() 如上所述做文件的翻转。 emit(record) 输出记录到文件,负责文件的翻转。
class logging.handlers.TimedRotatingFileHandler(filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False) 返回TimedRotatingFileHandler类的实例。 指明的文件被打开,并用作日志流。在循环时它也会设置文件后缀。循环发生基于when 和 interval的乘积。 使用when来指明interval的类型。可能的值列在下面。注意大小写不敏感。 Value Type of interval 'S' Seconds 'M' Minutes 'H' Hours 'D' Days 'W0'-'W6' Weekday (0=Monday) 'midnight' Roll over at midnight 注意在使用基于工作日的循环时,‘W0’表示星期一,‘W1’表示星期二,依此类推,‘W6’表示星期日。这种情况下不使用interval。 系统会保存老的日志文件,在文件名后添加扩展名。扩展名基于日期和时间,根据翻转间隔,使用strftime格式%Y-%m-%d_%H-%M-%S,或者其前面一部分。 第一次计算下一次翻转时间的时候(创建handler时),要么使用已存文件的上一次修改时间,要么使用当前时间。 如果utc为真,使用UTC时间;否则使用本地时间。 如果backupCount不为0,最多保留backupCount个文件,如果产生更多的文件,最老的文件会被删除。删除逻辑使用间隔来决定删除哪些文件,所以改变间隔可能会导致老的文件被保留。 如果delay为真,只到第一次调用emit()时文件才被打开。 改变于版本2.6:新增了delay和utc。 doRollover() 如上所述做文件的翻转。 emit(record) 输出记录到文件,负责文件的翻转。
改良第一步:将logging.handlers.TimedRotatingFileHandler和logging.handlers.RotatingFileHandler添加到初始版本中去
Logger类新增logginghandlers.TimeRotatingFileHandler和logging.handlers.RotatingFileHandler 的handler
class Logger(object): DEBUG_MODE = True LOG_LEVEL = 5 GLOBAL_FILENAME = 'static/testlog/syslog/atc.log' def __init__(self, name, filename=None): current_path = os.path.join(os.path.dirname( os.path.abspath(__file__)), 'static', 'testlog/syslog') if not os.path.exists(current_path): os.makedirs(current_path) # baseconfig logging.basicConfig() self.logger = logging.getLogger(name) self.logger.setLevel(logging.DEBUG) formatter = logging.Formatter(FORMAT) # output to terminal sh = logging.StreamHandler() sh.setFormatter(formatter) sh.setLevel(logging.DEBUG if self.DEBUG_MODE else logging.INFO) self.logger.addHandler(sh) self.stream = sh.stream # output to global file if self.GLOBAL_FILENAME: th_all = logging.handlers.TimedRotatingFileHandler(self.GLOBAL_FILENAME, when='midnight',interval=1, backupCount=7) th_all.setFormatter(formatter) th_all.setLevel(logging.DEBUG) self.logger.addHandler(th_all) # self.logger.propagate = 0 rh_all = logging.handlers.RotatingFileHandler('static/testlog/syslog/rf.log', mode='a',maxBytes=2000*2000, backupCount=3) rh_all.setFormatter(formatter) rh_all.setLevel(logging.DEBUG) self.logger.addHandler(rh_all) # self.logger.propagate = 0 # output to user define file if filename is not None: fh = logging.FileHandler(filename, 'a') fh.setFormatter(formatter) fh.setLevel(logging.DEBUG) self.logger.addHandler(fh) self.logger.propagate = 0
实际结果并不如人意,新添加的两个handler在日志发生反转的时候,新建立的日志文件并不能把各模块的日志输出记录下来,会出现只记录了一部分模块日志的情况。
再次修改,这次我们为了移除模块共写一个文件的影响,另外新建一个mainlogger,由它记录各模块的日志输出,并且在日志反转时,新建日志文件。
#!/usr/bin/python # -*- coding: utf-8 -*- import os import time import logging import logging.handlers import platform if platform.system() == 'Windows': from ctypes import windll, c_ulong def color_text_decorator(function): def real_func(self, string): windll.Kernel32.GetStdHandle.restype = c_ulong h = windll.Kernel32.GetStdHandle(c_ulong(0xfffffff5)) if function.__name__.upper() == 'ERROR': windll.Kernel32.SetConsoleTextAttribute(h, 12) elif function.__name__.upper() == 'WARN': windll.Kernel32.SetConsoleTextAttribute(h, 13) elif function.__name__.upper() == 'INFO': windll.Kernel32.SetConsoleTextAttribute(h, 14) elif function.__name__.upper() == 'DEBUG': windll.Kernel32.SetConsoleTextAttribute(h, 15) else: windll.Kernel32.SetConsoleTextAttribute(h, 15) function(self, string) windll.Kernel32.SetConsoleTextAttribute(h, 15) return real_func else: def color_text_decorator(function): def real_func(self, string): if function.__name__.upper() == 'ERROR': self.stream.write('\033[0;31;40m') elif function.__name__.upper() == 'WARN': self.stream.write('\033[0;35;40m') elif function.__name__.upper() == 'INFO': self.stream.write('\033[0;33;40m') elif function.__name__.upper() == 'DEBUG': self.stream.write('\033[0;37;40m') else: self.stream.write('\033[0;37;40m') function(self, string) self.stream.write('\033[0m') return real_func FORMAT = '[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s' class MainLogger(object): DEBUG_MODE = True LOG_LEVEL = 5 def __init__(self, name): current_path = os.path.join(os.path.dirname( os.path.abspath(__file__)), 'static', 'testlog', 'syslog') if not os.path.exists(current_path): os.makedirs(current_path) # baseconfig logging.basicConfig() self.logger = logging.getLogger(name) self.logger.setLevel(logging.DEBUG) formatter = logging.Formatter(FORMAT) th_all = logging.handlers.TimedRotatingFileHandler( os.path.join(current_path, 'master_main_atc.log'), when='midnight', interval=1, backupCount=7) th_all.setFormatter(formatter) th_all.setLevel(logging.DEBUG) self.logger.addHandler(th_all) rh_all = logging.handlers.RotatingFileHandler( os.path.join(current_path, 'master_main_logger_rf.log'), mode='a', maxBytes=2000 * 2000, backupCount=3) rh_all.setFormatter(formatter) rh_all.setLevel(logging.DEBUG) self.logger.addHandler(rh_all) # 防止在终端重复打印 self.logger.propagate = 0 def hint(self, string): # 去除多余连续空格 strTmp = str(string) strTmp = ' '.join(strTmp.split()) if self.LOG_LEVEL >= 5: return self.logger.debug(strTmp) else: pass def debug(self, string): # 去除多余连续空格 strTmp = str(string) strTmp = ' '.join(strTmp.split()) if self.LOG_LEVEL >= 4: return self.logger.debug(strTmp) else: pass def info(self, string): # 去除多余连续空格 strTmp = str(string) strTmp = ' '.join(strTmp.split()) if self.LOG_LEVEL >= 3: return self.logger.info(strTmp) else: pass def warn(self, string): # 去除多余连续空格 strTmp = str(string) strTmp = ' '.join(strTmp.split()) if self.LOG_LEVEL >= 2: return self.logger.warn(strTmp) else: pass def error(self, string): # 去除多余连续空格 strTmp = str(string) strTmp = ' '.join(strTmp.split()) if self.LOG_LEVEL >= 1: return self.logger.error(strTmp) else: pass main_logger = MainLogger('MasterMainLogger') class Logger(object): DEBUG_MODE = True LOG_LEVEL = 5 def __init__(self, name, filename=None): self.name = name # baseconfig logging.basicConfig() self.logger = logging.getLogger(name) self.logger.setLevel(logging.DEBUG) formatter = logging.Formatter(FORMAT) # output to terminal sh = logging.StreamHandler() sh.setFormatter(formatter) sh.setLevel(logging.DEBUG if self.DEBUG_MODE else logging.INFO) self.logger.addHandler(sh) self.stream = sh.stream # output to user define file if filename is not None: fh = logging.FileHandler(filename, 'a') fh.setFormatter(formatter) fh.setLevel(logging.DEBUG) self.logger.addHandler(fh) self.logger.propagate = 0 # 防止在终端重复打印 self.logger.propagate = 0 @color_text_decorator def hint(self, string): # 去除多余连续空格 strTmp = str(string) strTmp = ' '.join(strTmp.split()) main_logger.hint("[" + self.name + "] " + strTmp) if self.LOG_LEVEL >= 5: return self.logger.debug(strTmp) else: pass @color_text_decorator def debug(self, string): # 去除多余连续空格 strTmp = str(string) strTmp = ' '.join(strTmp.split()) main_logger.debug("[" + self.name + "] " + strTmp) if self.LOG_LEVEL >= 4: return self.logger.debug(strTmp) else: pass @color_text_decorator def info(self, string): # 去除多余连续空格 strTmp = str(string) strTmp = ' '.join(strTmp.split()) main_logger.info("[" + self.name + "] " + strTmp) if self.LOG_LEVEL >= 3: return self.logger.info(strTmp) else: pass @color_text_decorator def warn(self, string): # 去除多余连续空格 strTmp = str(string) strTmp = ' '.join(strTmp.split()) main_logger.warn("[" + self.name + "] " + strTmp) if self.LOG_LEVEL >= 2: return self.logger.warn(strTmp) else: pass @color_text_decorator def error(self, string): # 去除多余连续空格 strTmp = str(string) strTmp = ' '.join(strTmp.split()) main_logger.error("[" + self.name + "] " + strTmp) if self.LOG_LEVEL >= 1: return self.logger.error(strTmp) else: pass class TestLogModule(object): def __init__(self): pass def runtest(self): logger = Logger('TEST') iCount = 0 while True: iCount = iCount + 1 logger.error(str(iCount)) logger.debug('1 22 333 4444 55555 666666') logger.info('1 22 333 4444 55555 666666') logger.warn('1 22 333 4444 55555 666666') logger.error('1 22 333 4444 55555 666666') time.sleep(1) if iCount >= 120: break # for a in xrange(10): # logger.debug('1 22 333 4444 55555 666666') # logger.info('1 22 333 4444 55555 666666') # logger.warn('1 22 333 4444 55555 666666') # logger.error('1 22 333 4444 55555 666666') # time.sleep(1) if __name__ == '__main__': TestLogModule().runtest()
这次,终于达到笔者现阶段的需求,但是又发现了新的问题,如果以上代码运行在多进程环境中,日志反转时,不再建立新日志文件,python的logging模块报错,提示文件已打开,导致日志记录失败。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署