设置一个 class 级别的记录器

set up a class-level logger

我可以轻松配置 global 记录器的属性:

logging.basicConfig(
    level=logging.INFO,
    format="[%(asctime)s] [%(levelname)s]: %(message)s",
    datefmt="%d/%m/%Y ( %H:%M:%S )",
    stream=sys.stdout
)

如何在 class-level 实现同等的东西? (下面的代码 没有 工作)

import logging
class SomeClass:
    def __init__(self) -> None:
        self.logger = logging.getLogger(__name__)
        self.logger.dictConfig({
            "level": logging.INFO,
            "format": "[%(asctime)s] [%(levelname)s]: %(message)s",
            "datefmt": "%d/%m/%Y ( %H:%M:%S )",
            "stream": sys.stdout
        })
    def foo(self) -> None:
        self.logger.info("foooo ...")

c = SomeClass()
c.foo()

这是我得到的错误:

$ python logger.py 2>&1 | grep "Error"
AttributeError: 'Logger' object has no attribute 'dictConfig'

编辑: 我寻找一个 single 初始化命令,而不是像:

self.logger.setLevel(...)
self.logger.setFormatter(...)

我正在寻找比这更好的解决方案:

import sys
import logging
class SomeClass:
    def __init__(self) -> None:
        self.logger = logging.getLogger(__name__)
        H = logging.StreamHandler(sys.stdout)
        H.setLevel(logging.INFO)
        H.setFormatter(
            logging.Formatter(
                fmt="[%(asctime)s] %(levelname)s: %(message)s",
                datefmt="%d/%m/%Y ( %H:%M:%S )"
            ))
        self.logger.addHandler(H)
    def foo(self) -> None:
        self.logger.warning("foooo ...")

c = SomeClass()
c.foo()

首先,如果您希望它成为 class-level 记录器,我会将记录器定义为 class 属性。其次,当您调用 logging.getLogger 而不是 __name__ 时,我会使用记录器名称,而是使用 class 独有的名称。由于您可以在不同的模块中重复使用相同的 class 名称,因此我将使用 __name__ 和 class 名称的组合。为了演示这一点,下面的第二个演示有两个 class SomeClass 实例,一个在脚本文件中,一个在名为 workers 的模块中。这些 classes 将实例化记录器,其唯一区别是记录消息的格式。但首先:

在同一脚本文件中包含多个日志记录的示例 Class

import sys
import logging

class SomeClass1:
    logger = logging.getLogger(__name__ + '.SomeClass1')
    H = logging.StreamHandler(sys.stdout)
    H.setLevel(logging.INFO)
    H.setFormatter(
        logging.Formatter(
            fmt="SomeClass1: [%(asctime)s] %(levelname)s: %(message)s",
            datefmt="%d/%m/%Y ( %H:%M:%S )"
        ))
    logger.addHandler(H)


    def foo(self) -> None:
        self.logger.warning("foooo ...")

class SomeClass2:
    logger = logging.getLogger(__name__ + '.SomeClass2')
    H = logging.StreamHandler(sys.stdout)
    H.setLevel(logging.INFO)
    H.setFormatter(
        logging.Formatter(
            fmt="SomeClass2: [%(asctime)s] %(levelname)s: %(message)s",
            datefmt="%d/%m/%Y ( %H:%M:%S )"
        ))
    logger.addHandler(H)


    def bar(self) -> None:
        self.logger.warning("bar ...")

c1 = SomeClass1()
c1.foo()
c2 = SomeClass2()
c2.bar()

打印:

SomeClass1: [30/05/2022 ( 09:14:06 )] WARNING: foooo ...
SomeClass2: [30/05/2022 ( 09:14:06 )] WARNING: bar ...

在不同模块中具有相同 Class 名称的示例

workers.py

import sys
import logging

class SomeClass:
    logger = logging.getLogger(__name__ + '.SomeClass')
    H = logging.StreamHandler(sys.stdout)
    H.setLevel(logging.INFO)
    H.setFormatter(
        logging.Formatter(
            fmt="workers module: [%(asctime)s] %(levelname)s: %(message)s",
            datefmt="%d/%m/%Y ( %H:%M:%S )"
        ))
    logger.addHandler(H)


    def foo(self) -> None:
        self.logger.warning("foooo ...")

script.py

import sys
import logging
import workers

class SomeClass:
    logger = logging.getLogger(__name__ + '.SomeClass')
    H = logging.StreamHandler(sys.stdout)
    H.setLevel(logging.INFO)
    H.setFormatter(
        logging.Formatter(
            fmt="Script File: [%(asctime)s] %(levelname)s: %(message)s",
            datefmt="%d/%m/%Y ( %H:%M:%S )"
        ))
    logger.addHandler(H)


    def foo(self) -> None:
        self.logger.warning("foooo ...")

c1a = SomeClass()
c1b = SomeClass()
c1a.foo()
c1b.foo()
c2 = workers.SomeClass()
c2.foo()

打印:

Script File: [30/05/2022 ( 09:23:02 )] WARNING: foooo ...
Script File: [30/05/2022 ( 09:23:02 )] WARNING: foooo ...
workers module: [30/05/2022 ( 09:23:02 )] WARNING: foooo ...

您可能不喜欢这个答案,但在 Python 中,class-level 记录器并没有真正意义 - 不像 Java 和 C#,其中 class 是软件分解的单位,在Python中模块就是那个单位。因此,__name__ 给出了 模块的名称 而不是其中定义的任何特定 class。

此外,日志记录配置(关于处理程序、格式化程序、过滤器等)是在 应用程序 级别而不是库级别完成的,因此它应该只真正完成一次在 __name__ == '__main__' 条件下,而不是随机 classes.

如果您确实需要比模块级别更精细的日志记录,请为您的记录器使用 __name__ + 'SomeClass' 等记录器名称。

日志记录文档列出了许多 anti-patterns 与最佳实践相反的内容。

通常,除了单个 getLogger 调用之外,您不应该费心处理 setFormatter、setLevel 和诸如此类的方法系列,也不应该管理记录器实例的生命周期。如果您需要超越 logging.basciConfig 中的可能性,请使用 logging.config 模块!

鉴于您的 SomeClass 存在于一个模块中,因此其导入路径(因此 __name__ 变量的值)为 some.project.some.module,在您的应用程序启动期间的某处您应该配置所有

等日志记录工具
import logging.config

logging.config.dictConfig({
    "version": 1,
    "formatters": {
        "default": {
            "class": "logging.Formatter",
            "format": "[%(asctime)s] [%(levelname)s]: %(message)s",
            "datefmt": "%d/%m/%Y ( %H:%M:%S )",
        },
    },
    "handlers": {
        "stdout": {
            "formatter": "default",
            "class": "logging.StreamHandler",
            "stream": sys.stdout,
        },
        "null": {
            "class": "logging.NullHandler",
        }
    },
    "loggers": {
        "some.project.some.module": {
            "level": "INFO",
            "propagate": True,
            "handlers": ["null"],
        },
    },
    "root": {
        "handlers": ["stdout"],
        "level": "INFO",
    },
})

如果只将最顶层的根记录器附加到实际写入 file/stdout/whatever 的处理程序,我就更容易思考日志记录模块。通过这种方式,其他记录器仅作为调整每个模块的日志记录级别的一种方式,并可能注入特殊的错误处理程序。

查看 更详细的答案,了解为什么记录器对您的 class 不是特殊的,而是它的模块。