Python Logger模块让老板眼前一亮

什么是Logger?:

ChatGPT
Logger是Python标准库中logging模块提供的一种日志记录工具。它允许你在应用程序中记录不同级别的日志消息。使用Logger有助于管理和组织日志输出,以及提供可配置的日志记录。

以下是Logger的主要组成部分:

Logger(记录器):是主要的接口,负责发送日志消息。应用程序通常会创建一个或多个Logger对象,每个Logger对象负责一组相关的功能或模块。

Handler(处理器):决定将日志消息发送到何处。Handler可以将日志消息输出到控制台、文件、网络等不同的目标。

Formatter(格式化器):定义日志消息的输出格式。Formatter可以格式化消息的时间、级别、模块名等信息。

Filter(过滤器):允许更细粒度地控制哪些日志消息应该被处理。通过使用过滤器,可以根据需要选择性地记录或忽略某些消息。

下面是一个简单的例子,演示如何使用Logger记录日志:

import logging

# 创建Logger对象  名字为  my_logger
logger = logging.getLogger('my_logger')
logger.setLevel(logging.DEBUG)

# 创建文件处理器,并设置级别为DEBUG  输出文件名字为:my_log.log
file_handler = logging.FileHandler('my_log.log')
file_handler.setLevel(logging.DEBUG)

# 创建控制台处理器,并设置级别为INFO
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)

# 创建日志消息的格式化器
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')

# 将格式化器添加到处理器 
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)

# 将处理器添加到记录器
logger.addHandler(file_handler)
logger.addHandler(console_handler)

# 记录不同级别的日志消息
logger.debug('This is a debug message')
logger.info('This is an info message')
logger.warning('This is a warning message')
logger.error('This is an error message')
logger.critical('This is a critical message')

运行效果

同时还可以发现 

控制台终端输出的消息  比文件中 少了一条DEBUG消息

这是因为:

终端中设置的是INFO级别(日志消息只有>=INFO才记录),文件时DEBUG级别(日志消息只有>=DEBUG才记录), 而日志的级别从低到高分别是:DEBUG->INFO->WARNING->ERROR->CRITICAL。

对应的解释如下:

  1. DEBUG: 用于详细的调试信息。在开发和调试阶段使用,通常不应在生产环境中启用。

  2. INFO: 提供程序正常运行时的信息。适用于生产环境,用于确认程序是否按照预期工作。

  3. WARNING: 表示可能的问题,但不会影响程序的正常运行。需要注意并进行检查,以确保程序的稳定性。

  4. ERROR: 指示更严重的问题,可能导致某些功能的失败。需要及时关注和修复。

  5. CRITICAL: 表示严重的错误,可能导致程序无法继续运行。通常需要立即解决。

  6. 在给定的日志配置中,如果设置了级别为INFO,那么 INFO、WARNING、ERROR、CRITICAL级别的日志消息都会被记录,而DEBUG级别的消息将被忽略

同时还有一些没用的小知识:

logging.basicConfig函数各参数:

属性名称 格式 说明
filename 指定日志文件名
filemode 指定文件的打开模式,'w'或者'a';
format 格式器 指定输出的格式和内容
datefmt 指定时间格式,同time.strftime()
level 设置日志级别,默认为logging.WARNNING
stream 指定将日志的输出流,可以指定输出到sys.stderr,sys.stdout或者文件,默认输出到sys.stderr,当stream和filename同时指定时,stream被忽略

格式器

format = logging.Formatter('%(asctime)s – %(levelname)s – %(message)s')

可能会用到的一些其他参数名字: 用法如上

日志处理器 :

console_handler = logging.StreamHandler()  #创建日志处理器 logger.addHandler(console_handler) #添加日志处理器

还有一些其他的没用的小知识 :其他日志处理器如下

  • StreamHandler:logging.StreamHandler;日志输出到流,可以是sys.stderr,sys.stdout或者文件
  • FileHandler:logging.FileHandler;日志输出到文件
  • BaseRotatingHandler:logging.handlers.BaseRotatingHandler;基本的日志回滚方式
  • RotatingHandler:logging.handlers.RotatingHandler;日志回滚方式,支持日志文件最大数量和日志文件回滚
  • TimeRotatingHandler:logging.handlers.TimeRotatingHandler;日志回滚方式,在一定时间区域内回滚日志文件
  • 日志回滚

    举个栗子:

    import logging
    from logging.handlers import RotatingFileHandler
    
    # 配置日志
    logger = logging.getLogger("example_logger")
    logger.setLevel(logging.DEBUG)
    
    # 创建 RotatingFileHandler 处理器,设置日志文件名  和  最大文件大小 backupCout指定要保留的备份日志文件的数量
    handler = RotatingFileHandler("example.log", maxBytes=1, backupCount=3)
    
    # 配置日志格式
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    handler.setFormatter(formatter)
    
    # 将处理器添加到 logger
    logger.addHandler(handler)
    
    
    def simulate_logging():
        # 模拟写入日志
        for i in range(10):
            logger.debug(f"This is log entry {i}")
    
    
    if __name__ == "__main__":
        simulate_logging()
    

    结果很明显了:

    一共四个日志文件,注意看文件的内容

    在上面的例子中,我们使用了 RotatingFileHandler 处理器,并设置了最大文件大小为 1 字节,备份文件的数量为 3。这意味着当日志文件大小达到 1 字节时,会将当前日志文件切分为一个备份文件,并保留最多 3 个备份文件。

    你可以根据实际需求调整 maxBytesbackupCount 参数。当日志文件大小达到 maxBytes 时,会触发日志回滚。

    还有篇文章不错一定要看:https://www.cnblogs.com/yxh168/articles/11855581.html#:~:text=backupCount,%E6%98%AF%E4%BF%9D%E7%95%99%E6%97%A5%E5%BF%97%E4%B8%AA%E6%95%B0.%E9%BB%98%E8%AE%A4%E7%9A%840%E6%98%AF%E4%B8%8D%E4%BC%9A%E8%87%AA%E5%8A%A8%E5%88%A0%E9%99%A4%E6%8E%89%E6%97%A5%E5%BF%97%2C%E8%8B%A5%E8%AE%BE10%2C%E5%88%99%E5%9C%A8%E6%96%87%E4%BB%B6%E7%9A%84%E5%88%9B%E5%BB%BA%E8%BF%87%E7%A8%8B%E4%B8%AD%E5%BA%93%E4%BC%9A%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E6%9C%89%E8%B6%85%E8%BF%87%E8%BF%99%E4%B8%AA10%2C%E8%8B%A5%E8%B6%85%E8%BF%87%2C%E5%88%99%E4%BC%9A%E4%BB%8E%E6%9C%80%E5%85%88%E5%88%9B%E5%BB%BA%E7%9A%84%E5%BC%80%E5%A7%8B%E5%88%A0%E9%99%A4https://www.cnblogs.com/yxh168/articles/11855581.html#:~:text=backupCount,%E6%98%AF%E4%BF%9D%E7%95%99%E6%97%A5%E5%BF%97%E4%B8%AA%E6%95%B0.%E9%BB%98%E8%AE%A4%E7%9A%840%E6%98%AF%E4%B8%8D%E4%BC%9A%E8%87%AA%E5%8A%A8%E5%88%A0%E9%99%A4%E6%8E%89%E6%97%A5%E5%BF%97%2C%E8%8B%A5%E8%AE%BE10%2C%E5%88%99%E5%9C%A8%E6%96%87%E4%BB%B6%E7%9A%84%E5%88%9B%E5%BB%BA%E8%BF%87%E7%A8%8B%E4%B8%AD%E5%BA%93%E4%BC%9A%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E6%9C%89%E8%B6%85%E8%BF%87%E8%BF%99%E4%B8%AA10%2C%E8%8B%A5%E8%B6%85%E8%BF%87%2C%E5%88%99%E4%BC%9A%E4%BB%8E%E6%9C%80%E5%85%88%E5%88%9B%E5%BB%BA%E7%9A%84%E5%BC%80%E5%A7%8B%E5%88%A0%E9%99%A4

    如何捕获traceback,输出系统错误信息

    import logging
    import traceback
    
    # 配置日志
    logging.basicConfig(level=logging.DEBUG, format='%(asctime)s-%(levelname)s-%(message)s')
    
    
    def divide_numbers(a, b):
        try:
            result = a / b
            return result
        except Exception as e:
            # 捕获异常并记录 traceback 到日志 
            logging.error("An error occurred: %s", e, exc_info=True) # 错误信息捕获
            return None 
    
    
    def main():
        logging.info("logger开始")
        # 模拟除法操作,可能引发异常
        result = divide_numbers(10, 0)
    
        # 如果异常被捕获,程序可以继续执行
        if result is None:
            logging.warning("The result is not available due to a previous error.")
    
        # 模拟正常情况
        result = divide_numbers(10, 2)
        logging.info("The result is: %s", result)
    
    
    if __name__ == "__main__":
        main()
    

    结果一目了然了:

    多模块的logging

    这是更加常见的 因为一个完成的项目应该是包含不同的模块的,我又想针对不同的模块来设置不同的logging,就需要这个了

    module.py

    import logging
     
    # 为模块创建一个日志记录器 
    logger = logging.getLogger(__name__) 
    """
    在这里,__name__ 是一个特殊变量,它代表当前模块的名字。这样做的好处是,每个模块都可以有自己的日志记录器,通过使用模块名字,你可以更容易地追踪日志消息的来源。
    
    如果你在一个脚本中运行,__name__ 将会是 "__main__";
    如果你在一个模块中导入这个模块,__name__ 将会是模块的名字。
    """
    
    # 设置模块的日志级别为DEBUG 
    logger.setLevel(logging.DEBUG) 
    
    def print_message(): 
        logger.debug("这是来自模块的调试消息。") 
        logger.info("这是来自模块的信息消息。")

    main.py

    import logging 
    import module # 配置根日志记录器
     
    logging.basicConfig(level=logging.INFO, 
                        format='%(asctime)s - %(levelname)s - %(message)s'
                      )         # 为主模块创建一个日志记录器 
    
    logger = logging.getLogger(__name__) 
    # logger = logging.getLogger("mainxxxx")    #你也可以自己指定
    
    def main(): 
        logger.info("这是来自主模块的信息消息。") 
        module.print_message() 
    
    if __name__ == "__main__": 
        main()

    在这个例子中,主模块main.py的根日志级别设置为INFO,而module.py的日志级别被设置为DEBUG。这意味着,module.py中的DEBUG级别的日志消息将被输出,而在main.py中只有INFO级别及以上的消息才会被输出。

    这样,当你运行main.py时,你将看到类似以下的输出:

    2023-12-06 00:00:00,000 - INFO - 这是来自主模块的信息消息。

    2023-12-06 00:00:00,000 - DEBUG - 这是来自模块的调试消息。

    2023-12-06 00:00:00,000 - INFO - 这是来自模块的信息消息。

    注意:

    在Python的logging模块中,basicConfig函数是用于配置默认的根日志记录器(root logger)的函数,而不是为特定的logger配置参数。当你在一个模块中使用basicConfig时,它会配置根日志记录器,而不是模块专属的logger。并且只能调用一次,一般放在程序的最开始运行的地方。

    如果你想为特定模块配置独立的logger,通常会使用getLogger来创建一个logger实例,然后对该实例进行配置。这样做的好处是,你可以独立地控制每个logger的配置,而不影响其他模块或根记录器。而且 即使你当前的模块日志记录器 继承了根日志记录器(root logger)的一些设置,只要你在当前的模块中 显示的 设置一些参数 该参数还是会生效  。

    比如上面 模块记录器 继承了 root logger 的info 但是显示的 设置了debug 所以该模块的debug 还是会输出

    有一个知识点:

    当你在不同的模块中使用getLogger(__name__)时,每个模块都会得到一个独立的logger实例,其名称基于模块的名称。这样,你可以轻松地在日志中识别消息来自哪个模块。这样的好处是:如果你有两个模块mainmodule,它们都使用getLogger(__name__)创建logger,那么它们的logger名称分别是mainmodule。当你在日志中看到来自main模块的消息时,你知道这些消息来自main模块。

    例如:

    Logger从来不直接实例化,经常通过logging模块级方法(Module-Level Function)logging.getLogger(name="xxxx")来获得,其中如果name不指定就用root logger。

    名字是以点号分割的命名方式命名的(a.b.c)。这种命名方式里面,后面的loggers是前面logger的子logger,自动继承父loggers的log信息,正因为此,没有必要把一个应用的所有logger都配置一遍,只要把顶层的logger配置好了,然后子logger根据需要继承就行了。对同一个名字的多个调logging.getLogger()方法会返回同一个logger对象

    这句话的理解

    通过YAML文件来配置logger

    第一个案例:

    config.yml

    Log:
      log_level: 1
      path_to_log: ./log

    log.py

    import logging
    import os
    import sys
    from datetime import datetime
    from logging import handlers
    
    import yaml
    
    
    class Logger(object):
        # 日志级别关系映射
        level_relations = {
            'debug': logging.DEBUG,
            'info': logging.INFO,
            'warning': logging.WARNING,
            'error': logging.ERROR,
            'critical': logging.CRITICAL
        }
    
        def __init__(self, log_file_path, level='info', when='midnight', backCount=30,
                     fmt='%(asctime)s - %(module)s.cpp[line:%(lineno)d] - %(levelname)s: %(message)s'):
            """
            :param log_file_path: 日志文件路径
            :param level: 日志级别
            :param when: 切割时间间隔:S: 秒, M: 分, H: 小时, D: 天: W0~W6: 星期几, midnight: 每天零点
            :param backCount: 保留的日志文件数量
            :param fmt: 日志格式
            """
            self.logger = logging.getLogger(log_file_path)
            # self.logger.handlers.clear()
            # 是否已创建日志记录程序
            if not self.logger.handlers:
                self.logger.setLevel(self.level_relations.get(level))
                log_formatter = logging.Formatter(fmt)
    
                # 日志流处理器,将日志输出到屏幕上
                stream_handler = logging.StreamHandler()
                stream_handler.setFormatter(log_formatter)
                self.logger.addHandler(stream_handler)
                # 刷新标准输出缓冲区,确保之前的日志消息被立即输出到屏幕
                sys.stdout.flush()
    
                # 日志定时循环文件处理器,将日志写入到文件
                file_handler = handlers.TimedRotatingFileHandler(filename=log_file_path, when=when, backupCount=backCount,
                                                                 encoding='utf-8')
                file_handler.setFormatter(log_formatter)
                self.logger.addHandler(file_handler)
    
        def __getattr__(self, name):
            # log.xxx 时,如果在日志级别中,返回对应的日志级别方法
            if name in self.level_relations:
                return getattr(self.logger, name)
            # 返回日志对象的指定属性名的值
            return self.logger.__getattribute__(name)
    
    
    # 加载配置文件
    def get_config(file_name):
        # 读取配置文件
        if len(file_name) == 1:
            file_name = 'config.yml'
        else:
            file_name = file_name[1]  # 自定义配置文件名称
        try:
            with open(file_name, 'r', encoding='utf-8') as config_file:
                cfg = yaml.load(config_file, Loader=yaml.FullLoader)
        except:
            with open(file_name, 'r', encoding='ansi') as config_file:
                cfg = yaml.load(config_file, Loader=yaml.FullLoader)
    
        # log保存路径,以年月日命名log文件
        log_file_dir = cfg['Log']['path_to_log']
        log_file = log_file_dir + "/" + datetime.now().strftime('%m_%d_%Y.log')
    
        # 判断log文件夹是否存在,若不存在,创建该目录文件夹
        if not os.path.exists(log_file_dir):
            os.makedirs(log_file_dir)
        # 日志级别
        dict = {"0": "debug", "1": "info", "2": "warning", "3": "error", "4": "critical"}
        # 读取log路径及设置log等级
        log = Logger(log_file, level=dict[str(cfg['Log']['log_level'])])
        return log, cfg
    
    
    log, cfg = get_config(sys.argv)

    第二个案例:(更推荐的)

    resources/logging.yml

    version: 1  # 配置文件版本,当前为1
    
    formatters:
      simple:  # 定义一个名为 "simple" 的日志格式
        format: '%(asctime)s - %(module)s.cpp[line:%(lineno)d] - %(levelname)s: %(message)s'
        # 详细定义日志记录的格式,包括时间、模块、行号、日志级别和消息
    
    handlers:
      timed_rotating:
        class: logging.handlers.TimedRotatingFileHandler
        # 使用 TimedRotatingFileHandler 类来处理日志
        level: DEBUG  # 设置日志处理器的级别为 DEBUG
        formatter: simple  # 使用上面定义的 "simple" 格式
        when: midnight  # 每天的午夜切分日志文件
        interval: 1  # 间隔为1天
        backupCount: 30  # 保留最多30个备份文件
        filename: ./log/snap-video-service.log  # 指定日志文件路径
        encoding: utf-8  # 设置日志文件的编码为 utf-8
    
      console_handler:
        class: logging.StreamHandler
        # 使用 StreamHandler 类来将日志输出到控制台
        level: DEBUG  # 设置日志处理器的级别为 DEBUG
        formatter: simple  # 使用上面定义的 "simple" 格式
        stream: ext://sys.stdout  # 输出到标准输出流
    
    loggers:
      app:
        level: DEBUG  # 设置 "app" logger 的级别为 DEBUG
        handlers: [ timed_rotating ]  # 将 "timed_rotating" 处理器添加到 "app" logger
        propagate: no  # 防止日志传播给父 logger
    
    root:
      level: INFO  # 设置根 logger 的级别为 INFO
      handlers: [ console_handler, timed_rotating ]  # 将两个处理器添加到根 logger
    

    config.yml

    Log:
      log_level: 0
      path_to_log: ./log #保存本地配置
    
    #其他的配置xxxx
    videoSave:
      #推送视频画面的宽,与输入视频流保持一致
      # videoW: 1920
      videoW: 1280
      #推送视频画面的高,与输入视频流保持一致
      # videoH: 1080
      videoH: 720
     

    config.py

    # 导入必要的模块
    import logging
    import logging.config as log_config
    import os
    import yaml
    
    # 配置全局日志
    def global_config(logging_cnf='resources/logging.yml'):
        # 创建 'log' 目录,如果目录已存在则忽略
        os.makedirs('log', exist_ok=True)
    
        # 读取日志配置文件
        with open(logging_cnf, encoding='utf-8') as f:
            # 解析配置文件内容
            log_cnf = yaml.safe_load(f.read())
    
            # 通过字典配置 logging 模块
            log_config.dictConfig(log_cnf)
    
    # 获取根 logger
    logger = logging.getLogger()
    
    # 尝试读取应用程序配置文件
    try:
        with open('./config.yml', 'r', encoding='utf-8') as config_file:
            # 解析配置文件内容
            cfg = yaml.load(config_file, Loader=yaml.FullLoader)
    except (Exception,) as e:
        # 记录错误日志,指示配置文件读取失败
        logger.error("config.yml配置文件读取失败,请核查")
    

    main.py

    import sys
    
    import config
    from utils import VideoSave
    
    if __name__ == '__main__':
        if len(sys.argv) < 2:
            config.global_config()
        else:
            config.global_config(sys.argv[1])

    有一个小问题:

    如果程序是一个死循环,由多个模块组成,运行在服务器上, 我使用

    RotatingFileHandler 处理器的话,我指定每个日志文件的的大小为maxsize,保留backcount=2个副本的话, 那加入我当前处理的日志为C, .1副本内容为  B     .2副本为 A  (比如说我的日志是一个自增的字母)

    那么下一个日志  .1副本内容为C  .2副本为B, 内容A日志就被覆盖没了,当前日志处理的是字母D

    那就会出现 在服务器的 你当前时刻拷贝的一些日志, 比如10-20号文件,

    过了一会 你再去拷贝 就变成了 可能是20-30号文件, 相当于 明明相同的日志内容,时间越靠后 存到了越靠后的日志文件副本中,给我们分析日志带来了问题

    (我上午拷贝的 xxx.1  xxxx.2副本)  到了下午 成了   xxxx.10  xxxx.11副本了 我还得重新改名字。。。。

    值得阅读的文章:

    https://www.jb51.net/article/192489.htmhttps://www.jb51.net/article/192489.htm

    作者:不买Huracan不改名

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python Logger模块让老板眼前一亮

    发表回复