如何为每个Logger实例修改名称以追踪微服务模块日志
问题描述
我创建了一个全局日志类,打算用于所有微服务函数中。当前日志输出示例:[2023-01-01 13:46:26] - INFO - [utils.logger.<module>:5] - testaaaaa
由于Logger定义在utils.logger模块中,日志里的%(name)s固定显示该路径。我希望将其改为Logger实例化所在模块的点式结构(如routes.users.functiona、routes.users.functionb),以此明确日志来源。同时不清楚logging如何通过__name__捕获模块路径,也可以修改日志类使其初始化时支持传入name参数。另外需要优化该日志类的健壮性,原代码如下:
import typing import logging class GlobalLogger: MINIMUM_GLOBAL_LEVEL = logging.DEBUG GLOBAL_HANDLER = logging.StreamHandler() LOG_FORMAT = "[%(asctime)s] - %(levelname)s - [%(name)s.%(funcName)s:%(lineno)d] - %(message)s" LOG_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" def __init__(self, level: typing.Union[int, str] = MINIMUM_GLOBAL_LEVEL): self.level = level self.logger = self._get_logger() self.log_format = self._log_formatter() self.GLOBAL_HANDLER.setFormatter(self.log_format) def _get_logger(self): logger = logging.getLogger(__name__) logger.setLevel(self.level) logger.addHandler(self.GLOBAL_HANDLER) return logger def _log_formatter(self): return logging.Formatter(fmt=self.LOG_FORMAT, datefmt=self.LOG_DATETIME_FORMAT)
核心解决方案:让日志名称跟随调用模块
1. 理解__name__的作用
Python中每个模块的__name__变量会自动被设置为模块的点式路径:
- 若在
routes/users.py中执行代码,__name__的值为routes.users - 若直接运行某个模块,
__name__为__main__ logging.getLogger(name)会创建/复用对应名称的Logger实例,日志格式中的%(name)s就会填充这个名称,从而标识日志来源模块。
2. 修改日志类支持动态名称
有两种实现方式,按需选择:
方式一:实例化时手动传入__name__
修改类的__init__和_get_logger方法,支持传入name参数:
import typing import logging class GlobalLogger: MINIMUM_GLOBAL_LEVEL = logging.DEBUG GLOBAL_HANDLER = logging.StreamHandler() LOG_FORMAT = "[%(asctime)s] - %(levelname)s - [%(name)s.%(funcName)s:%(lineno)d] - %(message)s" LOG_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" # 新增name参数,默认值设为__name__(可选,若不传则用当前类所在模块名) def __init__(self, level: typing.Union[int, str] = MINIMUM_GLOBAL_LEVEL, name: str = __name__): self.level = level self.name = name self.logger = self._get_logger() self.log_format = self._log_formatter() self.GLOBAL_HANDLER.setFormatter(self.log_format) def _get_logger(self): # 使用传入的name创建Logger logger = logging.getLogger(self.name) logger.setLevel(self.level) # 避免重复添加Handler(关键优化点) if not logger.handlers: logger.addHandler(self.GLOBAL_HANDLER) return logger def _log_formatter(self): return logging.Formatter(fmt=self.LOG_FORMAT, datefmt=self.LOG_DATETIME_FORMAT)
使用方式:在其他模块中实例化时传入当前模块的__name__:
# 在routes/users/functiona.py中 logger = GlobalLogger(name=__name__) logger.logger.info("test log")
此时日志输出的%(name)s会是routes.users.functiona。
方式二:自动获取调用模块的名称
通过inspect模块自动获取实例化代码所在的模块名,无需手动传参:
import typing import logging import inspect class GlobalLogger: MINIMUM_GLOBAL_LEVEL = logging.DEBUG GLOBAL_HANDLER = logging.StreamHandler() LOG_FORMAT = "[%(asctime)s] - %(levelname)s - [%(name)s.%(funcName)s:%(lineno)d] - %(message)s" LOG_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" def __init__(self, level: typing.Union[int, str] = MINIMUM_GLOBAL_LEVEL): self.level = level # 自动获取调用模块的名称 self.name = self._get_caller_module_name() self.logger = self._get_logger() self.log_format = self._log_formatter() self.GLOBAL_HANDLER.setFormatter(self.log_format) def _get_caller_module_name(self): # 获取调用栈,跳过当前类的初始化栈帧,拿到调用者的模块名 stack = inspect.stack() try: # stack[0]是当前方法,stack[1]是__init__,stack[2]是实例化代码所在的栈帧 caller_frame = stack[2] return caller_frame.f_globals['__name__'] finally: del stack # 释放栈引用,避免内存泄漏 def _get_logger(self): logger = logging.getLogger(self.name) logger.setLevel(self.level) if not logger.handlers: logger.addHandler(self.GLOBAL_HANDLER) return logger def _log_formatter(self): return logging.Formatter(fmt=self.LOG_FORMAT, datefmt=self.LOG_DATETIME_FORMAT)
使用方式:直接实例化即可,无需传参:
# 在routes/users/functionb.py中 logger = GlobalLogger() logger.logger.info("auto caller log")
健壮性优化建议
避免重复添加Handler
原代码中每次实例化都会调用logger.addHandler(self.GLOBAL_HANDLER),而logging.getLogger是单例模式,多次实例化会导致同一个Logger被添加多个相同Handler,最终日志重复输出。解决方案是添加判断:if not logger.handlers:再添加Handler。全局Handler的线程安全与隔离
原代码中的GLOBAL_HANDLER是类级别的全局变量,若多个线程同时修改其Formatter(或其他属性)会出现并发问题。建议:- 若所有实例使用相同格式,可在类加载时一次性设置Formatter,无需在
__init__中重复设置; - 若需要支持不同格式,可改为每个实例创建独立的Handler,或使用线程锁保护全局Handler的修改。
- 若所有实例使用相同格式,可在类加载时一次性设置Formatter,无需在
日志级别输入合法性校验
若传入的level是字符串(如"INFO"),可统一转换为logging对应的级别常量,避免无效输入:def __init__(self, level: typing.Union[int, str] = MINIMUM_GLOBAL_LEVEL, name: str = __name__): # 处理字符串级别 if isinstance(level, str): self.level = logging.getLevelName(level.upper()) else: self.level = level # 确保级别在合法范围内 valid_levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL] if not isinstance(self.level, int) or self.level not in valid_levels: self.level = self.MINIMUM_GLOBAL_LEVEL # 其他初始化代码...添加日志方法封装
原代码需要通过logger.logger.info()调用日志方法,不够便捷。可以在GlobalLogger类中封装常用日志方法:def debug(self, msg: str, *args, **kwargs): self.logger.debug(msg, *args, **kwargs) def info(self, msg: str, *args, **kwargs): self.logger.info(msg, *args, **kwargs) def warning(self, msg: str, *args, **kwargs): self.logger.warning(msg, *args, **kwargs) def error(self, msg: str, *args, **kwargs): self.logger.error(msg, *args, **kwargs) def critical(self, msg: str, *args, **kwargs): self.logger.critical(msg, *args, **kwargs)使用时直接调用:
logger.info("test")。资源清理
对于文件Handler等需要关闭的资源,可添加__del__方法或显式关闭方法,但StreamHandler通常无需手动关闭。
内容的提问来源于stack exchange,提问作者dataviews

