Python 日志模块
1 前言
Python 八荣八耻中的一句就是: 以打印日志为荣 , 以单步跟踪为耻.
Python
中的内置模块 logging
就是用来打印日志的,然而,使用这个模块的过程总是伴随着各种的不满意,折腾过来折腾过去,算是勉强达到了满意的效果。
因此,用这篇博客总结一下。
2 简单的使用
logging
模块提供了一个默认的日志记录器,允许在不需要进行太多配置的情况下开始使用:
import logging 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
调用了五个函数但只有三个输出的原因是默认的日志记录器 root
根据设置好的 输出级别 进行日志输出,而默认的输出级别为 WARNING
.
输出级别由低到高为:
DEBUG --> INFO --> WARNING --> ERROR --> CRITICAL
可以通过 logging.basicConfig()
函数来配置默认日志记录器 root
的相关属性。
You will notice that the logging module breaks PEP8 styleguide and uses camelCase naming conventions. This is because it was adopted from Log4j, a logging utility in Java. It is a known issue in the package but by the time it was decided to add it to the standard library, it had already been adopted by users and changing it to meet PEP8 requirements would cause backwards compatibility issues. (Source)
该函数常用的参数有:
参数 | 作用 |
---|---|
level |
默认日志记录器的输出级别 |
filename |
用于指定日志的输出文件 |
filemode |
指定以何种方式打开日志文件,默认为 a |
format |
用于日志消息的格式 |
完整的参数文档: logging.basicConfig(**kwargs).
一个简单的例子:
import logging logging.basicConfig(level=logging.DEBUG, format='%(name)s - %(levelname)s - %(message)s') logging.debug('This will get logged')
输出结果为:
root - DEBUG - This will get logged
到目前为止,日志模块的使用还算顺畅,但奈何,很多时候,这些基本功能并不足以满足所有的需求。
3 配置日志消息的格式
日志消息的格式是很重要的,日志消息包含必要内容可以让你更快的找到问题的所在,而良好的格式也能让你在看日志的时候更舒服。
比如上面的例子:
logging.basicConfig(level=logging.DEBUG, format='%(name)s - %(levelname)s - %(message)s')
得到的输出比默认的配置清楚多了, format
字符串中所有可用的属性可以在这里找到: LogRecord attributes.
值得注意的是,属性 %(asctime)s
的输出格式会受到 datefmt
的影响:
import logging logging.basicConfig(format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S') logging.warning('Admin logged out') # output 12-Jul-18 20:53:19 - Admin logged out
datefmt
的配置指南: strftime() and strptime() Behavior.
到了这里,事情逐渐复杂起来,但是只要积累一些常用的格式配置,问题也不大,更大的问题在后面。
4 同时输出到终端和文件
一般在程序的调试阶段,我希望日志能够同时输出到终端和文件,方便调试。
但是,函数 logging.basicConfig()
是不允许同时设置参数 stream(终端) 和 filename(文件) 的,这意味着,如果要想让日志同时输出到终端和文件,就不得不使用一些更底层的接口。
曾经,我就在这一步倒下,前段时间,又重新爬了起来。
简单来说,你需要使用的主要是三个类:
类 | 作用 |
---|---|
Logger |
该类型的对象用于直接在应用程序代码中调用函数 |
Handler |
该类型的对象将日志发送到目标输出,常用的有 StreamHandler 和 FileHandler |
Formatter |
该类型的对象用于指定日志的输出格式 |
可以通过源码直观的了解这三个类之间的关系:
class Logger(Filterer): def __init__(self, name, level=NOTSET): ... self.handlers = [] ... class Handler(Filterer): def __init__(self, level=NOTSET): ... self.formatter = None ... class Formatter(object): def __init__(self, fmt=None, datefmt=None, style='%', validate=True): ...
由此可以得到创建一个 Logger
的一般流程为:
- 创建一个
Logger
对象 - 创建
Handler
对象 - 创建
Formatter
对象 - 设置
Handler
对象的formatter
- 将
Handler
添加到Logger
特别的, Logger
对象通常用 logging.getLogger(name)
方法来获取创建:
import logging logger = logging.getLogger('example_logger') logger.warning('This is a warning')
假如你希望每个模块单独使用一个 Logger
, 那么你可以使用 __name__
作为 name
参数。
对于 Handler
和 Formatter
, 不同的 Handler
需要的参数不一样,而 Formatter
不存在子类,因此只有一种形式:
class StreamHandler(Handler): def __init__(self, stream=None): ... class FileHandler(StreamHandler): def __init__(self, filename, mode='a', encoding=None, delay=False): ... class Formatter(object): def __init__(self, fmt=None, datefmt=None, style='%', validate=True): ...
这些参数的含义,其实和 logging.basicConfig()
函数中的参数大致相同,而 basicConfig()
还提供了一个参数 handlers
允许你同时为默认日志记录器 root
设置多个 handlers
.
import logging # Create a custom logger logger = logging.getLogger(__name__) # Create handlers c_handler = logging.StreamHandler() # default is sys.stderr f_handler = logging.FileHandler('file.log') c_handler.setLevel(logging.WARNING) f_handler.setLevel(logging.ERROR) # Create formatters and add it to handlers c_format = logging.Formatter('%(name)s - %(levelname)s - %(message)s') f_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') c_handler.setFormatter(c_format) f_handler.setFormatter(f_format) # Add handlers to the logger logger.addHandler(c_handler) logger.addHandler(f_handler) # Add handlers to the root logging.basicConfig(handlers=[c_handler, f_handler])
虽然到这里已经解决了如何将日志消息同时输出到终端和文件的问题,但是,随之而来的还有其他的问题。
5 日志配置
可以看到,创建和配置 Logger
的过程较为繁琐,因此 Python
为我们提供了 logging.config
模块来简化创建和配置 Logger
的过程。
logging.config
也是一个较为复杂的模块,这里就不多说了,想要了解可以查阅以下链接:
- 完整文档 - logging.config — Logging configuration
- 文件配置格式 - Configuration file format
- 字典配置格式 - Dictionary Schema Details
虽然我们可以通过 logging.config
模块简化创建和配置 Logger
的过程,但需要考虑的一个问题是, Logger
的获取和使用的方式。
诚然,我们可以通过 logging.getLogger(name)
的方式获取创建和配置好的 Logger
, 但是,在此之前依然存在一个加载配置的过程。
我不想在每个模块的开头都加载一次配置,因此,一个直接的想法是,将配置加载的过程放到一个单独的模块中,比如 __init__.py
.
但只是这样还是不够,这意味着你还是需要在每个模块的开头做这样的事:
import logging logger = logging.getLogger(...)
也许还可以把 logger
的获取也放到 __init__.py
里面去,这样就只需要:
from pkg import logger
然而,我不是很喜欢这种方式,我更喜欢 logging
模块那种原生的使用方式:
import logging
logging.debug(...)
当然,这可以通过配置默认的日志记录器 root
完成,然而,强迫症的我还是觉得不够完美。
因此,我选择了和 logging
类似的方式,用一个模块封装一个 logger
, 该模块就叫做 logger
:
import logging import logging.config ... # Load config ... logger = logging.getLogger(...) def critical(msg, *args, **kwargs): """Log a message with severity 'CRITICAL' on the root logger.""" logger.critical(msg, *args, **kwargs) fatal = critical def error(msg, *args, **kwargs): """Log a message with severity 'ERROR' on the root logger. """ logger.error(msg, *args, **kwargs) def exception(msg, *args, exc_info=True, **kwargs): """Log a message with severity 'ERROR' on the root logger, with exception information. """ error(msg, *args, exc_info=exc_info, **kwargs) def warning(msg, *args, **kwargs): """Log a message with severity 'WARNING' on the root logger.""" logger.warning(msg, *args, **kwargs) def info(msg, *args, **kwargs): """Log a message with severity 'INFO' on the root logger.""" logger.info(msg, *args, **kwargs) def debug(msg, *args, **kwargs): """Log a message with severity 'DEBUG' on the root logger.""" logger.debug(msg, *args, **kwargs) def log(level, msg, *args, **kwargs): """Log 'msg % args' with the integer severity 'level' on the root logger.""" logger.log(level, msg, *args, **kwargs) def disable(level=logging.CRITICAL): """Disable all logging calls of severity 'level' and below.""" logger.manager.disable = level logger.manager._clear_cache()
如此,便可以这样:
import logger
logger.info(...)
- Update 2019-01-01: 这里的代码其实是有问题的,更新后的代码:Python logging module wrapper configuration.
6 结语
总感觉日志配置部分做了一些多于的事情 QAQ
我也不知道这样做对不对,不过,自己心里舒坦了是最重要的 @_@
另外, logging.config
模块的使用挺麻烦的,建议直接找一个模板抄抄抄。
Over !