9.统一日志

予早 2024-10-05 11:30:39
Categories: Tags:

统一日志

通常日志分为访问日志和业务日志

https://docs.python.org/zh-cn/3/howto/logging.html

logging

logging为Python标准库。日志,主要由记录器Logger、处理器Handler、Formatter格式器构成。

  1. Logger(记录器):这是日志系统的入口点。每个记录器都有一个名称,并且记录器之间可以存在父子关系。
  2. Handler(处理器):记录器将日志消息发送到处理器,处理器决定如何处理这些消息。常见的处理器有StreamHandler、FileHandler等。
  3. Formatter(格式化器):格式化器定义了日志消息的最终输出格式。
  4. Log Level(日志级别):每条日志消息都有一个级别(比如DEBUG、INFO、WARNING、ERROR、CRITICAL),日志系统会根据级别决定是否记录某条消息。

日志记录原理

Logger,记录器

记录器负责记录日志,

根记录器

import logging

# 通过使用默认的Formatter创建一个StreamHandler并将其加入根日志记录器来为日志记录系统执行基本配置
# basicconfig不能同时往文件和标准输出写
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s",
    datefmt="%a, %d %b %Y %H:%M:%S",
    filename="test.log",
    filemode='a'
)
logging.debug("debug message")
logging.info("info message")
logging.warning("warning message")
logging.error("error message")
logging.critical("critical message")

自定义记录器

import logging

logger = logging.getLogger()
fh = logging.FileHandler('test.log', encoding='utf-8')
ch = logging.StreamHandler()
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")

fh.setLevel(logging.DEBUG)
fh.setFormatter(formatter)
ch.setFormatter(formatter)
logger.addHandler(fh)
logger.addHandler(ch)

logging.debug("debug message")
logging.info("info message")
logging.warning("warning message")
logging.error("error message")
logging.critical("critical message")

注:logging.debug(json.dumps(result))中debug级别日志可能不会输出,但是json序列化会执行,若result内容很多,耗时会很长,应当使用环境变量debug来控制,如if debug: logging.debug(json.dumps(result))

Handler,处理器

StreamHandler用于将日志消息输出到流(如控制台、文件等)。

FileHandler用于将日志消息输出到文件。

RotatingFileHandler:用于将日志消息输出到文件,并在文件达到一定大小时进行日志轮换。

TimedRotatingFileHandler:用于将日志消息输出到文件,并在特定时间间隔进行日志轮换。

SocketHandler:用于将日志消息发送到网络套接字。

SMTPHandler:用于通过电子邮件发送日志消息。

StreamHandler

import logging

logger = logging.getLogger('my_logger')
logger.setLevel(logging.DEBUG)

ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)

logger.addHandler(ch)

logger.debug('这是一个调试消息')

FileHandler

import logging

logger = logging.getLogger('my_logger')
logger.setLevel(logging.DEBUG)

fh = logging.FileHandler('my_log.log', encoding="utf-8")
fh.setLevel(logging.DEBUG)

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)

logger.addHandler(fh)

logger.debug('这是一个调试消息')

Formatter,格式化器

格式化器中指定相关信息

https://docs.python.org/3/library/logging.html#logrecord-attributes

%(asctime)s:日志事件发生的时间,格式是可配置的,默认格式为’YYYY-MM-DD HH:MM:SS,sss’。(asc 是 “ASCII” 的缩写。asctime 代表 “ASCII time”,即使用ASCII字符表示的时间字符串)
%(levelname)s:日志级别名称(如DEBUG、INFO、WARNING、ERROR、CRITICAL)。
%(name)s:记录器的名称。
%(message)s:日志消息。
%(filename)s:调用日志记录函数的源文件的文件名。
%(pathname)s:调用日志记录函数的源文件的全路径名。
%(module)s:调用日志记录函数的模块名。
%(funcName)s:调用日志记录函数的函数名。
%(lineno)d:调用日志记录函数的语句所在的行号。
%(created)f:日志事件发生时的时间戳(自UNIX epoch开始的秒数)。
%(msecs)d:日志事件发生时的毫秒部分。
%(relativeCreated)d:日志事件发生时相对于Logger创建时间的毫秒数。
%(thread)d:线程ID。
%(threadName)s:线程名称。
%(process)d:进程ID。
%(processName)s:进程名称。
————————————————

                        版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/Dontla/article/details/140261857

为什么需要两个 setLevel(logging.DEBUG)
记录器级别设置:这是为了控制日志消息的初始过滤。只有级别符合条件的消息才会被进一步处理。这一层的设置确保了不必要的日志消息不会浪费资源。

处理器级别设置:这是为了控制具体的输出行为。即使一条日志消息通过了记录器的级别过滤,如果它的级别低于处理器的级别,它也不会被输出到该处理器。这一层的设置确保了不同的处理器可以有不同的日志输出级别。例如,你可能希望某些重要消息被写入文件,而所有消息都打印到控制台。

import logging

logger = logging.getLogger('my_logger')
logger.setLevel(logging.DEBUG)

ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)

logger.addHandler(ch)

try:
    1 / 0
except ZeroDivisionError as e:
    logger.error('捕捉到异常', exc_info=True)

日志级别

每个日志级别函数都可以接受以下参数:

  1. msg:这是必需的参数,表示要记录的日志消息。可以是字符串,也可以是包含占位符的字符串,然后通过*args或**kwargs传入占位符对应的值。
  2. *args:用于为msg中的占位符提供参数。这些参数会以位置参数的形式传递,替换msg中的占位符。
  3. **kwargs:用于为日志消息提供额外的参数,通常用于设置一些可选参数,比如exc_info、stack_info、extra等。
    1). exc_info:用于记录异常信息。如果设置为True,日志消息会包含异常的堆栈信息。这在记录异常时非常有用。
    2). stack_info:如果设置为True,日志消息会包含调用日志函数时的堆栈信息。
    3). extra:一个字典,用于向日志消息添加额外的信息。这个字典中的键必须是格式化器中使用的占位符。格式化器没有使用所有额外信息不会报错,但格式化器使用的信息在额外信息中没有则一定会报错。
import logging

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(user)s')
ch.setFormatter(formatter)
logger.addHandler(ch)

# 定义额外的参数
extra_info = {'user': 'admin'}

# 记录WARNING级别的日志,并使用位置参数
logger.info('这是一个警告消息,参数1: %s, 参数2: %d', 'param1', 2, stack_info=True, extra=extra_info)

# 记录ERROR级别的日志,并包含异常信息
try:
    1 / 0
except ZeroDivisionError as e:
    logger.error('捕捉到异常', exc_info=True, extra=extra_info)


logging.Formatter("[%(asctime)s] [%(levelname)8s] --- %(message)s (%(filename)s:%(lineno)s)", "%Y-%m-%d %H:%M:%S")

日志打印中不建议使用f-string,建议使用%惰性格式化

Pylint 提示 W1203: logging-fstring-interpolation 是因为在使用 logging 模块时,推荐使用惰性 % 格式化而不是 f-string 格式化。这是因为使用惰性 % 格式化能够在日志级别低于当前设置的情况下避免不必要的字符串格式化操作,从而提高性能。

在 logging 模块中,如果你使用 f-string 进行字符串插值,即使日志消息最终不会被记录,字符串插值操作仍然会发生。这可能会导致性能问题,特别是在大量日志记录的情况下。相反,使用 % 格式化字符串时,格式化操作只有在日志消息实际被记录时才会发生。

优化

消息参数的格式化将被推迟,直到无法避免。但是,计算传递给日志记录方法的参数也可能很消耗资源,如果记录器只是丢弃你的事件,你可能希望避免这样做。要决定做什么,可以调用 isEnabledFor() 方法,该方法接受一个 level 参数,如果记录器为该级别的调用创建了该事件,则返回 true 。 你可以写这样的代码:

if logger.isEnabledFor(logging.DEBUG):
    logger.debug('Message with %s, %s', expensive_func1(),
                                        expensive_func2())

Logger用于记录日志,可以设置Logger日志级别以记录该级别及以上级别的日志,Logger以名称组织为树状结构

Handler用于处理Logger记录的日志,可以设置Handler日志级别以处理该级别及以上级别的日志

Manager用于管理Logger和Handler

Logger是一种树状结构,某个Logger记录一条日志,若日志级别低于Logger设置的级别则该日志不会被记录(Logger级别为NOSET时会依据父Logger级别判断),若等于或高于Logger设置的级别,则由Logger的Handler处理,该Logger的所有Handler处理完该日志后,若Logger可向上传播日志,则将该日志交由父Logger的Handler处理,以此类推直到根Logger,若不可传播日志则处理该日志完毕,最终若未发现任何一个Handler则会将该日志交由默认的WARNING级别的处理器输出到标准错误。

import logging

# 创建一个示例的 logger 层级结构
logger = logging.getLogger('parent.child1')
logger = logging.getLogger('parent.child2')
logger = logging.getLogger('parent.child3')
# logger = logging.getLogger('parent')

# 输出当前 logger 树
def print_logger_tree(logger_obj, indent=0):
    print(' ' * indent + logger_obj.name)
    for handler in logger_obj.handlers:
        print(' ' * (indent + 4) + str(handler))
    for child_logger in logger_obj.manager.loggerDict.values():
        if isinstance(child_logger, logging.Logger) and child_logger.parent == logger_obj:
            print_logger_tree(child_logger, indent + 4)

print_logger_tree(logging.root)