diff --git a/app/runtime/log.py b/app/runtime/log.py index a6349362c..57ff28f88 100644 --- a/app/runtime/log.py +++ b/app/runtime/log.py @@ -384,6 +384,8 @@ class LoggerManager: """ 识别日志调用文件和插件来源。 + 虚拟实例共享物理源码,因此优先使用实例专属模块命名空间路由日志; + 无模块身份的旧插件仍按调用文件路径识别。 插件调用宿主公共方法时,调用栈中仍保留插件帧,因此日志继续进入该插件 的独立文件,而不是混入主程序日志。 """ @@ -400,6 +402,16 @@ class LoggerManager: parts = filepath.parts if not caller_name: caller_name = parts[-2] if parts[-1] == "__init__.py" and len(parts) >= 2 else parts[-1] + module_name = frame.f_globals.get("__name__") + if isinstance(module_name, str): + module_parts = module_name.split(".") + if ( + len(module_parts) >= 3 + and module_parts[:2] == ["app", "plugins"] + and module_parts[2] + ): + plugin_name = module_parts[2] + break if "app" in parts: if not plugin_name and "plugins" in parts: try: diff --git a/tests/test_log_plugin_routing.py b/tests/test_log_plugin_routing.py new file mode 100644 index 000000000..a7194fdb6 --- /dev/null +++ b/tests/test_log_plugin_routing.py @@ -0,0 +1,56 @@ +"""插件日志文件路由测试。""" + +from pathlib import Path +from types import SimpleNamespace + +from app.runtime.log import LoggerManager, logger + + +class CapturingLogWriter: + """记录日志写入目标,避免测试访问真实文件系统。""" + + def __init__(self) -> None: + self.entries: list[tuple[str, str, Path]] = [] + + def write_log(self, level: str, message: str, file_path: Path) -> None: + """保存单条日志的级别、内容和目标路径。""" + self.entries.append((level, message, file_path)) + + @staticmethod + def shutdown() -> bool: + """测试写入器没有待释放资源。""" + return True + + +def test_virtual_plugin_routes_log_by_runtime_module_identity(monkeypatch, tmp_path): + """共享物理源码的虚拟实例应按运行实例 ID 写入独立日志文件。""" + writer = CapturingLogWriter() + monkeypatch.setattr(LoggerManager, "_writer", writer) + monkeypatch.setattr(LoggerManager, "_log_path", tmp_path) + monkeypatch.setattr( + LoggerManager, + "_get_console_logger", + classmethod( + lambda _cls, _logfile: SimpleNamespace(info=lambda *_args, **_kwargs: None) + ), + ) + namespace = { + "__name__": "app.plugins.mediawarp1", + "logger": logger, + } + source = compile( + "def emit_log():\n logger.info('virtual instance started')\n", + "/config/app/plugins/mediawarp/__init__.py", + "exec", + ) + exec(source, namespace) + + namespace["emit_log"]() + + assert writer.entries == [ + ( + "INFO", + "mediawarp - virtual instance started", + tmp_path / "plugins" / "mediawarp1.log", + ) + ]