Loading

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 创建 FileHandler
  • filemode:如果指定 filename,则以此模式打开文件(‘r’、‘w’、‘a’)。默认为“a”
  • format:配置日志格式字符串,参考:logrecord-attributes
  • datefmt:配置时间格式字符串,支持time.strftime() 所接受的日期/时间格式
  • level:指定日志记录器级别,默认为WARNING
  • handlers:指定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都有一个name属性,它代表了这个logger在用户程序中所属的模块(与作用域的概念类似),不同模块下logger的name可以通过.来组织层级关系,比如hookhook.sparkhook.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,它们常用的配置方法包括:

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可以通过三种方法配置:

  1. 在python代码中显式地声明loggers、handlers和formatters等组件并调用相关方法进行配置
  2. 创建一个logging配置文件,并调用fileConfig()进行配置
  3. 声明一个包含配置信息的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)

posted @ 2022-01-24 11:13  云野Winfield  阅读(1722)  评论(0编辑  收藏  举报