我想用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 中设计一个日志记录模块来确保每个进程的记录器都是唯一的,您可以实现一个延迟初始化记录器的解决方案,并使用模块级变量来确保每个进程只初始化一次。
为此,您需要
这是一些实现代码,可能需要一些调整:
# 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
。
这一切都应确保记录器进程仅初始化一次,并且不需要传递实例。