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("愤怒的小猥琐");

参阅文献