如何在Python中构建用于日志记录的模块?

问题描述 投票:0回答:1

我想用Python编写一个库(模块)用于日志记录。每个进程的记录器实例应该是唯一的,并且每个进程都是全局的。 (这意味着它不应该作为参数传递给不同的函数。)

我在尝试设计一些合理的东西时遇到了一些心理障碍。

让我尝试解释一下我的方法。

我从一个流程开始。我在这个过程中添加了一些日志代码。

# process_1.py

from datetime import datetime
from datetime import timezone

import logging
import sys

logger_process_name = 'process_1'
logger_file_datetime = datetime.now(timezone.utc).date()

logger = logging.getLogger(__name__)

stdout_log_formatter = logging.Formatter('%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d | %(message)s')

stdout_log_handler = logging.StreamHandler(stream=sys.stdout)
stdout_log_handler.setLevel(logging.INFO)
stdout_log_handler.setFormatter(stdout_log_formatter)

file_log_formatter = logging.Formatter('%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d | %(message)s')

file_log_handler = logging.FileHandler(filename=f'{logger_process_name}_{logger_file_datetime}.log')
file_log_handler.setLevel(logging.DEBUG)
file_log_handler.setFormatter(file_log_formatter)

logger.setLevel(logging.DEBUG)
logger.addHandler(stdout_log_handler)
logger.addHandler(file_log_handler)

def main():
    logger.info('hello!')
    logger.info('goodbye!')

if __name__ == '__main__':
    main()

如您所见,其中大部分只是样板代码,用于初始化

logger
对象的实例,该对象写入
stdout
和文件,其文件名标记有当前日期。 (在 UTC 时区。)

然后我写了第二个过程。

# process_2.py

from datetime import datetime
# ...
# ... skip copy & paste of same lines above relating to the logger ...
# ...
logger.addHandler(file_log_handler)

def main():
    logger.info('hello from process_2!')
    logger.info('goodbye from process_2!')

if __name__ == '__main__':
    main()

啊 - 重复的代码。也许是模块的理想候选者?

我们来试着写一下吧。

# lib_logger.py

from datetime import datetime
from datetime import timezone

import logging
import sys

logger_process_name = 'process_1' # <--- !!! Wrong name !!!
logger_file_datetime = datetime.now(timezone.utc).date()

logger = logging.getLogger(__name__)

stdout_log_formatter = logging.Formatter('%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d | %(message)s')

stdout_log_handler = logging.StreamHandler(stream=sys.stdout)
stdout_log_handler.setLevel(logging.INFO)
stdout_log_handler.setFormatter(stdout_log_formatter)

file_log_formatter = logging.Formatter('%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d | %(message)s')

file_log_handler = logging.FileHandler(filename=f'{logger_process_name}_{logger_file_datetime}.log')
file_log_handler.setLevel(logging.DEBUG)
file_log_handler.setFormatter(file_log_formatter)

logger.setLevel(logging.DEBUG)
logger.addHandler(stdout_log_handler)
logger.addHandler(file_log_handler)

请注意,如果我们想使用

process_2.py
中的进程名称,则进程名称是错误的。

# process_2.py

from lib_logger import logger

def main():
    logger.info('hello from process_2!') # <--- the wrong process name is printed here
    logger.info('goodbye from process_2!') # <- and here
    # it writes to the file `process_1_2024-08-22.log` not
    # `process_2_2024-08-22.log` as it should

if __name__ == '__main__':
    main()

此时我陷入了困境,并没有想出一个好的解决方案。

最初我认为如何解决这个问题是相当明显的。向记录器模块添加一个函数来初始化并返回记录器实例,并添加另一个函数来注册名称。

这是我尝试实现这一点的方法:

# lib_logger.py

logger_process_name = None
logger = None

def set_logger_process_name(process_name: str) -> None:
    global logger_process_name
    logger_process_name = process_name

def get_logger() -> Logger:
    global logger

    if logger is not None:
        return logger

    if logger_process_name is None:
        raise RuntimeError(f'process name not set, cannot create logger instance')

    logger = logging.getLogger(__name__)

    stdout_log_formatter = # ...

    # skip some lines

    file_log_handler = logging.FileHandler(filename=f'{logger_process_name}_{logger_file_datetime}.log')
    file_log_handler.setLevel(logging.DEBUG)
    file_log_handler.setFormatter(file_log_formatter)

    logger.setLevel(logging.DEBUG)
    logger.addHandler(stdout_log_handler)
    logger.addHandler(file_log_handler)
    
    return logger

然而,从一个进程和其他模块实际使用它是一场灾难。导入语句的顺序突然变得很重要。

# process_1.py

from lib_logger import get_logger
from lib_logger import set_logger_process_name

# might defer process name definitions to another module
from lib_process_names import PROCESS_NAME_PROCESS_1

set_logger_process_name(PROCESS_NAME_PROCESS_1)
log = get_logger()

# must initialize the module, or at least call `set_logger_process_name`
# before initializing any further modules which use the logger module
# (call `get_logger`)
# Why? Because cannot call `get_logger` before `set_logger_process_name`
# has been called
from example_module import example_function

只是为了展示

example_function
可能会做什么:

# example_module.py

from lib_logger import get_logger()

log = get_logger()

def example_function():
    log.info('started work in example_function...')

我们实际上可以通过让所有 functions 在开始运行之前调用

log = get_logger()
来解决这个问题,但这不是一个好的解决方案。
log
句柄应该在模块内全局可访问,每个函数不应该负责获取记录器的句柄。与将
logger
实例作为函数参数传递相比,这并没有多大改进。

python logging design-patterns module software-design
1个回答
0
投票

要在 Python 中设计一个日志记录模块来确保每个进程的记录器都是唯一的,您可以实现一个延迟初始化记录器的解决方案,并使用模块级变量来确保每个进程只初始化一次。

为此,您需要

  1. 创建一个单例记录器
  2. 使用工厂函数
  3. 处理初始化顺序

这是一些实现代码,可能需要一些调整:

# lib_logger.py

import logging
import sys
from datetime import datetime, timezone

# Module-level variable to hold the logger instance
_logger = None

def get_logger(process_name: str) -> logging.Logger:
    """
    Get the logger instance, creating it if necessary.
    
    :param process_name: Name of the process (used in log filenames)
    :return: The logger instance
    """
    global _logger
    
    if _logger is not None:
        return _logger
    
    logger_file_datetime = datetime.now(timezone.utc).date()
    
    _logger = logging.getLogger(process_name)
    
    stdout_log_formatter = logging.Formatter(
        '%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d | %(message)s'
    )
    
    stdout_log_handler = logging.StreamHandler(stream=sys.stdout)
    stdout_log_handler.setLevel(logging.INFO)
    stdout_log_handler.setFormatter(stdout_log_formatter)
    
    file_log_formatter = logging.Formatter(
        '%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d | %(message)s'
    )
    
    file_log_handler = logging.FileHandler(filename=f'{process_name}_{logger_file_datetime}.log')
    file_log_handler.setLevel(logging.DEBUG)
    file_log_handler.setFormatter(file_log_formatter)
    
    _logger.setLevel(logging.DEBUG)
    _logger.addHandler(stdout_log_handler)
    _logger.addHandler(file_log_handler)
    
    return _logger

现在,在每个进程中,您只需使用相应的进程名称调用

get_logger
函数即可:

# process_1.py

from lib_logger import get_logger

def main():
    logger = get_logger("process_1")
    logger.info('hello from process_1!')
    logger.info('goodbye from process_1!')

if __name__ == '__main__':
    main()

这是第二个使用示例流程:

# process_2.py

from lib_logger import get_logger

def main():
    logger = get_logger("process_2")
    logger.info('hello from process_2!')
    logger.info('goodbye from process_2!')

if __name__ == '__main__':
    main()

阐释

记录器实例存储在模块级变量

_logger
中。
get_logger
函数检查此变量是否为
None
。如果是,则初始化记录器;如果没有,则返回现有的记录器实例。

进程名称作为参数传递给

get_logger

因为记录器仅在调用

get_logger
时才初始化,因此导入顺序并不重要。您可以在多个模块中安全地导入
get_logger

这一切都应确保记录器进程仅初始化一次,并且不需要传递实例。

© www.soinside.com 2019 - 2024. All rights reserved.