MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python日志模块的使用与配置

2024-02-042.2k 阅读

Python日志模块的使用与配置

日志的重要性

在软件开发过程中,日志记录是一项至关重要的任务。日志提供了程序运行时的详细信息,包括程序执行流程、出现的错误以及关键事件等。通过分析日志,开发人员可以快速定位程序中的问题,优化代码性能,以及了解程序在不同环境下的运行情况。

想象一下,当一个复杂的生产系统出现故障时,如果没有详细的日志记录,开发人员可能需要花费大量时间在代码中添加调试语句来追踪问题的根源。而有了完善的日志系统,开发人员可以直接通过查看日志文件,了解在故障发生前后程序执行的具体步骤,从而迅速找到问题所在。

Python中的日志模块——logging

Python的标准库中提供了logging模块,它为开发者提供了灵活且功能强大的日志记录工具。logging模块可以满足不同场景下的日志需求,从简单的控制台输出到复杂的多文件、多级别日志记录。

基本使用

  1. 简单日志记录 要开始使用logging模块,最简单的方式是使用basicConfig函数进行基本配置,并调用logging模块提供的不同级别的日志记录函数。以下是一个简单的示例:

    import logging
    
    logging.basicConfig(level = logging.INFO)
    
    logging.debug('这是一条调试信息')
    logging.info('这是一条信息')
    logging.warning('这是一条警告信息')
    logging.error('这是一条错误信息')
    logging.critical('这是一条严重错误信息')
    

    在上述代码中,首先通过basicConfig函数设置日志级别为logging.INFO。这意味着只有级别大于或等于INFO的日志信息才会被记录。因此,debug级别的日志信息不会被输出,而infowarningerrorcritical级别的日志信息会被输出到控制台。

  2. 日志级别 logging模块定义了以下几种日志级别,按照严重程度从低到高排列:

    • DEBUG:用于调试目的,通常包含详细的调试信息,在生产环境中可能会过于冗长。
    • INFO:用于记录程序的正常运行信息,比如程序启动、某个功能模块开始执行等。
    • WARNING:表示程序出现了一些可能需要关注的情况,但并不影响程序的正常运行,例如使用了过期的API等。
    • ERROR:表示程序发生了错误,导致部分功能无法正常执行,但程序整体可能仍在运行。
    • CRITICAL:表示程序发生了严重错误,可能导致程序无法继续运行,例如数据库连接完全失败等。

日志格式配置

  1. 自定义日志格式 basicConfig函数可以接受一个format参数来定义日志的格式。例如,我们希望在日志中包含时间、日志级别和日志消息,可以这样配置:

    import logging
    
    logging.basicConfig(level = logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
    
    logging.info('这是一条信息')
    

    在上述format字符串中:

    • %(asctime)s:表示日志记录的时间。
    • %(levelname)s:表示日志级别。
    • %(message)s:表示实际的日志消息。
  2. 更多格式占位符 除了上述常用的占位符外,logging模块还提供了许多其他占位符:

    • %(name)s:记录日志的模块名。
    • %(filename)s:记录日志的文件名。
    • %(lineno)d:日志记录所在的行号。
    • %(process)d:进程ID。
    • %(thread)d:线程ID。

    例如,以下配置可以让我们知道日志是在哪个文件的哪一行产生的:

    import logging
    
    logging.basicConfig(level = logging.INFO, format='%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s')
    
    def some_function():
        logging.info('这是函数内的一条信息')
    
    some_function()
    

日志输出到文件

  1. 基本文件输出 要将日志输出到文件,而不是控制台,可以在basicConfig函数中使用filename参数。例如:

    import logging
    
    logging.basicConfig(level = logging.INFO, filename='app.log', format='%(asctime)s - %(levelname)s - %(message)s')
    
    logging.info('这是一条写入文件的信息')
    

    上述代码会将INFO级别及以上的日志信息写入到app.log文件中。每次运行程序时,新的日志会追加到文件末尾。

  2. 文件模式 basicConfig函数的filemode参数可以指定文件的打开模式。默认模式是'a'(追加模式),如果希望每次运行程序时覆盖原有的日志文件,可以将filemode设置为'w'。例如:

    import logging
    
    logging.basicConfig(level = logging.INFO, filename='app.log', filemode='w', format='%(asctime)s - %(levelname)s - %(message)s')
    
    logging.info('这是一条覆盖写入文件的信息')
    

日志处理器(Handlers)

  1. 什么是日志处理器 日志处理器(Handlers)负责将日志记录发送到不同的目的地,如控制台、文件、网络等。logging模块提供了多种类型的处理器,如StreamHandler(用于输出到控制台)、FileHandler(用于输出到文件)、SocketHandler(用于通过网络发送日志)等。

  2. 使用多个处理器 有时候,我们可能希望同时将日志输出到控制台和文件。这可以通过创建多个处理器并将它们添加到日志记录器(Logger)来实现。以下是一个示例:

    import logging
    from logging.handlers import FileHandler
    
    # 创建日志记录器
    logger = logging.getLogger('my_logger')
    logger.setLevel(logging.INFO)
    
    # 创建控制台处理器
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    
    # 创建文件处理器
    file_handler = FileHandler('app.log')
    file_handler.setLevel(logging.INFO)
    
    # 创建日志格式
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    
    # 将格式应用到处理器
    console_handler.setFormatter(formatter)
    file_handler.setFormatter(formatter)
    
    # 将处理器添加到日志记录器
    logger.addHandler(console_handler)
    logger.addHandler(file_handler)
    
    logger.info('这是一条同时输出到控制台和文件的信息')
    

    在上述代码中:

    • 首先通过logging.getLogger('my_logger')获取一个名为my_logger的日志记录器,并设置其日志级别为INFO
    • 然后分别创建了StreamHandler(控制台处理器)和FileHandler(文件处理器),并设置它们的日志级别为INFO
    • 接着创建了一个日志格式Formatter,并将其应用到控制台处理器和文件处理器。
    • 最后将这两个处理器添加到日志记录器logger中,这样日志记录器就会将日志同时发送到控制台和文件。

日志记录器(Loggers)

  1. 日志记录器层次结构 logging模块采用层次结构的日志记录器。根日志记录器是所有日志记录器的祖先。通过logging.getLogger()函数获取的日志记录器,如果没有指定名称,默认返回根日志记录器。当我们指定一个名称时,例如logging.getLogger('my_module'),就会创建一个名称为my_module的日志记录器,并且它是根日志记录器的子记录器。

    日志记录器的层次结构有助于管理不同模块的日志记录。例如,在一个大型项目中,不同的模块可以使用各自独立的日志记录器,并且可以根据模块的需求分别配置日志级别等属性。

  2. 子日志记录器的继承 子日志记录器会继承父日志记录器的配置,包括日志级别和处理器等。例如:

    import logging
    
    # 获取根日志记录器并设置级别
    root_logger = logging.getLogger()
    root_logger.setLevel(logging.INFO)
    
    # 获取子日志记录器
    sub_logger = logging.getLogger('my_module')
    
    sub_logger.info('这是子日志记录器的信息')
    

    在上述代码中,虽然没有直接为sub_logger设置日志级别,但由于它继承了根日志记录器的级别(INFO),所以sub_logger.info()的日志信息会被输出。

    不过,我们也可以为子日志记录器单独设置日志级别,以覆盖从父日志记录器继承的级别。例如:

    import logging
    
    # 获取根日志记录器并设置级别
    root_logger = logging.getLogger()
    root_logger.setLevel(logging.INFO)
    
    # 获取子日志记录器并设置单独的级别
    sub_logger = logging.getLogger('my_module')
    sub_logger.setLevel(logging.DEBUG)
    
    sub_logger.debug('这是子日志记录器的调试信息')
    

    在这个例子中,由于sub_logger的日志级别被设置为DEBUG,所以sub_logger.debug()的日志信息会被输出,尽管根日志记录器的级别是INFO

高级日志配置——使用配置文件

  1. 配置文件格式 对于更复杂的日志配置,使用配置文件是一个更好的选择。logging模块支持使用configparser格式的配置文件。以下是一个简单的配置文件示例(假设文件名为logging.conf):

    [loggers]
    keys = root, my_module
    
    [handlers]
    keys = consoleHandler, fileHandler
    
    [formatters]
    keys = simpleFormatter
    
    [logger_root]
    level = INFO
    handlers = consoleHandler, fileHandler
    
    [logger_my_module]
    level = DEBUG
    handlers = consoleHandler
    qualname = my_module
    propagate = 0
    
    [handler_consoleHandler]
    class = StreamHandler
    level = INFO
    formatter = simpleFormatter
    args = (sys.stdout,)
    
    [handler_fileHandler]
    class = FileHandler
    level = INFO
    formatter = simpleFormatter
    args = ('app.log', 'a')
    
    [formatter_simpleFormatter]
    format = %(asctime)s - %(levelname)s - %(message)s
    datefmt = %Y-%m-%d %H:%M:%S
    

    在上述配置文件中:

    • [loggers]部分定义了日志记录器,包括根日志记录器root和名为my_module的子日志记录器。
    • [handlers]部分定义了两个处理器,consoleHandler(控制台处理器)和fileHandler(文件处理器)。
    • [formatters]部分定义了一个日志格式simpleFormatter
    • [logger_root]部分配置了根日志记录器的级别和使用的处理器。
    • [logger_my_module]部分配置了my_module子日志记录器的级别、处理器等属性。propagate = 0表示该子日志记录器的日志不会传播到父日志记录器。
    • [handler_consoleHandler][handler_fileHandler]分别配置了控制台处理器和文件处理器的详细信息,如级别、格式和参数等。
    • [formatter_simpleFormatter]配置了日志格式的具体内容和日期格式。
  2. 加载配置文件 在Python代码中,可以使用logging.config.fileConfig()函数来加载配置文件。例如:

    import logging
    import logging.config
    
    logging.config.fileConfig('logging.conf')
    
    logger = logging.getLogger('my_module')
    logger.debug('这是根据配置文件记录的调试信息')
    

    上述代码加载了logging.conf配置文件,并根据配置获取了my_module日志记录器,然后使用该记录器记录了一条调试信息。由于配置文件中my_module日志记录器的级别被设置为DEBUG,所以这条调试信息会根据配置被正确处理(在这个例子中会输出到控制台)。

日志过滤(Filters)

  1. 什么是日志过滤 日志过滤允许我们根据特定的条件决定是否记录一条日志。例如,我们可能只想记录来自某个特定模块或者满足特定条件的日志信息。logging模块提供了Filter类来实现日志过滤功能。

  2. 自定义过滤器 要创建一个自定义过滤器,需要继承logging.Filter类并实现filter方法。以下是一个简单的示例,该过滤器只允许记录包含特定关键字的日志信息:

    import logging
    
    class KeywordFilter(logging.Filter):
        def __init__(self, keyword):
            super().__init__()
            self.keyword = keyword
    
        def filter(self, record):
            return self.keyword in record.getMessage()
    
    logger = logging.getLogger('my_logger')
    logger.setLevel(logging.INFO)
    
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    
    keyword_filter = KeywordFilter('重要')
    console_handler.addFilter(keyword_filter)
    
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    console_handler.setFormatter(formatter)
    
    logger.addHandler(console_handler)
    
    logger.info('这是一条普通信息')
    logger.info('这是一条重要信息')
    

    在上述代码中:

    • 首先定义了一个KeywordFilter类,它继承自logging.Filterfilter方法检查日志消息中是否包含指定的关键字重要
    • 然后创建了一个KeywordFilter实例,并将其添加到控制台处理器console_handler中。
    • 当使用logger记录日志时,只有包含重要关键字的日志信息会被输出到控制台。

日志轮转(Log Rotation)

  1. 为什么需要日志轮转 在长期运行的程序中,日志文件可能会不断增大,占用大量的磁盘空间。日志轮转就是为了解决这个问题,它允许我们定期将旧的日志文件归档,并创建新的日志文件。

  2. 使用TimedRotatingFileHandler进行日志轮转 logging.handlers.TimedRotatingFileHandler类可以根据时间间隔进行日志轮转。例如,我们希望每天创建一个新的日志文件,可以这样使用:

    import logging
    from logging.handlers import TimedRotatingFileHandler
    
    logger = logging.getLogger('my_logger')
    logger.setLevel(logging.INFO)
    
    timed_handler = TimedRotatingFileHandler('app.log', when='D', interval = 1, backupCount = 7)
    timed_handler.setLevel(logging.INFO)
    
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    timed_handler.setFormatter(formatter)
    
    logger.addHandler(timed_handler)
    
    for i in range(10):
        logger.info(f'这是第{i + 1}条信息')
    

    在上述代码中:

    • TimedRotatingFileHandlerwhen='D'表示按天进行日志轮转,interval = 1表示每天轮转一次,backupCount = 7表示最多保留7个旧的日志文件。
    • 随着时间推移,每天会创建一个新的app.log文件,旧的日志文件会按照日期命名并保存,当超过7个旧文件时,最早的文件会被删除。
  3. 使用RotatingFileHandler进行日志大小轮转 logging.handlers.RotatingFileHandler类可以根据日志文件的大小进行轮转。例如,当日志文件大小达到1MB时进行轮转:

    import logging
    from logging.handlers import RotatingFileHandler
    
    logger = logging.getLogger('my_logger')
    logger.setLevel(logging.INFO)
    
    rotating_handler = RotatingFileHandler('app.log', maxBytes = 1024 * 1024, backupCount = 5)
    rotating_handler.setLevel(logging.INFO)
    
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    rotating_handler.setFormatter(formatter)
    
    logger.addHandler(rotating_handler)
    
    for i in range(10000):
        logger.info(f'这是第{i + 1}条信息')
    

    在上述代码中:

    • RotatingFileHandlermaxBytes = 1024 * 1024表示日志文件最大为1MB,backupCount = 5表示最多保留5个旧的日志文件。
    • app.log文件大小达到1MB时,会将其重命名为app.log.1,然后创建一个新的app.log文件继续记录日志。当旧文件数量超过5个时,最早的文件会被删除。

日志在多线程和多进程中的应用

  1. 多线程中的日志记录 在多线程环境中,多个线程可能同时记录日志。为了避免日志记录混乱,logging模块是线程安全的。这意味着多个线程可以同时调用日志记录函数,而不会出现数据竞争问题。例如:

    import logging
    import threading
    
    logger = logging.getLogger('my_logger')
    logger.setLevel(logging.INFO)
    
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(threadName)s - %(message)s')
    console_handler.setFormatter(formatter)
    
    logger.addHandler(console_handler)
    
    def thread_function():
        logger.info('这是线程中的信息')
    
    threads = []
    for _ in range(5):
        t = threading.Thread(target = thread_function)
        threads.append(t)
        t.start()
    
    for t in threads:
        t.join()
    

    在上述代码中,通过在日志格式中添加%(threadName)s占位符,可以清楚地看到每条日志是由哪个线程产生的。由于logging模块的线程安全性,多个线程同时记录日志不会出现问题。

  2. 多进程中的日志记录 在多进程环境中,情况相对复杂一些。由于每个进程都有自己独立的内存空间,默认情况下,不同进程的日志记录会相互独立。如果希望多个进程共享日志配置和输出,可以使用QueueHandlerQueueListener。以下是一个简单的示例:

    import logging
    from logging.handlers import QueueHandler, QueueListener
    from queue import Queue
    import multiprocessing
    
    log_queue = Queue()
    
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(processName)s - %(message)s')
    
    file_handler = logging.FileHandler('app.log')
    file_handler.setFormatter(formatter)
    
    queue_listener = QueueListener(log_queue, file_handler)
    queue_listener.start()
    
    def process_function():
        queue_handler = QueueHandler(log_queue)
        root = logging.getLogger()
        root.addHandler(queue_handler)
        logger = logging.getLogger('my_logger')
        logger.info('这是进程中的信息')
    
    processes = []
    for _ in range(5):
        p = multiprocessing.Process(target = process_function)
        processes.append(p)
        p.start()
    
    for p in processes:
        p.join()
    
    queue_listener.stop()
    

    在上述代码中:

    • 首先创建了一个Queue对象log_queue,用于在进程间传递日志记录。
    • 创建了一个QueueListener,它监听log_queue,并将接收到的日志记录通过file_handler写入到文件中。
    • 在每个进程中,创建一个QueueHandler,将日志记录发送到log_queue中。这样,多个进程的日志记录就可以统一处理并写入到同一个日志文件中。

通过以上对Python日志模块logging的详细介绍,从基本使用到高级配置,包括日志格式、处理器、记录器、过滤、轮转以及在多线程和多进程中的应用,开发者可以根据项目的具体需求构建出完善的日志系统,为程序的开发、调试和维护提供有力的支持。