Skip to main content

logging

基础使用​

使用流程​

# 1 创建一个日志实例

# 1.1 设置全局对象的日志等级

# 2 创建一个控制台处理器,用来将日志打印到控制台

# 3 设置一个日志等级,哪些等级的日志会在控制台输出

# 4 将控制台处理器挂载到日志实例上,通过这步才能正确的在控制台输出

内置功能​

logging 是一个非常完善的日志模块,内置了非常多的使用功能,基本能胜任任何场景

  • 日志分级
  • 输出文件
  • 输出控制台
  • 自定义格式
  • 按时间、大小划分文件

日志级别​

  • logging.NOTSET(0)
  • logging.DEBUG(10)
  • logging.INFO(20)
  • logging.WARNING(30)
  • logging.ERROR(40)
  • logging.CRITICAL(50)

括号为级别对应的数值

核心组成​

相关类型:

Logger:日志,暴露函数给应用程序,基于日志记录器和过滤器级别决定哪些日志有效。

LogRecord :日志记录器,将日志传到相应的处理器处理。

Handler :处理器, 将(日志记录器产生的)日志记录发送至合适的目的地。

Filter :过滤器, 提供了更好的粒度控制,它可以决定输出哪些日志记录。

Formatter:格式化器, 指明了最终输出中日志记录的布局。

具体流程:

  • 判断 Logger 对象对于设置的级别是否可用,如果可用,则往下执行,否则,流程结束。

  • 创建 LogRecord 对象,如果注册到 Logger 对象中的 Filter 对象过滤后返回 False,则不记录日志,流程结束,否则,则向下执行。

  • LogRecord 对象将 Handler 对象传入当前的 Logger 对象,(图中的子流程)如果 Handler 对象的日志级别大于设置的日志级别,再判断注册到 Handler 对象中的 Filter 对象过滤后是否返回 True 而放行输出日志信息,否则不放行,流程结束。

  • 如果传入的 Handler 大于 Logger 中设置的级别,也即 Handler 有效,则往下执行,否则,流程结束。

  • 判断这个 Logger 对象是否还有父 Logger 对象,如果没有(代表当前 Logger 对象是最顶层的 Logger 对象 root Logger),流程结束。否则将 Logger 对象设置为它的父 Logger 对象,重复上面的 3、4 两步,输出父类 Logger 对象中的日志输出,直到是 root Logger 为止。

基本使用​

logging 使用非常简单,使用 basicConfig() 方法就能满足基本的使用需要,如果方法没有传入参数,会根据默认的配置创建Logger 对象,默认的日志级别被设置为 WARNING,默认的日志输出格式如上图,该函数可选的参数如下表所示。

日志输出格式​

image-20220514071304648

从配置文件中获取配置信息​

ini​

# config.ini
[loggers]
keys=root,sampleLogger

[handlers]
keys=consoleHandler

[formatters]
keys=sampleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_sampleLogger]
level=DEBUG
handlers=consoleHandler
qualname=sampleLogger
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=sampleFormatter
args=(sys.stdout,)

[formatter_sampleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
import logging.config

logging.config.fileConfig(fname='config.ini', disable_existing_loggers=False)
logger = logging.getLogger("sampleLogger")
# 省略日志输出

Yaml​

需要安装 pyymal 库

# config.yaml
version: 1
formatters:
simple:
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
console:
class: logging.StreamHandler
level: DEBUG
formatter: simple

loggers:
simpleExample:
level: DEBUG
handlers: [console]
propagate: no
root:
level: DEBUG
handlers: [console]

import logging.config

# 需要安装 pyymal 库
import yaml

with open('config.yaml', 'r') as f:
config = yaml.safe_load(f.read())
logging.config.dictConfig(config)

logger = logging.getLogger("sampleLogger")
# 省略日志输出

输出中文​

上面的例子中日志输出都是英文内容,发现不了将日志输出到文件中会有中文乱码的问题,如何解决到这个问题呢?FileHandler 创建对象时可以设置文件编码,如果将文件编码设置为 “utf-8”(utf-8 和 utf8 等价),就可以解决中文乱码问题啦。一种方法是自定义 Logger 对象,需要写很多配置,另一种方法是使用默认配置方法 basicConfig(),传入 handlers 处理器列表对象,在其中的 handler 设置文件的编码。网上很多都是无效的方法,关键参考代码如下:

# 自定义 Logger 配置
handler = logging.FileHandler(filename="test.log", encoding="utf-8")
# 使用默认的 Logger 配置
logging.basicConfig(handlers=[logging.FileHandler("test.log", encoding="utf-8")], level=logging.DEBUG)

配置​

basicConfig()​

参数名称参数描述
filename日志输出到文件的文件名
filemode文件模式,r[+]、w[+]、a[+]
format日志输出的格式
datefat日志附带日期时间的格式
style格式占位符,默认为 "%" 和 “{}”
level设置日志输出级别
stream定义输出流,用来初始化 StreamHandler 对象,不能 filename 参数一起使用,否则会ValueError 异常
handles定义处理器,用来创建 Handler 对象,不能和 filename 、stream 参数一起使用,否则也会抛出 ValueError 异常
import logging

logging.basicConfig(
filename='test.log',
filemode='w',
level=logging.DEBUG
)

exception 异常捕获​

但是当发生异常时,直接使用无参数的 debug()、info()、warning()、error()、critical() 方法并不能记录异常信息,需要设置 exc_info 参数为 True 才可以,或者使用 exception() 方法,还可以使用 log() 方法,但还要设置日志级别和 exc_info 参数。

import logging

logging.basicConfig(
filename="test.log", filemode="w",
format="%(asctime)s %(name)s:%(levelname)s:%(message)s",
datefmt="%d-%M-%Y %H:%M:%S",
level=logging.DEBUG)

a = 5
b = 0

try:
c = a / b
except Exception as e:
# 下面三种方式三选一,推荐使用第一种
logging.exception("Exception occurred")
logging.error("Exception occurred", exc_info=True)
logging.log(level=logging.DEBUG, msg="Exception occurred", exc_info=True)

logger 对象​

变量格式变量描述
asctime%(asctime)s将日志的时间构造成可读的形式,默认情况下是精确到毫秒,如 2018-10-13 23:24:57,832,可以额外指定 datefmt 参数来指定该变量的格式
name%(name)日志对象的名称
filename%(filename)s不包含路径的文件名
pathname%(pathname)s包含路径的文件名
funcName%(funcName)s日志记录所在的函数名
levelname%(levelname)s日志的级别名称
message%(message)s具体的日志信息
lineno%(lineno)d日志记录所在的行号
pathname%(pathname)s完整路径
process%(process)d当前进程ID
processName%(processName)s当前进程名称
thread%(thread)d当前线程ID
threadName%threadName)s当前线程名称

Logger 对象和 Handler 对象都可以设置级别,而默认 Logger 对象级别为 30 ,也即 WARNING,默认 Handler 对象级别为 0,也即 NOTSET。

  • 通过 logging.getLogger(xxx) 获得

  • 一个系统只有一个 Logger 对象,并且该对象不能被直接实例化,没错,这里用到了单例模式,获取 Logger 对象的方法为 getLogger。

  • 整个系统只有一个根 Logger 对象,Logger 对象在执行 info()、error() 等方法时实际上调用都是根 Logger 对象对应的 info()、error() 等方法。

我们可以创造多个 Logger 对象,但是真正输出日志的是根 Logger 对象。每个 Logger 对象都可以设置一个名字,如果设置logger = logging.getLogger(__name__),name 是 Python 中的一个特殊内置变量,他代表当前模块的名称(默认为 main)。则 Logger 对象的 name 为建议使用使用以点号作为分隔符的命名空间等级制度。

Handler 对象​

logging内置了几个常用的基础处理器

config 对象​

从字典中获取配置信息

基础用例​

默认使用​

import logging

# 默认
logging.basicConfig()
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')

# 结果输出:
WARNING:root:This is a warning message
ERROR:root:This is an error message
CRITICAL:root:This is a critical message

自定义参数​

import logging

datefmt="%d-%m-%Y %H:%M:%S"
format = "%(asctime)s %(name)s:%(levelname)s:%(message)s"
logging.basicConfig(
filename="test.log",
filemode="w",
format=format,
datefmt=datefmt,
level=logging.DEBUG)

logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')

自定义Handler​

关键函数:logger.addHandler()

import logging
import logging.handlers

logger = logging.getLogger("logger")

handler1 = logging.StreamHandler()
handler2 = logging.FileHandler(filename="test.log")

logger.setLevel(logging.DEBUG)
handler1.setLevel(logging.WARNING)
handler2.setLevel(logging.DEBUG)

formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s")
handler1.setFormatter(formatter)
handler2.setFormatter(formatter)

logger.addHandler(handler1)
logger.addHandler(handler2)

# 分别为 10、30、30
# print(handler1.level)
# print(handler2.level)
# print(logger.level)

logger.debug('This is a customer debug message')
logger.info('This is an customer info message')
logger.warning('This is a customer warning message')
logger.error('This is an customer error message')
logger.critical('This is a customer critical message')

通过字典配置logging​

import logging.config

config = {
'version': 1,
'formatters': {
'simple': {
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
},
# 其他的 formatter
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'formatter': 'simple'
},
'file': {
'class': 'logging.FileHandler',
'filename': 'logging.log',
'level': 'DEBUG',
'formatter': 'simple'
},
# 其他的 handler
},
'loggers':{
'StreamLogger': {
'handlers': ['console'],
'level': 'DEBUG',
},
'FileLogger': {
# 既有 console Handler,还有 file Handler
'handlers': ['console', 'file'],
'level': 'DEBUG',
},
# 其他的 Logger
}
}

logging.config.dictConfig(config)
StreamLogger = logging.getLogger("StreamLogger")
FileLogger = logging.getLogger("FileLogger")

临时关闭日志输出​

logging.disable(logging.INFO)
# or
logger.disabled = True

按大小或者日期划分文件​

# 每隔 1000 Byte 划分一个日志文件,备份文件为 3 个
file_handler = logging.handlers.RotatingFileHandler(
"test.log", mode="w", maxBytes=1000, backupCount=3, encoding="utf-8"
)
# 每隔 1小时 划分一个日志文件,interval 是时间间隔,备份文件为 10 个
handler2 = logging.handlers.TimedRotatingFileHandler(
"test.log", when="H", interval=1, backupCount=10
)

基础封装案例​

  • 封装
# encoding=utf-8
import logging,time

# log_path是存放日志的路径
import os


cur_path = os.path.dirname(os.path.realpath(__file__))
log_path = os.path.join(os.path.dirname(cur_path), 'logs')
# 如果不存在这个logs文件夹,就自动创建一个
if not os.path.exists(log_path):os.mkdir(log_path)
class Log(object):

def __init__(self):
# 文件的命名
self.logname = os.path.join(log_path, '%s.log' % time.strftime('%Y_%m_%d'))
self.logger = logging.getLogger()
self.logger.setLevel(logging.DEBUG)
# 日志输出格式
self.formatter = logging.Formatter('[%(asctime)s] - %(filename)s] - %(levelname)s: %(message)s')

def __console(self, level, message):
# 创建一个FileHandler,用于写到本地
fh = logging.FileHandler(self.logname, 'a', encoding='utf-8') # 这个是python3的
fh.setLevel(logging.DEBUG)
fh.setFormatter(self.formatter)
self.logger.addHandler(fh)

# 创建一个StreamHandler,用于输出到控制台
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(self.formatter)
self.plogger.addHandler(ch)

if level == 'info':
self.logger.info(message)
elif level == 'debug':
self.logger.debug(message)
elif level == 'warning':
self.logger.warning(message)
elif level == 'error':
self.logger.error(message)

# 这两行代码是为了避免日志输出重复问题
self.logger.removeHandler(ch)
self.logger.removeHandler(fh)
# 关闭打开的文件
fh.close()

def debug(self, message):
self.__console('debug', message)

def info(self, message):
self.__console('info', message)

def warning(self, message):
self.__console('warning', message)

def error(self, message):
self.__console('error', message)

  • 使用
from log_init import Log
log.info("愤怒的小猥琐");

参阅文献​