ML 系统的日志记录

2022年10月

Posted by franztao on October 10, 2022

Intuition

日志记录是跟踪和记录应用程序中发生的关键事件的过程,用于检查、调试等。它们比print语句更强大,因为它们允许将特定的信息片段发送到具有自定义功能的特定位置格式化、共享接口等。这使得日志记录成为能够从应用程序的内部流程中发现有洞察力的信息的关键支持者。

成分

有几个总体概念需要注意:

  • Logger: 从应用程序发出日志消息。
  • Handler:将日志记录发送到特定位置。
  • Formatter: 格式化和样式化日志记录。

日志记录还有很多,例如过滤器、异常日志记录等,但这些基础知识将使能够为应用程序做需要的一切。

级别

在创建专门的配置记录器之前,让看看使用基本配置记录的消息是什么样子的。

import logging
import sys

# Create super basic logger
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)

# Logging levels (from lowest to highest priority)
logging.debug("Used for debugging your code.")
logging.info("Informative messages from your code.")
logging.warning("Everything works but there is something to be aware of.")
logging.error("There's been a mistake with the process.")
logging.critical("There is something terribly wrong and process may terminate.")
DEBUG:root:Used for debugging your code.
INFO:root:Informative messages from your code.
WARNING:root:Everything works but there is something to be aware of.
ERROR:root:There's been a mistake with the process.
CRITICAL:root:There is something terribly wrong and process may terminate.

这些是日志记录的基本级别,其中DEBUG优先级最低,优先级CRITICAL最高。定义了记录器,basicConfig用于将日志消息发送到标准输出(即终端控制台),但也可以写入任何其他流甚至文件。还将日志定义为对从 level 开始的日志消息敏感DEBUG。这意味着所有记录的消息都将显示,因为DEBUG它是最低级别。如果设置了 level ,ERROR那么只会显示日志消息。

import logging
import sys

# Create super basic logger
logging.basicConfig(stream=sys.stdout, level=logging.ERROR)

# Logging levels (from lowest to highest priority)
logging.debug("Used for debugging your code.")
logging.info("Informative messages from your code.")
logging.warning("Everything works but there is something to be aware of.")
logging.error("There's been a mistake with the process.")
logging.critical("There is something terribly wrong and process may terminate.")
ERROR:root:There's been a mistake with the process.
CRITICAL:root:There is something terribly wrong and process may terminate.

配置

首先,将在config.py脚本中设置日志的位置:

# config/config.py
LOGS_DIR = Path(BASE_DIR, "logs")
LOGS_DIR.mkdir(parents=True, exist_ok=True)

接下来,将为应用程序配置记录器:

# config/config.py
import logging
import sys
logging_config = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "minimal": {"format": "%(message)s"},
        "detailed": {
            "format": "%(levelname)s %(asctime)s [%(name)s:%(filename)s:%(funcName)s:%(lineno)d]\n%(message)s\n"
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "stream": sys.stdout,
            "formatter": "minimal",
            "level": logging.DEBUG,
        },
        "info": {
            "class": "logging.handlers.RotatingFileHandler",
            "filename": Path(LOGS_DIR, "info.log"),
            "maxBytes": 10485760,  # 1 MB
            "backupCount": 10,
            "formatter": "detailed",
            "level": logging.INFO,
        },
        "error": {
            "class": "logging.handlers.RotatingFileHandler",
            "filename": Path(LOGS_DIR, "error.log"),
            "maxBytes": 10485760,  # 1 MB
            "backupCount": 10,
            "formatter": "detailed",
            "level": logging.ERROR,
        },
    },
    "root": {
        "handlers": ["console", "info", "error"],
        "level": logging.INFO,
        "propagate": True,
    },
}
  1. [Lines 6-11]:定义两个不同的Formatters(确定日志消息的格式和样式),minimal 和 detailed,它们使用各种LogRecord 属性为日志消息创建格式化模板。
  2. [Lines 12-35]:定义不同的处理程序(有关发送日志消息的位置的详细信息):
    • console:将日志消息(使用minimal格式化程序)发送到stdout高于级别的消息流DEBUG(即所有记录的消息)。
    • info:将日志消息(使用detailed格式化程序)发送到logs/info.log(一个可以达到的文件,1 MB将备份10它的最新版本)以获得级别以上的消息INFO
    • error:将日志消息(使用detailed格式化程序)发送到logs/error.log(一个可以达到的文件,1 MB将备份10它的最新版本)以获得级别以上的消息ERROR
  3. [Lines 36-40]:将不同的处理程序附加到根Logger

选择使用字典来配置记录器,但还有其他方法,例如 Python 脚本、配置文件等。单击下面的不同选项以展开并查看各自的实现。

Python 脚本

import logging
from rich.logging import RichHandler
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
console_handler = RichHandler(markup=True)
console_handler.setLevel(logging.DEBUG)
info_handler = logging.handlers.RotatingFileHandler(
    filename=Path(LOGS_DIR, "info.log"),
    maxBytes=10485760,  # 1 MB
    backupCount=10,
)
info_handler.setLevel(logging.INFO)
error_handler = logging.handlers.RotatingFileHandler(
    filename=Path(LOGS_DIR, "error.log"),
    maxBytes=10485760,  # 1 MB
    backupCount=10,
)
error_handler.setLevel(logging.ERROR)

minimal_formatter = logging.Formatter(fmt="%(message)s")
detailed_formatter = logging.Formatter(
    fmt="%(levelname)s %(asctime)s [%(name)s:%(filename)s:%(funcName)s:%(lineno)d]\n%(message)s\n"
)

console_handler.setFormatter(fmt=minimal_formatter)
info_handler.setFormatter(fmt=detailed_formatter)
error_handler.setFormatter(fmt=detailed_formatter)
logger.addHandler(hdlr=console_handler)
logger.addHandler(hdlr=info_handler)
logger.addHandler(hdlr=error_handler)
import logging
from rich.logging import RichHandler

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

console_handler = RichHandler(markup=True)
console_handler.setLevel(logging.DEBUG)
info_handler = logging.handlers.RotatingFileHandler(
    filename=Path(LOGS_DIR, "info.log"),
    maxBytes=10485760,  # 1 MB
    backupCount=10,
)
info_handler.setLevel(logging.INFO)
error_handler = logging.handlers.RotatingFileHandler(
    filename=Path(LOGS_DIR, "error.log"),
    maxBytes=10485760,  # 1 MB
    backupCount=10,
)
error_handler.setLevel(logging.ERROR)

minimal_formatter = logging.Formatter(fmt="%(message)s")
detailed_formatter = logging.Formatter(
    fmt="%(levelname)s %(asctime)s [%(name)s:%(filename)s:%(funcName)s:%(lineno)d]\n%(message)s\n"
)

console_handler.setFormatter(fmt=minimal_formatter)
info_handler.setFormatter(fmt=detailed_formatter)
error_handler.setFormatter(fmt=detailed_formatter)
logger.addHandler(hdlr=console_handler)
logger.addHandler(hdlr=info_handler)
logger.addHandler(hdlr=error_handler)
  1. 把它放在一个logging.config文件中:

    [formatters]
    keys=minimal,detailed
       
    [formatter_minimal]
    format=%(message)s
       
    [formatter_detailed]
    format=
        %(levelname)s %(asctime)s [%(name)s:%(filename)s:%(funcName)s:%(lineno)d]
        %(message)s
       
    [handlers]
    keys=console,info,error
       
    [handler_console]
    class=StreamHandler
    level=DEBUG
    formatter=minimal
    args=(sys.stdout,)
       
    [handler_info]
    class=handlers.RotatingFileHandler
    level=INFO
    formatter=detailed
    backupCount=10
    maxBytes=10485760
    args=("logs/info.log",)
       
    [handler_error]
    class=handlers.RotatingFileHandler
    level=ERROR
    formatter=detailed
    backupCount=10
    maxBytes=10485760
    args=("logs/error.log",)
       
    [loggers]
    keys=root
       
    [logger_root]
    level=INFO
    handlers=console,info,error
    
  2. 把它放在你的 Python 脚本中

    import logging
    import logging.config
    from rich.logging import RichHandler
       
    # Use config file to initialize logger
    logging.config.fileConfig(Path(CONFIG_DIR, "logging.config"))
    logger = logging.getLogger()
    logger.handlers[0] = RichHandler(markup=True)  # set rich handler
    

可以像这样加载记录器配置字典:

# config/config.py
from rich.logging import RichHandler
logging.config.dictConfig(logging_config)
logger = logging.getLogger()
logger.handlers[0] = RichHandler(markup=True)  # pretty formatting

# Sample messages (note that we use configured `logger` now)
logger.debug("Used for debugging your code.")
logger.info("Informative messages from your code.")
logger.warning("Everything works but there is something to be aware of.")
logger.error("There's been a mistake with the process.")
logger.critical("There is something terribly wrong and process may terminate.")
DEBUG    Used for debugging your code.                                 config.py:71
INFO     Informative messages from your code.                          config.py:72
WARNING  Everything works but there is something to be aware of.       config.py:73
ERROR    There's been a mistake with the process.                      config.py:74
CRITICAL There is something terribly wrong and process may terminate.  config.py:75

使用RichHandler作为console处理程序来为日志消息获取漂亮的格式。这不是预安装的库,因此需要安装并添加到requirements.txt

pip install rich==12.4.4

# Add to requirements.txt rich==12.4.4

记录的消息存储在日志目录中的相应文件中:

logs/
    ├── info.log
    └── error.log

由于定义了详细的格式化程序,因此会看到如下信息丰富的日志消息:

应用

在项目中,可以将所有打印语句替换为日志语句:

print("✅ Saved data!")

──── 变成:────

from config.config import logger
logger.info("✅ Saved data!")

所有的日志消息都在INFO级别上,但在开发过程中可能不得不使用级别,如果系统以意外的方式运行DEBUG,还会添加一些ERROR或日志消息。CRITICAL

  • what:记录您希望从应用程序中显示的所有必要详细信息,这些详细信息将在开发期间之后的回顾性检查中有用。

  • 其中:最佳做法是不要将模块化函数与日志语句混为一谈。相反,应该在小函数之外和更大的工作流程中记录消息。例如,除了main.pytrain.py文件之外,任何脚本中都没有日志消息。这是因为这些脚本使用了其他脚本(data.py、evaluate.py 等)中定义的较小函数。如果觉得需要在其他功能中登录,那么它通常表明该功能需要进一步分解。

Elastic stack(以前称为 ELK stack)是生产级日志记录的常用选项。它结合了Elasticsearch(分布式搜索引擎)、Logstash(摄取管道)和Kibana(可定制可视化)的特性。也可以简单地将日志上传到云博客存储(例如 S3、谷歌云存储等)。


更多干货,第一时间更新在以下微信公众号:

您的一点点支持,是我后续更多的创造和贡献

转载到请包括本文地址 更详细的转载事宜请参考文章如何转载/引用

本文主体源自以下链接:

@article{madewithml,
    author       = {Goku Mohandas},
    title        = { Made With ML },
    howpublished = {\url{https://madewithml.com/}},
    year         = {2022}
}