logging-cookbook
python 文档-logging 模块
logging 的基础 Python 标准库 logging 用作记录日志。
logging 的基础使用
1 2 3 4 5 6 7 8 9 10 11 12 import logging logging.debug("A quirky message only developers care about" ) logging.info("Curious users might want to know this" ) logging.warning("Something is wrong and any user should be informed" ) logging.error("Serious stuff, this is red for a reason" ) logging.critical("OH NO everything is on fire" ) WARNING:root:Something is wrong and any user should be informed ERROR:root:Serious stuff, this is red for a reason CRITICAL:root:OH NO everything is on fire
如果没有特殊配置 logging 有默认配置,日志级别为 warning ,root
为根记录器
logging 的日志级别 默认分为六种日志级别(括号为级别对应的数值),NOTSET(0)、DEBUG(10)、INFO(20)、WARNING(30)、ERROR(40)、CRITICAL(50)。我们自定义日志级别时注意不要和默认的日志级别数值相同,logging 执行时输出大于等于设置的日志级别的日志信息,如设置日志级别是 INFO,则 INFO、WARNING、ERROR、CRITICAL 级别的日志都会输出。
可以通过logging.DEBUG
等等查看,在命名习惯上这个值为常量
默认级别为30,loggg.info 的级别为20, 只有当每条日志的日志级别大于等于全局的才会输出
python logging 配置 使用basicconfig 配置 logging.``basicConfig
(**kwargs )¶
通过使用默认的 Formatter
创建一个 StreamHandler
并将其加入根日志记录器来为日志记录系统执行基本配置。 如果没有为根日志记录器定义处理器则 debug()
, info()
, warning()
, error()
和 critical()
等函数将自动调用 basicConfig()
。
如果根日志记录器已配置了处理器则此函数将不执行任何操作,除非关键字参数 force 被设为 True
。
支持以下关键字参数。
格式
描述
filename
使用指定的文件名创建一个 FileHandler
,而不是 StreamHandler
。
filemode
如果指定了 filename ,则用此 模式 打开该文件。 默认模式为 'a'
。
format
使用指定的格式字符串作为处理器。 默认为属性以冒号分隔的 levelname
, name
和 message
。
datefmt
使用指定的日期/时间格式,与 time.strftime()
所接受的格式相同。
style
如果指定了 format ,将为格式字符串使用此风格。 '%'
, '{'
或 '$'
分别对应于 printf 风格 , str.format()
或 string.Template
。 默认为 '%'
。
level
设置根记录器级别为指定的 level .
stream
使用指定的流初始化 StreamHandler
。 请注意此参数与 filename 不兼容 —— 如果两者同时存在,则会引发 ValueError
。
handlers
如果指定,这应为一个包含要加入根日志记录器的已创建处理器的可迭代对象。 任何尚未设置格式描述符的处理器将被设置为在此函数中创建的默认格式描述符。 请注意此参数与 filename 或 stream 不兼容 —— 如果两者同时存在,则会引发 ValueError
。
force
如果将此关键字参数指定为 true,则在执行其他参数指定的配置之前,将移除并关闭附加到根记录器的所有现有处理器。
encoding
如果此关键字参数与 filename 一同被指定,则其值会在创建 FileHandler
时被使用,因而也会在打开输出文件时被使用。
errors
如果此关键字参数与 filename 一同被指定,则其值会在创建 FileHandler
时被使用,因而也会在打开输出文件时被使用。 如果未指定,则会使用值 ‘backslashreplace’。 请注意如果指定为 None
,它将被原样传给 open()
,这意味着它将会当作传入 ‘errors’ 一样处理。
format 的参数修改自 python 文档 ,删除了用户不可格式化的属性。
属性名称
格式
描述
asctime
%(asctime)s
表示人类易读的 LogRecord
生成时间。 默认形式为 ‘2003-07-08 16:49:45,896’ (逗号之后的数字为时间的毫秒部分)。
created
%(created)f
LogRecord
被创建的时间(即 time.time()
的返回值)。
filename
%(filename)s
pathname
的文件名部分。
funcName
%(funcName)s
函数名包括调用日志记录.
levelname
%(levelname)s
消息文本记录级别('DEBUG'
,'INFO'
,'WARNING'
,'ERROR'
,'CRITICAL'
)。
levelno
%(levelno)s
消息数字的记录级别 (DEBUG
, INFO
, WARNING
, ERROR
, CRITICAL
).
lineno
%(lineno)d
发出日志记录调用所在的源行号(如果可用)。
message
%(message)s
记入日志的消息,即 msg % args
的结果。 这是在发起调用 Formatter.format()
时设置的。
module
%(module)s
模块 (filename
的名称部分)。
msecs
%(msecs)d
LogRecord
被创建的时间的毫秒部分。
name
%(name)s
用于记录调用的日志记录器名称。
pathname
%(pathname)s
发出日志记录调用的源文件的完整路径名(如果可用)。
process
%(process)d
进程ID(如果可用)
processName
%(processName)s
进程名(如果可用)
relativeCreated
%(relativeCreated)d
以毫秒数表示的 LogRecord 被创建的时间,即相对于 logging 模块被加载时间的差值。
thread
%(thread)d
线程ID(如果可用)
threadName
%(threadName)s
线程名(如果可用)
1 2 3 4 5 6 7 8 9 import logging LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" logging.basicConfig(filename='my.log' , level=logging.DEBUG, format =LOG_FORMAT) logging.debug("A quirky message only developers care about" ) logging.info("Curious users might want to know this" ) logging.warning("Something is wrong and any user should be informed" ) logging.error("Serious stuff, this is red for a reason" ) logging.critical("OH NO everything is on fire" )
logging 的四大组件
logging 字典配置 参考
配置字典架构
version - 用数字表示的模式版本,当前有效值只能为1,主要是为了后续配置升级后提供兼容性
formatters - 格式化对象,值还是一个dict,该dict的键为格式化对象的id,值为一个dict,存储了formatter对象的配置内容
format
: None
datefmt
: None
filters - 过滤器对象,逻辑同formatters
handlers - 逻辑同formatters
class
(必须的): handler类的全限定名
level
(可选的): handler对象级别
formatter
(可选的):为formatters中定义的formatter对象id
filters
(可选的):为一个filter对象的列表,里面存放filters中定义的filter对象id 也可以存入其他的参数,视实际handler对象而定
loggers - 逻辑同formatters,该logger对象的id也是logger name
level
(可选的):logger对象级别
propagate
(可选的):logger对象的消息传递设置
filters
(可选的):为一个filter对象的列表,里面存放filters中定义的filter对象id
handlers
(可选的):为一个handler对象的列表,里面存放handlers中定义的handler对象id
root - 值为dict,存放root这个logger对象的配置,但是propagate
这个值对root没用
incremental - 该配置文件的配置是否作为已有配置的一个补充,该值默认为False
,即覆盖原有配置,如果为True
,也不是完全覆盖,因为filter对象和formatter对象是匿名的,无法获取到具体的对象,也就无法修改,但是可以替代logger对象的level
和propagate
属性,handler对象的level
属性
disable_existing_loggers - 是否禁用已存在的logger对象,默认是True
,如果incremental 是True
的话,该值就会被忽略
logging 输出颜色的几种方案 使用颜色控制码输出颜色 先看以下代码及其运行效果
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 import loggingfrom logging import config dict_config = { "version" : 1 , "formatters" : { 'detail' : { 'format' : '\033[0;32;40m[%(asctime)s]\033[0m %(filename)s -> %(funcName)s line:%(lineno)d [%(levelname)s] : %(message)s' , }, }, "handlers" : { "stream" : { "class" : "logging.StreamHandler" , "formatter" : "detail" , "level" : "INFO" , "stream" : "ext://sys.stdout" }, }, "root" : { 'handlers' : ['stream' , ], 'level' : "INFO" , } }if __name__ == '__main__' : logging.config.dictConfig(dict_config) log = logging.getLogger() log.debug("A quirky message only developers care about" ) log.info("Curious users might want to know this" ) log.warning("Something is wrong and any user should be informed" ) log.error("Serious stuff, this is red for a reason" ) log.critical("OH NO everything is on fire" )
\033[
是控制码开始,’;’是分割符用来分割,显示样式;前景色;背景色 \033[0m
是控制码结束
查下与之对应的颜色对照表,可以使用这种方式自定义颜色配置,日志颜色的配置的频率不高,一次查阅之后,就可以不用改响应的配置,下次可以直接使用。
具体的8位色,256色,truecolor 以及响应的颜色控制码不再赘述,给出响应的资料感兴趣的,可参考以下文章或者自行搜索
这篇文章 这篇文章 或者维基百科中的详细解释 ,
这个博文解释的更全面些 不过是英文的 链接在此
使用颜色封装库输出颜色 需要先安装 colorama
运行以下命令进行安装pip install colorama
1 2 3 4 5 6 7 from colorama import init, Fore, Back, Style init(autoreset=True )print (Style.BRIGHT + Back.YELLOW + Fore.RED + "CHEESY" )
colorama 是对颜色控制码的语义化封装,使颜色控制变得更人性化
来看例子
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 import loggingfrom logging.config import dictConfigfrom colorama import Fore, Back, Style dict_config = { "version" : 1 , "formatters" : { 'color' : { 'format' : f'{Fore.GREEN} %(asctime)s - {Fore.CYAN} %(name)s - %(levelname)s - %(message)s' , 'datefmt' : "%Y-%m-%d %H:%M:%S" } }, "handlers" : { "stream" : { "class" : "logging.StreamHandler" , "formatter" : "color" , "level" : "INFO" , "stream" : "ext://sys.stdout" }, }, "root" : { 'handlers' : ['stream' , ], 'level' : "INFO" , } } dictConfig(dict_config) log = logging.getLogger() log.debug("A quirky message only developers care about" ) log.info("Curious users might want to know this" ) log.warning("Something is wrong and any user should be informed" ) log.error("Serious stuff, this is red for a reason" ) log.critical("OH NO everything is on fire" )
输出如下
但是以上代码所有级别看起来都是一个颜色,不同级别的提示需要看登记区分,不如登记按照颜色区分方便。
使用三方库,实现按照日志等级区分颜色 下面来实现一个根据日志级别输出不同色彩的 config
先运行代码 python3 -m pip install colorlog
安装三方库
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 from logging import configimport logging.config log_config_dict = { "version" : 1 , "disable_existing_loggers" : False , "formatters" : { "colored" : { "()" : "colorlog.ColoredFormatter" , 'format' : "%(log_color)s%(asctime)s %(filename)s %(funcName)s [line:%(lineno)d] %(levelname)s %(message)s" , "log_colors" : { "DEBUG" : "cyan" , "INFO" : "green" , "WARNING" : "yellow" , "ERROR" : "red" , "CRITICAL" : "red,bg_white" }, }, }, "handlers" : { "stream" : { "class" : "logging.StreamHandler" , "formatter" : "colored" , "level" : "INFO" , }, }, "root" : { 'handlers' : ['stream' , ], 'level' : "INFO" , } } config.dictConfig(log_config_dict) log = logging.getLogger() log.debug("A quirky message only developers care about" ) log.info("Curious users might want to know this" ) log.warning("Something is wrong and any user should be informed" ) log.error("Serious stuff, this is red for a reason" ) log.critical("OH NO everything is on fire" )
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 import loggingfrom logging import configfrom colorama import Fore, Back, Styleclass CustomFormatter (logging.Formatter): """Logging colored formatter, adapted from https://stackoverflow.com/a/56944256/3638629""" level_color = '' def __init__ (self, fmt, datefmt=None , style='%' , validate=True ): super ().__init__(fmt, datefmt=None , style='%' , validate=True ) self .fmt = fmt self .datefmt = datefmt self .base_formatter = f'{Fore.GREEN} %(asctime)s-{Fore.CYAN} [%(name)s]-|%(filename)s|%(funcName)s|%(lineno)d|level_color[%(levelname)9s ]{Fore.CYAN} -%(message)s' self .FORMATS = { logging.DEBUG: self .base_formatter.replace('level_color' , Fore.WHITE), logging.INFO: self .base_formatter.replace('level_color' , Fore.BLUE), logging.WARNING: self .base_formatter.replace('level_color' , Fore.YELLOW), logging.ERROR: self .base_formatter.replace('level_color' , Fore.RED), logging.CRITICAL: f'{Fore.GREEN} %(asctime)s-{Fore.CYAN} [%(name)s]-|%(filename)s|%(funcName)s|%(lineno)d|{Style.BRIGHT} {Fore.RED} [%(levelname)9s ]{Fore.CYAN} -%(message)s' } def format (self, record ): log_fmt = self .FORMATS.get(record.levelno) formatter = logging.Formatter(log_fmt, datefmt=self .datefmt) return formatter.format (record) dict_config = { "version" : 1 , "formatters" : { 'color_by_level' : { "()" : CustomFormatter, 'fmt' : "%(asctime)s - %(name)s - %(levelname) s - %(message)s" , 'datefmt' : '%H:%M:%S' }, }, "handlers" : { "stream" : { "class" : "logging.StreamHandler" , "formatter" : "color_by_level" , "level" : "INFO" , "stream" : "ext://sys.stdout" }, }, "root" : { 'handlers' : ['stream' , ], 'level' : "INFO" , } }if __name__ == '__main__' : logging.config.dictConfig(dict_config) log = logging.getLogger() log.debug("A quirky message only developers care about" ) log.info("Curious users might want to know this" ) log.warning("Something is wrong and any user should be informed" ) log.error("Serious stuff, this is red for a reason" ) log.critical("OH NO everything is on fire" )
logging 和异常捕获 logging 中的异常捕获
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 import loggingfrom logging import config dict_config = { "version" : 1 , "formatters" : { "simple" : { 'format' : "%(asctime)s - %(levelname)s - %(user)s[%(ip)s] - %(message)s" , 'datefmt' : "%Y-%m-%d %H:%M:%S" }, }, "handlers" : { "stream" : { "class" : "logging.StreamHandler" , "formatter" : "simple" , "level" : "INFO" , "stream" : "ext://sys.stdout" }, }, "root" : { 'handlers' : ['stream' , ], 'level' : "INFO" , } } config.dictConfig(dict_config) log = logging.getLogger()try : assert 1 == 2 except : log.exception("Some one delete the log file." , exc_info=True , extra={'user' : 'Tom' , 'ip' : '47.98.53.222' }, stack_info=False )
logging.exception
是对logging.error
的封装 ,有个默认值 exec_info=True,会默认输出Traceback 信息
其他关键的关键词参数有:
extra
: 向logrecord中添加指定的字段
exec_info
: 打印traceback 信息
stack_info
打印stack 信息
修改logging.excetion 的参数如下:
1 2 3 log.exception("Some one delete the log file." , exc_info=False , extra={'user' : 'Tom' , 'ip' : '47.98.53.222' }, stack_info=True )
执行结果为:
![image-20230204122023336](/Users/king/Library/Application Support/typora-user-images/image-20230204122023336.png)
在实际运行的过程中,代码可能会比上面的demo 更加复杂,传入的参数很多,引起错误的原因也不太好定位,这样记录日志只会知道代码有报错,但是具体的代码的参数是什么,哪个参数引起的报错,无从得知。如果复现debug 的时间长,过程繁琐 。。。。。 不说了已经开始头疼了
使用better_exceptions 显示函数的参数值 github 地址
1 2 3 4 5 import better_exceptions better_exceptions.hook() request = "test test test" a, b, c, d = request.split()
![image-20230204123509091](/Users/king/Library/Application Support/typora-user-images/image-20230204123509091.png)
可以看到代码提示中有参数输出,对于复杂程序的定位还是很有帮助的
better_exception 还有个设置就是控制变量的字符数, 在0.3.3
版本汇总 MAX_LENGTH 有如下默认值
better_exceptions.MAX_LENGTH = 128
better_exceptions.MAX_LENGTH = None 表示无限制
better_exceptions
是怎么实现参数打印的呢
better_exceptions
中的hook
方法把 sys.excepthook 函数定义成自己的excepthook
方法 ,调用了 自己的格式化方法给错误信息添加上了变量和颜色设置
从代码中也可以看到,这种错误信息值输出到了屏幕,虽然有hook logging,但是只在没有
loggging 自定义异常处理 先简单说一下:
logging模块在输出一个异常的时候,会调用handler
的formater
的formatException
函数来格式化异常.
better_exceptions
有个format_exception
方法会将异常调用栈变量值输出,就是 上面例子的那种输出.
我们的思路就是在handler 在调用 formater 的formatException 是返回 better_exceptions.format_exception 方法的返回值
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 import loggingfrom logging import configfrom logging import FileHandlerclass LogExceptionFormatter (logging.Formatter): def formatException (self, exc_info ): from better_exceptions import format_exception result = format_exception(*exc_info) return '' .join(result) def format (self, record ): return super ().format (record) config.dictConfig( { "version" : 1 , "formatters" : { 'log_exception' : { '()' : LogExceptionFormatter, 'format' : '%(asctime)s [%(threadName)s:%(thread)d] [%(name)s:%(lineno)d] [%(levelname)s]- %(message)s' , }, }, "handlers" : { "file" : { "class" : "logging.FileHandler" , "formatter" : "log_exception" , "level" : "INFO" , "filename" : 'test.log' }, "console" : { "class" : "logging.StreamHandler" , "level" : "INFO" , "formatter" : "log_exception" , "stream" : "ext://sys.stdout" }, }, "root" : { 'handlers' : ['console' , 'file' ], 'level' : "INFO" , } } )def get_student_infos (logs ): student_infos = [] for log in logs: name, catgory, grade = log.split(' ' ) student_infos.append({ 'name' : name, 'catgory' : catgory, 'grade' : grade, }) return student_infosif __name__ == '__main__' : logger = logging.getLogger() exam_logs = [ 'zhangsan math 60' , 'lisi english 80' , 'wangwu chinese 90' , 'qianliu music' ] try : get_student_infos(exam_logs) except Exception as e: logger.exception(e)
文件内容如下 其中类如 ESC[36m 是相关的颜色控制码
在终端使用cat 命令查看日志,会显示下面的效果
使用loguru 中的 异常处理 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 import loggingfrom logging import configfrom loguru._better_exceptions import ExceptionFormatterclass LoguruExceptionFormatter (logging.Formatter): def __init__ (self, *args, **kwargs ): super ().__init__(*args, **kwargs) self .exception_formatter = ExceptionFormatter(colorize=True , backtrace=True , diagnose=True ) def formatException (self, exc_info ): result = self .exception_formatter.format_exception(*exc_info, from_decorator=False ) return '' .join([i for i in result]) def format (self, record ): return super ().format (record) config.dictConfig( { "version" : 1 , "formatters" : { 'log_exception' : { '()' : LoguruExceptionFormatter, 'format' : '%(asctime)s [%(threadName)s:%(thread)d] [%(name)s:%(lineno)d][%(levelname)s]- %(message)s' , }, }, "handlers" : { "file" : { "class" : "logging.FileHandler" , "formatter" : "log_exception" , "level" : "INFO" , "filename" : 'test.log' }, "console" : { "class" : "logging.StreamHandler" , "level" : "INFO" , "formatter" : "log_exception" , "stream" : "ext://sys.stdout" }, }, "root" : { 'handlers' : ['console' ], 'level' : "INFO" , } } )def get_student_infos (logs ): student_infos = [] for log in logs: name, catgory, grade = log.split(' ' ) student_infos.append({ 'name' : name, 'catgory' : catgory, 'grade' : grade, }) return student_infosif __name__ == '__main__' : logger = logging.getLogger() exam_logs = [ 'zhangsan math 60' , 'lisi english 80' , 'wangwu chinese 90' , 'qianliu music' ] try : get_student_infos(exam_logs) except Exception as e: logger.exception(e)
使用loguru 中的_better_exceptions
原理与上面类似
rich 中的RichHandler 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 import loggingfrom logging import config config.dictConfig( { "version" : 1 , "formatters" : { 'rich' : { "format" : "%(message)s" } }, "handlers" : { "rich" : { "class" : "rich.logging.RichHandler" , "formatter" : "rich" , "level" : "INFO" , "rich_tracebacks" : True , "tracebacks_show_locals" : True , "locals_max_length" : 5 , }, }, "root" : { 'handlers' : ['rich' ], 'level' : "INFO" , } } ) log = logging.getLogger() log.debug("A quirky message only developers care about" ) log.info("Curious users might want to know this" ) log.warning("Something is wrong and any user should be informed" ) log.error("Serious stuff, this is red for a reason" ) log.critical("OH NO everything is on fire" ) x = 1 y = 0 try : t = x / yexcept Exception: log.exception('e' )
rich 中的RichHandler 也能实现 打印堆栈和局部变量的功能,但是遗憾的是我只找到了的 rich 使用 console 实例保存文件的方法,保存后,里面的内容是渲染好的html 并不适合保存到日志文件中。
有关rich中RichHandler 的参数可以参考rich 文档中关于traceback的主题和Richhandler 的主题 传送门
logging 中的RotatingFileHandler
官方文档
RotatingFileHandler
可以按照文件大小旋转日志
class logging.handlers.RotatingFileHandler(filename, mode=‘a’, maxBytes=0, backupCount=0, encoding=None, delay=False, errors=None)
返回一个 RotatingFileHandler 类的新实例。 将打开指定的文件并将其用作日志记录流。 如果未指定 mode,则会使用 ‘a’。 如果 encoding 不为 None,则会将其用作打开文件的编码格式。 如果 delay 为真值,则文件打开会被推迟至第一次调用 emit()。 默认情况下,文件会无限增长。 如果提供了 errors,它会被用于确定编码格式错误的处理方式。
你可以使用 maxBytes 和 backupCount 值来允许文件以预定的大小执行 rollover。 当即将超出预定大小时,将关闭旧文件并打开一个新文件用于输出。 只要当前日志文件长度接近 maxBytes 就会发生轮换;但是如果 maxBytes 或 backupCount 两者之一的值为零,就不会发生轮换,因此你通常要设置 backupCount 至少为 1,而 maxBytes 不能为零。 当 backupCount 为非零值时,系统将通过为原文件名添加扩展名 ‘.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 等等。
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 import loggingfrom logging.config import dictConfig LOGGER = { 'version' : 1 , 'formatters' : { 'default' : { 'format' : '[%(asctime)s] %(levelname)s in %(module)s: %(message)s' } }, 'handlers' : { 'file' : { 'class' : 'logging.handlers.RotatingFileHandler' , 'formatter' : 'default' , 'filename' : '../logs/app.log' , 'level' : 'DEBUG' , 'maxBytes' : 1 , 'backupCount' : 5 , 'encoding' : 'utf8' , 'delay' : True } }, 'root' : { 'level' : 'INFO' , 'handlers' : ['file' ] } } dictConfig(LOGGER) log = logging.getLogger() log.setLevel(logging.DEBUG)for i in range (10 ): log.debug(f"A quirky message only developers care about {i} " )
结果展示
不算当前文件名,日志只会保留五个备份,会舍弃一部分日志,日志文件具体大小,文件路径可以根据自身情况来配置
logging 的TimedRotatingFileHandler
class logging.handlers.TimedRotatingFileHandler (filename , when=’h’ , interval=1 , backupCount=0 , encoding=None , delay=False , utc=False , atTime=None , errors=None )
返回一个新的 TimedRotatingFileHandler
类实例。 指定的文件会被打开并用作日志记录的流。 对于轮换操作它还会设置文件名前缀。 轮换的发生是基于 when 和 interval 的积。
你可以使用 when 来指定 interval 的类型。 可能的值列表如下。 请注意它们不是大小写敏感的。
值
间隔类型
如果/如何使用 atTime
'S'
秒
忽略
'M'
分钟
忽略
'H'
小时
忽略
'D'
天
忽略
'W0'-'W6'
工作日(0=星期一)
用于计算初始轮换时间
'midnight'
如果未指定 atTime 则在午夜执行轮换,否则将使用 atTime 。
用于计算初始轮换时间
当使用基于星期的轮换时,星期一为 ‘W0’,星期二为 ‘W1’,以此类推直至星期日为 ‘W6’。 在这种情况下,传入的 interval 值不会被使用。
系统将通过为文件名添加扩展名来保存旧日志文件。 扩展名是基于日期和时间的,根据轮换间隔的长短使用 strftime 格式 %Y-%m-%d_%H-%M-%S
或是其中有变动的部分。
当首次计算下次轮换的时间时(即当处理程序被创建时),现有日志文件的上次被修改时间或者当前时间会被用来计算下次轮换的发生时间。
如果 utc 参数为真值,将使用 UTC 时间;否则会使用本地时间。
如果 backupCount 不为零,则最多将保留 backupCount 个文件,而如果当轮换发生时创建了更多的文件,则最旧的文件会被删除。 删除逻辑使用间隔时间来确定要删除的文件,因此改变间隔时间可能导致旧文件被继续保留。
如果 delay 为真值,则会将文件打开延迟到首次调用 emit()
的时候。
如果 atTime 不为 None
,则它必须是一个 datetime.time
的实例,该实例指定轮换在一天内的发生时间,用于轮换被设为“在午夜”或“在每星期的某一天”之类的情况。 请注意在这些情况下,atTime 值实际上会被用于计算 初始 轮换,而后续轮换将会通过正常的间隔时间计算来得出。
如果指定了 errors ,它会被用来确定编码错误的处理方式。
函数解释copy 于官网
运行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 import loggingimport timefrom logging.config import dictConfig LOGGER = { 'version' : 1 , 'formatters' : { 'default' : { 'format' : '[%(asctime)s] %(levelname)s in %(module)s: %(message)s' } }, 'handlers' : { 'file' : { "class" : "logging.handlers.TimedRotatingFileHandler" , "level" : "DEBUG" , "formatter" : "default" , "filename" : "../logs/test.log" , "backupCount" : 5 , 'interval' : 1 , "encoding" : "utf8" , "when" : "s" , 'delay' : True , }, }, 'root' : { 'level' : 'INFO' , 'handlers' : ['file' ] } } dictConfig(LOGGER) log = logging.getLogger() log.setLevel(logging.DEBUG)for i in range (10 ): log.debug(f"A quirky message only developers care about {i} " ) time.sleep(1 )
在多进程下写入单文件的问题 按照官方文档 的介绍,logging 是线程安全的,也就是说,在一个进程内的多个线程同时往同一个文件写日志是安全的。但是(对,这里有个但是)多个进程往同一个文件写日志不是安全的。官方的说法是这样的:
Because there is no standard way to serialize access to a single file across multiple processes in Python. If you need to log to a single file from multiple processes, one way of doing this is to have all the processes log to a SocketHandler, and have a separate process which implements a socket server which reads from the socket and logs to file. (If you prefer, you can dedicate one thread in one of the existing processes to perform this function.)
对于这种方式写入日志 cookbook 的翻译如下
尽管 logging 是线程安全的,将单个进程中的多个线程日志记录至单个文件也 是 受支持的,但将 多个进程 中的日志记录至单个文件则 不是 受支持的,因为在 Python 中并没有在多个进程中实现对单个文件访问的序列化的标准方案。 如果你需要将多个进程中的日志记录至单个文件,有一个方案是让所有进程都将日志记录至一个 SocketHandler
,然后用一个实现了套接字服务器的单独进程一边从套接字中读取一边将日志记录至文件。 (如果愿意的话,你可以在一个现有进程中专门开一个线程来执行此项功能。) 这一部分 文档对此方式有更详细的介绍,并包含一个可用的套接字接收器,你自己的应用可以在此基础上进行适配。
你也可以编写你自己的处理程序,让其使用 multiprocessing
模块中的 Lock
类来顺序访问你的多个进程中的文件。 现有的 FileHandler
及其子类目前并不使用 multiprocessing
,尽管它们将来可能会这样做。 请注意在目前,multiprocessing
模块并未在所有平台上都提供可用的锁功能 (参见 https://bugs.python.org/issue3770)。
使用多进程进程写入单文件时会导致:
进程之间的写入会导致日志中乱序. 这个其他还可以接受 因为看日志的时候,都是使用的grep 命令筛选的
日志丢失
文件旋转的速度和预期的速度不一致
文件大小不是设定的值
解决办法:
使用官方推荐的SocketHandler
自定义实现多进程写入的handler
这里推荐几个三方库
logging-process 支持按时间和按文件大小旋转,
concurrent-log-handler 推荐更新时间在近期,对rotating 参数做了扩展,只能按照文件大小分割
ConcurrentLogHandler 好久没更新,python3.8 能正常跑起来
mrfh 好久没更新
最后给出自己配置
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 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 import datetimeimport osimport timeimport loggingfrom logging import configfrom pathlib import Pathfrom platform import platformfrom loguru._better_exceptions import ExceptionFormatterfrom colorama import Fore, Styleif 'linux' in platform().lower(): LOG_DIR = '/tmp/spider_log' else : LOG_DIR = os.path.join(Path(__file__).parent.parent.parent, 'logs' )if not os.path.exists(LOG_DIR): os.makedirs(LOG_DIR) ''' Loggers:记录器,提供应用程序代码能直接使用的接口; Handlers:处理器,将记录器产生的日志发送至目的地; Filters:过滤器,提供更好的粒度控制,决定哪些日志会被输出; Formatters:格式化器,设置日志内容的组成结构和消息字段。 %(name)s Logger的名字 #也就是其中的.getLogger里的路径,或者我们用他的文件名看我们填什么 %(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用户输出的消息 ''' class ColorByLevelSimple (logging.Formatter): """Logging colored formatter, adapted from https://stackoverflow.com/a/56944256/3638629 参考loguru颜色设置,实现自定义的颜色配置,loguru 的错误输出。 """ level_color = '' def __init__ (self, format , datefmt=None , style='%' , validate=True ): super ().__init__(format , datefmt=None , style='%' , validate=True ) self .fmt = format self .datefmt = datefmt self .base_formatter = f'{Fore.GREEN} %(asctime)s-{Fore.CYAN} [%(name)s]-|%(filename)s|%(funcName)s|%(lineno)d|level_color[%(levelname)9s ]{Fore.WHITE} -%(message)s' self .error_formatter = ExceptionFormatter(colorize=True , backtrace=True , diagnose=True ) self .FORMATS = { logging.DEBUG: self .base_formatter.replace('level_color' , Fore.WHITE), logging.INFO: self .base_formatter.replace('level_color' , Fore.BLUE), logging.WARNING: self .base_formatter.replace('level_color' , Fore.YELLOW), logging.ERROR: self .base_formatter.replace('level_color' , Fore.RED), logging.CRITICAL: f'{Fore.GREEN} %(asctime)s-{Fore.CYAN} [%(name)s]-|%(filename)s|%(funcName)s|%(lineno)d|{Style.BRIGHT} {Fore.RED} [%(levelname)9s ]{Fore.CYAN} -%(message)s' } def format (self, record ): log_fmt = self .FORMATS.get(record.levelno) formatter = logging.Formatter(log_fmt, datefmt=self .datefmt) if record.exc_info: record.exc_text = '' .join(self .error_formatter.format_exception(*record.exc_info)) return formatter.format (record)class LoguruExceptionFormatter (logging.Formatter): def __init__ (self, *args, **kwargs ): super (LoguruExceptionFormatter, self ).__init__(*args, **kwargs) self .exception_formatter = ExceptionFormatter(colorize=True , backtrace=True , diagnose=True ) def formatException (self, exc_info ): result = self .exception_formatter.format_exception(*exc_info) print (result) return '' .join([i for i in result])class DisableErrorFilter (logging.Filter): def filter (self, record ): flag = False if record.levelname == "ERROR" else True return flag simple_format = '%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] - %(message)s' detail_format = '%(asctime)s [%(threadName)s:%(thread)d] [%(name)s:%(lineno)d] [%(levelname)s]- %(message)s' verbose_format = "%(asctime)s - %(process)d - %(processName)s - %(thread)d - %(threadName)s - %(levelname)s -:%(lineno)d] %(message)s" datefmt = '%Y-%m-%d %H:%M:%S' log_config_dict = { "version" : 1 , "disable_existing_loggers" : False , 'incremental' : False , 'filters' : { 'disable_error_filter' : { '()' : DisableErrorFilter, } }, "formatters" : { "simple" : { 'format' : simple_format, 'datefmt' : datefmt, }, 'detail' : { 'format' : detail_format, 'datefmt' : datefmt, }, 'verbose' : { 'format' : verbose_format, 'datefmt' : datefmt, }, 'color_by_level' : { '()' : ColorByLevelSimple, 'format' : detail_format, 'datefmt' : datefmt, }, 'rich' : { "format" : "%(message)s" }, }, "handlers" : { "console" : { "class" : "logging.StreamHandler" , "level" : "INFO" , "formatter" : "color_by_level" , "stream" : "ext://sys.stdout" }, "rich" : { "class" : "rich.logging.RichHandler" , "formatter" : "rich" , "level" : "INFO" , "rich_tracebacks" : True , "tracebacks_show_locals" : True , "locals_max_length" : 5 , }, 'info_log' : { 'class' : 'logging_process.clog.MyRotatingFileHandler' , 'maxBytes' : 1024 * 1024 * 512 , 'backupCount' : 20 , 'filename' : os.path.join(LOG_DIR, 'info.log' ), 'level' : 'INFO' , 'formatter' : 'color_by_level' , 'encoding' : 'utf-8' , 'filters' : ['disable_error_filter' ], 'delay' : True , }, "error_log" : { 'class' : 'logging_process.clog.MyRotatingFileHandler' , 'maxBytes' : 1024 * 1024 * 512 , 'backupCount' : 10 , 'filename' : os.path.join(LOG_DIR, 'error.log' ), 'level' : 'ERROR' , 'formatter' : 'color_by_level' , 'encoding' : 'utf-8' , 'delay' : True , }, }, 'loggers' : { 'facebook' : { "handlers" : ['info_log' , 'error_log' ], 'level' : "INFO" , "propagate" : False , }, }, "root" : { 'handlers' : ['rich' ], 'level' : "INFO" , } } config.dictConfig(log_config_dict)