Python logging

Python logging的使用教程


作者:elfin   资料来源:官方网址

目录:



#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2022/12/15 下午5:34
# @Author  : firstelfin
# @File    : get_logger.py

import logging
import os
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))


def set_logger(name, log_path):
    app_logger = logging.getLogger(name=name)
    level = logging.INFO if int(os.getenv("RANK", -1)) in {-1, 0} else logging.ERROR
    app_logger.setLevel(level)
    # script header
    sh = logging.StreamHandler()
    # file header
    # fh = logging.FileHandler(log_path, encoding="utf-8")
    # fh = TimedRotatingFileHandler(log_path, "D", 1, 30, encoding="utf-8")
    fh = TimedRotatingFileHandler(log_path, "D", 1, 30, encoding="utf-8")
    # fh.namer = logger_namer
    fh.suffix = "%Y-%m-%d.log"  # 时间模块格式不能修改,修改后会导致自动删除文件失效
    log_format = logging.Formatter(fmt="%(asctime)s - [%(levelname)8s]: %(message)s")
    sh.setLevel(level)
    fh.setLevel(level)
    sh.setFormatter(log_format)
    fh.setFormatter(log_format)
    app_logger.addHandler(sh)
    app_logger.addHandler(fh)
    app_logger.info(f"操作员:{os.getlogin()}@{s.getsockname()[0]}")
    return app_logger


if __name__ == '__main__':
    log = set_logger(name=__name__, log_path="../output/test.log")
    log.critical("error")

1、日志模块的主要知识点

日志模块的配置:

  • 记录器:getLogger()
  • 日志级别
  • 处理器对象
  • 格式器对象
  • Filter对象
  • LogRecord属性
  • LoggerAdpter对象
  • 线程安全
  • 模块级别函数
  • 与警告模块集成

Top  ---  Bottom

2、基础教程

日志是对软件执行时所发生事件的一种追踪方式。软件开发人员对他们的代码添加日志调用,借此来指示某事件的发生。一个事件通过一些包含变量数据的描述信息来描述(比如:每个事件发生时的数据都是不同的)。开发者还会区分事件的重要性,重要性也被称为 等级严重性

2.1 什么时候使用日志

​ 对于简单的日志使用来说日志功能提供了一系列便利的函数。它们是 debug()info()warning()error()critical()。想要决定何时使用日志,请看下表,其中显示了对于每个通用任务集合来说最好的工具。

你想要执行的任务 此任务的最好的工具
对于命令行或程序的应用,结果显示在控制台。 print()
在对程序的普通操作发生时提交事件报告(比如:状态监控和错误调查) logging.info() 函数(当有诊断目的需要详细输出信息时使用 logging.debug() 函数)
提出一个警告信息基于一个特殊的运行时事件 warnings.warn() 位于代码库中,该事件是可以避免的,需要修改客户端应用以消除告警logging.warning() 不需要修改客户端应用,但是该事件还是需要引起关注
对一个特殊的运行时事件报告错误 引发异常
报告错误而不引发异常(如在长时间运行中的服务端进程的错误处理) logging.error(), logging.exception()logging.critical() 分别适用于特定的错误及应用领域

日志功能应以所追踪事件级别或严重性而定。各级别适用性如下(以严重性递增):

级别 何时使用
DEBUG 细节信息,仅当诊断问题时适用。
INFO 确认程序按预期运行
WARNING 表明有已经或即将发生的意外(例如:磁盘空间不足)。程序仍按预期进行
ERROR 由于严重的问题,程序的某些功能已经不能正常执行
CRITICAL 严重的错误,表明程序已不能继续执行

默认的级别是WARNING,意味着只会追踪该级别及以上的事件,除非更改日志配置。


Top  ---  Bottom

2.2 打印日志到终端

案例:命令行打印

所追踪事件可以以不同形式处理。最简单的方式是输出到控制台。另一种常用的方式是写入磁盘文件。

import logging
logging.warning('Watch out!')  # will print a message to the console
logging.info('I told you so')  # will not print anything

在终端将会显示:

WARNING:root:Watch out!

输出到命令行。INFO 消息并没有出现,因为默认级别是 WARNING 。打印的信息包含事件的级别以及在日志调用中的对于事件的描述,例如“Watch out!”。暂时不用担心“root”部分:之后会作出解释。输出格式可按需要进行调整,格式化选项同样会在之后作出解释。


Top  ---  Bottom

2.3 打印日志到文件

案例:记录到文件

一种非常常见的情况是将日志事件记录到文件,让我们继续往下看。请确认启动新的Python 解释器,不要在上一个环境中继续操作:

import logging
logging.basicConfig(filename='example.log', encoding='utf-8', level=logging.DEBUG)
logging.debug('This message should go to the log file')
logging.info('So should this')
logging.warning('And this, too')
logging.error('And non-ASCII stuff, too, like Øresund and Malmö')

在 3.9 版更改: 增加了 encoding 参数。在更早的 Python 版本中或没有指定时,编码会用 open() 使用的默认值。尽管在上面的例子中没有展示,但也可以传入一个决定如何处理编码错误的 errors 参数。可使用的值和默认值,请参照 open() 的文档。

现在,如果我们打开日志文件,我们应当能看到日志信息:

DEBUG:root:This message should go to the log file
INFO:root:So should this
WARNING:root:And this, too
ERROR:root:And non-ASCII stuff, too, like Øresund and Malmö

该示例同样展示了如何设置日志追踪级别的阈值。该示例中,由于我们设置的阈值是 DEBUG,所有信息全部打印。你页可以设置其他级别。

如果你想从命令行设置日志级别,例如:

--log=INFO

在python文件中接手到此参数后:

loglevel = getattr(logging, "INFO".upper())
print(loglevel)  # 可以把这个loglevel值传给logging.basicConfig
# 输出20
logging.basicConfig(filename="../Log/elfin.log",
                    level=loglevel)

logging.basicConfig调用应该在 debug()info() 等的前面


Top  ---  Bottom

2.4 在多个文件中使用日志模块

如果你有多个文件要打印日志,只需要在主文件配置logging.basicConfig即可。

# myapp.py
import logging
import mylib

def main():
    logging.basicConfig(filename='myapp.log', level=logging.INFO)
    logging.info('Started')
    mylib.do_something()
    logging.info('Finished')

if __name__ == '__main__':
    main()
# mylib.py
import logging

def do_something():
    logging.info('Doing something')

如果你运行 myapp.py ,你应该在 myapp.log 中看到:

INFO:root:Started
INFO:root:Doing something
INFO:root:Finished

​ 这是你期待看到的。 你可以使用 mylib.py 中的模式将此概括为多个模块。 请注意,对于这种简单的使用模式,除了查看事件描述之外,你不能通过查看日志文件来了解应用程序中消息的 来源 。如果要跟踪消息的位置,则需要参考教程级别以外的文档 - 请参阅 进阶日志教程


Top  ---  Bottom

2.5 格式化输出

更改消息的格式:

import logging
logging.basicConfig(format="[%(levelname)8s]:\t%(message)s",
                    filename="../Log/elfin.log",
                    level=logging.INFO)
logging.info("测试文件")
logging.debug("256245")
logging.info("54321")
logging.warning("策划那个徐指向玩")
logging.critical("策划那个徐指向玩")

这将输出:

[INFO]:	测试文件
[INFO]:	54321
[WARNING]:	策划那个徐指向玩
[CRITICAL]:	策划那个徐指向玩

显示时间:

logging.basicConfig(format="[%(levelname)8s]:[%(asctime)s]\t%(message)s",
                    filename="../Log/elfin.log",
                    level=logging.INFO)
logging.info("测试文件")
logging.debug("256245")
logging.info("54321")
logging.warning("策划那个徐指向玩")
logging.critical("策划那个徐指向玩")

这将输出:

[INFO]:[2021-03-05 16:29:52,986]	测试文件
[INFO]:[2021-03-05 16:29:52,987]	54321
[WARNING]:[2021-03-05 16:29:52,987]	    策划那个徐指向玩
[CRITICAL]:[2021-03-05 16:29:52,987]	策划那个徐指向玩

基础教程已经能够满足你的级别使用需求了,更多操作参考高级教程!

Top  ---  Bottom

3、高级教程

日志库采用模块化方法,并提供几类组件:记录器、处理程序、过滤器和格式化程序。

  • 记录器暴露了应用程序代码直接使用的接口。
  • 处理程序将日志记录(由记录器创建)发送到适当的目标。
  • 过滤器提供了更精细的附加功能,用于确定要输出的日志记录。
  • 格式化程序指定最终输出中日志记录的样式。

日志事件信息在 LogRecord 实例中的记录器、处理程序、过滤器和格式化程序之间传递。

​ 通过调用 Logger 类(以下称为 loggers , 记录器)的实例来执行日志记录。 每个实例都有一个名称,它们在概念上以点(句点)作为分隔符排列在命名空间的层次结构中。 例如,名为 'scan' 的记录器是记录器 'scan.text' ,'scan.html' 和 'scan.pdf' 的父级。 记录器名称可以是你想要的任何名称,并指示记录消息源自的应用程序区域。

在命名记录器时使用的一个好习惯是在每个使用日志记录的模块中使用模块级记录器,命名如下:

logger = logging.getLogger(__name__)

这意味着记录器名称跟踪包或模块的层次结构,并且直观地从记录器名称显示记录事件的位置。

​ 记录器层次结构的根称为根记录器。 这是函数 debug()info()warning()error()critical() 使用的记录器,它们只调用根记录器的同名方法。 功能和方法具有相同的签名。 根记录器的名称在记录的输出中打印为 'root' 。

​ 当然,可以将消息记录到不同的地方。 软件包中的支持包含,用于将日志消息写入文件、 HTTP GET/POST 位置、通过 SMTP 发送电子邮件、通用套接字、队列或特定于操作系统的日志记录机制(如 syslog 或 Windows NT 事件日志)。 目标由 handler 类提供。 如果你有任何内置处理程序类未满足的特殊要求,则可以创建自己的日志目标类。

​ 默认情况下,没有为任何日志记录消息设置目标。 你可以使用 basicConfig() 指定目标(例如控制台或文件),如教程示例中所示。 如果你调用函数 debug()info()warning()error()critical() ,他们将检查是否有设置目的地;如果没有设置,它们将在委托给根记录器进行实际的消息输出之前设置目标为控制台( sys.stderr )和默认格式的显示消息。

../_images/logging_flow.png

Top  ---  Bottom

3.1 记录器

Logger 对象有三重任务。首先,它们向应用程序代码公开了几种方法,以便应用程序可以在运行时记录消息。其次,记录器对象根据严重性(默认过滤工具)或过滤器对象确定要处理的日志消息。第三,记录器对象将相关的日志消息传递给所有感兴趣的日志处理程序。

记录器对象上使用最广泛的方法分为两类:配置和消息发送。

这些是最常见的配置方法:

  • Logger.setLevel() 指定记录器将处理的最低严重性日志消息,其中 debug 是最低内置严重性级别, critical 是最高内置严重性级别。 例如,如果严重性级别为 INFO ,则记录器将仅处理 INFO 、 WARNING 、 ERROR 和 CRITICAL 消息,并将忽略 DEBUG 消息。
  • Logger.addHandler()Logger.removeHandler() 从记录器对象中添加和删除处理程序对象。处理程序在以下内容中有更详细的介绍 处理程序
  • Logger.addFilter()Logger.removeFilter() 可以添加或移除记录器对象中的过滤器。 Filter 对象 包含更多的过滤器细节。

配置记录器对象后,以下方法将创建日志消息:

  • Logger.debug()Logger.info()Logger.warning()Logger.error()Logger.critical() 都创建日志记录,包含消息和与其各自方法名称对应的级别。该消息实际上是一个格式化字符串,它可能包含标题字符串替换语法 %s%d%f 等等。其余参数是与消息中的替换字段对应的对象列表。关于 **kwargs ,日志记录方法只关注 exc_info 的关键字,并用它来确定是否记录异常信息。
  • Logger.exception() 创建与 Logger.error() 相似的日志信息。 不同之处是, Logger.exception() 同时还记录当前的堆栈追踪。仅从异常处理程序调用此方法。
  • Logger.log() 将日志级别作为显式参数。对于记录消息而言,这比使用上面列出的日志级别方便方法更加冗长,但这是自定义日志级别的方法。

getLogger() 返回对具有指定名称的记录器实例的引用(如果已提供),或者如果没有则返回 root 。名称是以句点分隔的层次结构。多次调用 getLogger() 具有相同的名称将返回对同一记录器对象的引用。在分层列表中较低的记录器是列表中较高的记录器的子项。例如,给定一个名为 foo 的记录器,名称为 foo.barfoo.bar.bazfoo.bam 的记录器都是 foo 子项。

​ 记录器具有 有效等级 的概念。如果未在记录器上显式设置级别,则使用其父级别作为其有效级别。如果父级没有明确的级别设置,则检查 父级。依此类推,搜索所有上级元素,直到找到明确设置的级别。根记录器始终具有显式级别集(默认情况下为 WARNING )。在决定是否处理事件时,记录器的有效级别用于确定事件是否传递给记录器的处理程序。

​ 子记录器将消息传播到与其上级记录器关联的处理程序。因此,不必为应用程序使用的所有记录器定义和配置处理程序。为顶级记录器配置处理程序并根据需要创建子记录器就足够了。(但是,你可以通过将记录器的 propagate 属性设置 False 来关闭传播。)


Top  ---  Bottom

3.2 处理程序

Handler 对象负责将适当的日志消息(基于日志消息的严重性)分派给处理程序的指定目标。 Logger 对象可以使用 addHandler() 方法向自己添加零个或多个处理程序对象。作为示例场景,应用程序可能希望将所有日志消息发送到日志文件,将错误或更高的所有日志消息发送到标准输出,以及将所有关键消息发送至一个邮件地址。 此方案需要三个单独的处理程序,其中每个处理程序负责将特定严重性的消息发送到特定位置。

标准库包含很多处理程序类型(参见 有用的处理程序 );教程主要使用 StreamHandlerFileHandler

​ 处理程序中很少有方法可供应用程序开发人员使用。与使用内置处理程序对象(即不创建自定义处理程序)的应用程序开发人员相关的唯一处理程序方法是以下配置方法:

  • setLevel() 方法,就像在记录器对象中一样,指定将被分派到适当目标的最低严重性。为什么有两个 setLevel() 方法?记录器中设置的级别确定将传递给其处理程序的消息的严重性。每个处理程序中设置的级别确定处理程序将发送哪些消息。
  • setFormatter() 选择一个该处理程序使用的 Formatter 对象。
  • addFilter()removeFilter() 分别在处理程序上配置和取消配置过滤器对象。

应用程序代码不应直接实例化并使用 Handler 的实例。 相反, Handler 类是一个基类,它定义了所有处理程序应该具有的接口,并建立了子类可以使用(或覆盖)的一些默认行为。


Top  ---  Bottom

3.3 格式化程序

​ 格式化程序对象配置日志消息的最终顺序、结构和内容。 与 logging.Handler 类不同,应用程序代码可以实例化格式化程序类,但如果应用程序需要特殊行为,则可能会对格式化程序进行子类化。构造函数有三个可选参数 —— 消息格式字符串、日期格式字符串和样式指示符。

logging.Formatter.init(fmt=None, datefmt=None, style='%')

​ 如果 style 是 '%',则消息格式字符串使用 %(<dictionary key>)s 样式字符串替换;可能的键值在 LogRecord 属性 中。 如果样式为 '{',则假定消息格式字符串与 str.format() (使用关键字参数)兼容,而如果样式为 '$' ,则消息格式字符串应符合 string.Template.substitute()

建议定义的格式为:

'%(asctime)s - %(levelname)8s - %(message)s'

​ 格式化程序使用用户可配置的函数将记录的创建时间转换为元组。 默认情况下,使用 time.localtime() ;要为特定格式化程序实例更改此项,请将实例的 converter 属性设置为具有相同签名的函数 time.localtime()time.gmtime() 。 要为所有格式化程序更改它,例如,如果你希望所有记录时间都以 GMT 显示,请在格式化程序类中设置 converter 属性(对于 GMT 显示,设置为 time.gmtime )。


Top  ---  Bottom

3.4 配置日志记录[important]

开发者可以通过三种方式配置日志记录:

  1. 使用调用上面列出的配置方法的 Python 代码显式创建记录器、处理程序和格式化程序。
  2. 创建日志配置文件并使用 fileConfig() 函数读取它。
  3. 创建配置信息字典并将其传递给 dictConfig() 函数。

​ 有关最后两个选项的参考文档,请参阅 配置函数 。 以下示例使用 Python 代码配置一个非常简单的记录器/一个控制台处理程序和一个简单的格式化程序:

import logging

# create logger
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG)

# create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
fh = logging.FileHandler(log_path, encoding="utf-8")
fh.setLevel(logging.INFO)

# create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# add formatter to ch \ fh
ch.setFormatter(formatter)
fh.setFormatter(formatter)

# add ch to logger
logger.addHandler(ch)
logger.addHandler(fh)

# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')

从命令行运行此模块将生成以下输出:

$ python simple_logging_module.py
2005-03-19 15:10:26,618 - simple_example - DEBUG - debug message
2005-03-19 15:10:26,620 - simple_example - INFO - info message
2005-03-19 15:10:26,695 - simple_example - WARNING - warn message
2005-03-19 15:10:26,697 - simple_example - ERROR - error message
2005-03-19 15:10:26,773 - simple_example - CRITICAL - critical message

以下 Python 模块创建的记录器、处理程序和格式化程序几乎与上面列出的示例中的相同,唯一的区别是对象的名称:

import logging
import logging.config

logging.config.fileConfig('logging.conf')

# create logger
logger = logging.getLogger('simpleExample')

# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')

这是 logging.conf 文件:

[loggers]
keys=root,simpleExample

[handlers]
keys=consoleHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=

输出几乎与不基于配置文件的示例相同:

$ python simple_logging_config.py
2005-03-19 15:38:55,977 - simpleExample - DEBUG - debug message
2005-03-19 15:38:55,979 - simpleExample - INFO - info message
2005-03-19 15:38:56,054 - simpleExample - WARNING - warn message
2005-03-19 15:38:56,055 - simpleExample - ERROR - error message
2005-03-19 15:38:56,130 - simpleExample - CRITICAL - critical message

你可以看到配置文件方法比 Python 代码方法有一些优势,主要是配置和代码的分离以及非开发者轻松修改日志记录属性的能力。

Notice:

  • fileConfig() 函数接受一个默认参数 disable_existing_loggers ,出于向后兼容的原因,默认为 True 。这可能是你想要的,也可能不是你想要的,因为除非在配置中明确命名它们(或一个上级节点中),否则它将导致在 fileConfig() 调用之前存在的任何非 root 记录器被禁用。有关更多信息,请参阅参考文档,如果需要,请为此参数指定 False
  • 传递给 dictConfig() 的字典也可以用键 disable_existing_loggers 指定一个布尔值,如果没有在字典中明确指定,也默认被解释为 True 。这会导致上面描述的记录器禁用行为,这可能不是你想要的——在这种情况下,明确地为键提供 False 值。

​ 请注意,配置文件中引用的类名称需要相对于日志记录模块,或者可以使用常规导入机制解析的绝对值。因此,你可以使用 WatchedFileHandler (相对于日志记录模块)或 mypackage.mymodule.MyHandler (对于在 mypackage 包中定义的类和模块 mymodule ,其中 mypackage 在Python导入路径上可用)。

​ 在 Python 3.2 中,引入了一种新的配置日志记录的方法,使用字典来保存配置信息。 这提供了上述基于配置文件方法的功能的超集,并且是新应用程序和部署的推荐配置方法。 因为 Python 字典用于保存配置信息,并且由于你可以使用不同的方式填充该字典,因此你有更多的配置选项。 例如,你可以使用 JSON 格式的配置文件,或者如果你有权访问 YAML 处理功能,则可以使用 YAML 格式的文件来填充配置字典。当然,你可以在 Python 代码中构造字典,通过套接字以 pickle 形式接收它,或者使用对你的应用程序合理的任何方法。

以下是与上述相同配置的示例,采用 YAML 格式,用于新的基于字典的方法:

version: 1
formatters:
  simple:
    format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
  console:
    class: logging.StreamHandler
    level: DEBUG
    formatter: simple
    stream: ext://sys.stdout
loggers:
  simpleExample:
    level: DEBUG
    handlers: [console]
    propagate: no
root:
  level: DEBUG
  handlers: [console]

有关使用字典进行日志记录的更多信息,请参阅 配置函数


3.5 python文件直接配置

import logging


class MyFormatter(logging.Formatter):
    # ANSI escape sequences for bold style
    COLORS = {
        'RESET': '\033[0m',
        'BOLD': '\033[1m',
        'GREEN': '\033[32m',
        'CYAN': '\033[36m',
        'RED': '\033[31m',
        'YELLOW': '\033[33m',
        'BLUE': '\033[34m',
    }

    def __init__(self, fmt=None, datefmt=None):
        super().__init__(fmt, datefmt)

    def format(self, record):
        # Original formatted message
        original_message = super().format(record)

        # 设置时间打印为绿色
        timestamp = self.formatTime(record, self.datefmt) + f".{int(record.msecs):03d}"
        colored_timestamp = f"{self.COLORS['GREEN']}{timestamp}{self.COLORS['RESET']}"
        # Apply bold style to levelname
        bold_levelname = f"{self.COLORS['BOLD']}{record.levelname}{self.COLORS['RESET']}"
        # 设置name为黄色
        yellow_name = f"{self.COLORS['YELLOW']}{record.name}{self.COLORS['RESET']}"
        
        # Replace the levelname in the original message
        formatted_message = original_message.replace(record.levelname, bold_levelname)
        formatted_message = formatted_message.replace(timestamp, colored_timestamp)
        formatted_message = formatted_message.replace(record.name, yellow_name)

        return formatted_message


def setup_logger():

    config = {
        "version": 1,
        "disable_existing_loggers": False,

        "formatters": {
            "default": {
                "format": '%(asctime)s.%(msecs)03d | %(levelname)-7s  | %(name)s - %(message)s',
                "datefmt": '%Y-%m-%d %H:%M:%S'
            },
            "access": {
                "format": '%(asctime)s.%(msecs)03d | %(levelname)-7s  | %(name)s - %(message)s',
                "datefmt": '%Y-%m-%d %H:%M:%S'
            },
            "custom": {
                '()': MyFormatter,
                "format": '%(asctime)s.%(msecs)03d | %(levelname)-7s  | %(name)s - %(message)s',
                "datefmt": '%Y-%m-%d %H:%M:%S'
            }
        },
        "handlers": {
            "console": {
                "class": "logging.StreamHandler",
                "formatter": "custom",
                "level": "INFO",
                "stream": "ext://sys.stdout"
            }
        },

        "loggers": {
            "uvicorn": {
                "handlers": ["console"],
                "level": "INFO",
                "propagate": False
            },
            "uvicorn.access": {
                "handlers": ["console"],
                "level": "INFO",
                "propagate": False
            }
        },
        "root": {
            "handlers": ["console"],
            "level": "INFO"
        }
    }

    # Apply logging configuration
    logging.config.dictConfig(config)

Top  ---  Bottom

未完待续……

posted @ 2021-03-06 09:10  巴蜀秀才  阅读(201)  评论(0编辑  收藏  举报