python-logging配置

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 级别的日志都会输出。

image-20230209111133743

可以通过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, namemessage
datefmt 使用指定的日期/时间格式,与 time.strftime() 所接受的格式相同。
style 如果指定了 format,将为格式字符串使用此风格。 '%', '{''$' 分别对应于 printf 风格, str.format()string.Template。 默认为 '%'
level 设置根记录器级别为指定的 level.
stream 使用指定的流初始化 StreamHandler。 请注意此参数与 filename 不兼容 —— 如果两者同时存在,则会引发 ValueError
handlers 如果指定,这应为一个包含要加入根日志记录器的已创建处理器的可迭代对象。 任何尚未设置格式描述符的处理器将被设置为在此函数中创建的默认格式描述符。 请注意此参数与 filenamestream 不兼容 —— 如果两者同时存在,则会引发 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 的四大组件

img

logging 字典配置

参考

配置字典架构

  • version - 用数字表示的模式版本,当前有效值只能为1,主要是为了后续配置升级后提供兼容性
  • formatters - 格式化对象,值还是一个dict,该dict的键为格式化对象的id,值为一个dict,存储了formatter对象的配置内容
    • format: None
    • datefmt: None
  • filters - 过滤器对象,逻辑同formatters
    • name: ''
  • 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对象的levelpropagate属性,handler对象的level属性
  • disable_existing_loggers - 是否禁用已存在的logger对象,默认是True,如果incrementalTrue的话,该值就会被忽略

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 logging
from 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
# colorama_demo.py
from colorama import init, Fore, Back, Style

# 初始化Colorama
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 logging
from logging.config import dictConfig
from 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")


输出如下

image-20230203224822763但是以上代码所有级别看起来都是一个颜色,不同级别的提示需要看登记区分,不如登记按照颜色区分方便。

使用三方库,实现按照日志等级区分颜色

下面来实现一个根据日志级别输出不同色彩的 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
# coding=utf-8
from logging import config
import logging.config


log_config_dict = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
# 使用模块做到的输出带颜色的日志 python3 -m pip install colorlog
"colored": {
"()": "colorlog.ColoredFormatter",
# "format": "%(log_color)s%(levelname)s:%(name)s:%(message)s",
# 'format': "%(log_color)s%(levelname)-8s%(reset)s %(blue)s%(message)s",
'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")

image-20230204113211127

使用自定义Formatter 的方式实现按照日志等级区分颜色

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 logging
from logging import config

from colorama import Fore, Back, Style


class 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", # 键名之所以是fmt,是CustomFormatter的参数是fmt
'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")

image-20230203235140037

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 logging
from 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)

image-20230204120514526

logging.exception 是对logging.error的封装 ,有个默认值 exec_info=True,会默认输出Traceback 信息

image-20230204121612310

其他关键的关键词参数有:

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" # 从request中获取到数据
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 是怎么实现参数打印的呢

image-20230204180853544

better_exceptions中的hook 方法把 sys.excepthook 函数定义成自己的excepthook 方法 ,调用了 自己的格式化方法给错误信息添加上了变量和颜色设置

从代码中也可以看到,这种错误信息值输出到了屏幕,虽然有hook logging,但是只在没有

loggging 自定义异常处理

先简单说一下:

  • logging模块在输出一个异常的时候,会调用handlerformaterformatException函数来格式化异常.
  • 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
# -*- encoding: utf-8 -*-
import logging
from logging import config

from logging import FileHandler


class 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_infos


if __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 是相关的颜色控制码

image-20230204174501618

在终端使用cat 命令查看日志,会显示下面的效果

image-20230204175310487

使用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
# -*- encoding: utf-8 -*-
import logging
from logging import config

from loguru._better_exceptions import ExceptionFormatter


class 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_infos


if __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 原理与上面类似

image-20230204202328707

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 logging
from 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 / y

except Exception:
log.exception('e')

image-20230204183950065

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 logging
from 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}")



结果展示

image-20230205210252540

image-20230205210424080

不算当前文件名,日志只会保留五个备份,会舍弃一部分日志,日志文件具体大小,文件路径可以根据自身情况来配置

logging 的TimedRotatingFileHandler

class logging.handlers.TimedRotatingFileHandler(filename, when=’h’, interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None, errors=None)

返回一个新的 TimedRotatingFileHandler 类实例。 指定的文件会被打开并用作日志记录的流。 对于轮换操作它还会设置文件名前缀。 轮换的发生是基于 wheninterval 的积。

你可以使用 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 logging
import time
from 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)

image-20230205212400281

在多进程下写入单文件的问题

按照官方文档的介绍,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 命令筛选的

  1. 日志丢失
  2. 文件旋转的速度和预期的速度不一致
  3. 文件大小不是设定的值

解决办法:

  1. 使用官方推荐的SocketHandler

  2. 自定义实现多进程写入的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
# coding=utf-8
import datetime
import os
import time
import logging
from logging import config
from pathlib import Path
from platform import platform

from loguru._better_exceptions import ExceptionFormatter
from colorama import Fore, Style

if '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"
},
# "json": {
# "()": jsonlogger.JsonFormatter,
# # pip install python-json-logger from pythonjsonlogger import jsonlogger
# "format": "%(levelname)s %(asctime)s %(name)s %(filename)s %(lineno)s %(funcName)s %(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)

python-logging配置
https://kingjem.github.io/2024/10/14/pythonlogging配置/
作者
Ruhai
发布于
2024年10月14日
许可协议