python logging用法的简单总结
官方文档:Logging facility for Python — Python 3 documentation
基本用法
logging默认配置了六种日志级别(括号为级别对应的数值),按优先级升序,依次为:
- NOTSET(0)
- DEBUG(10)
- INFO(20)
- WARNING(30)
- ERROR(40)
- CRITICAL(50)
logging 执行时输出大于等于设置的日志级别的日志信息,如设置日志级别是 INFO,则 INFO、WARNING、ERROR、CRITICAL 级别的日志都会输出
先来看一个简单的用法示例:
import logging
logging.basicConfig() # 自动化配置
logging.warning('This is a warning message') # 默认的日志输出级别为Warning
logging.basicConfig(**kwargs)
中常用的参数包括:
filename
:指定使用指定的文件名而不是 StreamHandler 创建 FileHandlerfilemode
:如果指定 filename,则以此模式打开文件(‘r’、‘w’、‘a’)。默认为“a”format
:配置日志格式字符串,参考:logrecord-attributesdatefmt
:配置时间格式字符串,支持time.strftime() 所接受的日期/时间格式level
:指定日志记录器级别,默认为WARNINGhandlers
:指定handlers,注意此参数与filename和filemode不兼容,同时配置时会报错
basicConfig配置示例:
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)
进阶配置
我们先了解一下logging中的主要模块:
- Loggers:expose the interface that application code directly uses.
- Handlers:send the log records (created by loggers) to the appropriate destination.
- Filters:provide a finer grained facility for determining which log records to output.
- Formatters:specify the layout of log records in the final output.
程序中发送的日志信息将被包装成LogRecord对象传入logging的各个组件中:
Loggers
Logger
objects have a threefold job. First, they expose several methods to application code so that applications can log messages at runtime. Second, logger objects determine which log messages to act upon based upon severity (the default filtering facility) or filter objects. Third, logger objects pass along relevant log messages to all interested log handlers.
Logger与handler之间的关系类似于邮件和邮箱,一封邮件可以在需要的时候抄送至多个收件箱。Logger常用的配置方法包括:
Logger.setLevel()
:设置最低显示级别Logger.addHandler()
和Logger.removeHandler()
:增删Handlers,每个Logger可以对应0个或多个handlerLogger.addFilter()
和Logger.removeFilter()
:增删格式配置项
每个Logger都有一个name属性,它代表了这个logger在用户程序中所属的模块(与作用域的概念类似),不同模块下logger的name可以通过.
来组织层级关系,比如hook
,hook.spark
,hook.spark.attachment
等。不同层级的logger间有类似于编程对象中“继承”的关系:父logger的各种配置项都会被子logger继承(包括handler,filter等)
我们可以通过logging.getLogger()
方法获得logger对象并配置name。由于logger遵从单例模式,因此多次调用getLogger()并配置相同的name时,该接口将返回同一个logger对象
Handlers
何时我们需要多个Handler?
As an example scenario, an application may want to send all log messages to a log file, all log messages of error or higher to stdout, and all messages of critical to an email address.
标准库中提供了一系列预定义的Handler,参考: Useful Handlers,它们常用的配置方法包括:
setLevel()
设置handler将处理哪个级别以上的消息setFormatter()
为Handler指定一个格式配置对象addFilter()
andremoveFilter()
增删Filter
Formatters
formatter用来配置日志的各种格式,它包括三个参数:
logging.Formatter.__init__(fmt=None, datefmt=None, style='%')
其中:
fmt
指定了日志的消息格式,如:'%(asctime)s - %(levelname)s - %(message)s'
datefmt
指定了日期的组织格式,如:%Y-%m-%d %H:%M:%S
style
允许用户指定用什么类型的标识符来描述前两个参数的内容。比如,在以上两行例子中,我们用的都是%
标识符。除此之外,我们还可以用{}
和$
标识符,具体内容请参考:LogRecord attributes
整体配置
Logging可以通过三种方法配置:
- 在python代码中显式地声明loggers、handlers和formatters等组件并调用相关方法进行配置
- 创建一个logging配置文件,并调用
fileConfig()
进行配置 - 声明一个包含配置信息的dict,并将它传入
dictConfig()
进行配置
后两种配置方式的具体细节可参阅: Configuration functions,接下来展示一个第一种配置方式的简单demo:
import logging
# create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
# add formatter to ch
ch.setFormatter(formatter)
# create logger
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG)
# add ch to logger
logger.addHandler(ch)
# 'application' code
logger.debug('debug message')
以上代码先创建了一个formatter,然后把它添加至handler,接着把该handler关联至一个logger,之后我们就可以用这个logger记录信息了
代码样例
一个日志模块封装样例:
import os
import logging
from logging.handlers import RotatingFileHandler
class Log:
_logger = None
_log_dir = None
@staticmethod
def get():
if Log._logger:
return Log._logger
else:
return Log._build_logger()
@staticmethod
def set_dir(file_dir):
if Log._log_dir:
raise Exception('log directory has already been set. (Check if "get()" has been called before)')
else:
if not (os.path.exists(file_dir) and os.path.isdir(file_dir)):
os.mkdir(file_dir)
Log._log_dir = file_dir
@staticmethod
def filter(name, level):
level = str.upper(level)
assert level in {'DEBUG', 'INFO', 'WARNING', 'ERROR'}
logging.getLogger(name).setLevel(getattr(logging, level))
@staticmethod
def _build_logger():
log_fmt = '[%(asctime)s | \"%(filename)s\" line %(lineno)s | %(levelname)s] %(message)s'
formatter = logging.Formatter(log_fmt, datefmt="%Y/%m/%d %H:%M:%S")
if not Log._log_dir:
os.makedirs('logs', exist_ok=True)
Log._log_dir = 'logs'
log_filepath = os.path.join(Log._log_dir, "rotating.log")
log_file_handler = RotatingFileHandler(filename=log_filepath, maxBytes=500, backupCount=3)
log_file_handler.setFormatter(formatter)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
logging.basicConfig(level=logging.INFO, handlers=(log_file_handler, stream_handler))
Log._logger = logging.getLogger()
return Log._logger
if __name__ == '__main__':
import time
# Log.set_dir('logs') # Configure the log directory before use, if needed.
log = Log.get()
Log.set_dir('logs')
for count in range(20):
log.error(f"logger count: {count}")
time.sleep(1)
另一种写法,根据调用者的文件位置自动给logger命名:
import os
import logging
import traceback
from logging.handlers import RotatingFileHandler
class Log:
_root = None
_logger = None
@staticmethod
def get():
# Construct the name of the logger based on the file path
code_file = traceback.extract_stack()[-2].filename # Get the file path of the caller
if Log._root not in os.path.abspath(code_file):
raise Exception(f'The file calling the method is outside the home directory: "{code_file}"')
relpath = os.path.relpath(code_file, Log._root).replace('.py', '').replace('/', '.')
root_name = os.path.basename(Log._root)
return logging.getLogger(f"{root_name}.{relpath}")
@staticmethod
def init(home):
assert os.path.isdir(home), f'invalid home directory: "{home}"'
Log._root = os.path.abspath(home)
log_dir = os.path.join(Log._root, 'logs')
if not os.path.isdir(log_dir):
os.mkdir(log_dir)
Log._configure_root_logger(log_dir)
@staticmethod
def filter(name, level):
level = str.upper(level)
assert level in {'DEBUG', 'INFO', 'WARNING', 'ERROR'}
logging.getLogger(name).setLevel(getattr(logging, level))
@staticmethod
def _configure_root_logger(log_dir):
log_fmt = '[%(asctime)s | %(name)s | %(levelname)s] %(message)s'
formatter = logging.Formatter(log_fmt, datefmt="%Y/%m/%d %H:%M:%S")
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
log_filepath = os.path.join(log_dir, "rotating.log")
log_file_handler = RotatingFileHandler(filename=log_filepath, maxBytes=1e5, backupCount=3)
log_file_handler.setFormatter(formatter)
root_logger_name = os.path.basename(Log._root)
root_logger = logging.getLogger(root_logger_name)
root_logger.addHandler(log_file_handler)
root_logger.addHandler(stream_handler)
root_logger.setLevel(logging.DEBUG) # default level
if __name__ == '__main__':
import os, time
log_dir = os.path.dirname(__file__)
Log.init(log_dir) # project root directory
log = Log.get()
for count in range(20):
log.info(f"logger count: {count}")
time.sleep(1)