mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
feat: observe legacy module contract hits
This commit is contained in:
@@ -13,6 +13,7 @@ from app.runtime.observability import observe_duration, record_metric
|
|||||||
from app.runtime.extensions.module.contracts import (
|
from app.runtime.extensions.module.contracts import (
|
||||||
diagnose_module_callable,
|
diagnose_module_callable,
|
||||||
get_module_method_contract,
|
get_module_method_contract,
|
||||||
|
is_explicit_module_method,
|
||||||
)
|
)
|
||||||
from app.schemas.exception import RateLimitExceededException
|
from app.schemas.exception import RateLimitExceededException
|
||||||
|
|
||||||
@@ -124,6 +125,11 @@ class ModuleInvocationDispatcher:
|
|||||||
func = module_dict.get(method)
|
func = module_dict.get(method)
|
||||||
if not func:
|
if not func:
|
||||||
continue
|
continue
|
||||||
|
self._record_legacy_hit(
|
||||||
|
method,
|
||||||
|
caller_type="plugin",
|
||||||
|
abi_source="third_party_plugin",
|
||||||
|
)
|
||||||
self._diagnose_callable(method, func, f"插件 {plugin_id}")
|
self._diagnose_callable(method, func, f"插件 {plugin_id}")
|
||||||
logger.info("请求插件 %s 执行:%s ...", plugin_name, method)
|
logger.info("请求插件 %s 执行:%s ...", plugin_name, method)
|
||||||
if self.is_valid_empty(result):
|
if self.is_valid_empty(result):
|
||||||
@@ -172,6 +178,11 @@ class ModuleInvocationDispatcher:
|
|||||||
func = module_dict.get(method)
|
func = module_dict.get(method)
|
||||||
if not func:
|
if not func:
|
||||||
continue
|
continue
|
||||||
|
self._record_legacy_hit(
|
||||||
|
method,
|
||||||
|
caller_type="plugin",
|
||||||
|
abi_source="third_party_plugin",
|
||||||
|
)
|
||||||
self._diagnose_callable(method, func, f"插件 {plugin_id}")
|
self._diagnose_callable(method, func, f"插件 {plugin_id}")
|
||||||
logger.info("请求插件 %s 执行:%s ...", plugin_name, method)
|
logger.info("请求插件 %s 执行:%s ...", plugin_name, method)
|
||||||
if self.is_valid_empty(result):
|
if self.is_valid_empty(result):
|
||||||
@@ -219,6 +230,11 @@ class ModuleInvocationDispatcher:
|
|||||||
module_name = self._module_name(module, module_id)
|
module_name = self._module_name(module, module_id)
|
||||||
try:
|
try:
|
||||||
func = getattr(module, method)
|
func = getattr(module, method)
|
||||||
|
self._record_legacy_hit(
|
||||||
|
method,
|
||||||
|
caller_type="system",
|
||||||
|
abi_source="host_module",
|
||||||
|
)
|
||||||
self._diagnose_callable(method, func, f"宿主模块 {module_id}")
|
self._diagnose_callable(method, func, f"宿主模块 {module_id}")
|
||||||
if self.is_valid_empty(result):
|
if self.is_valid_empty(result):
|
||||||
result = func(*args, **kwargs)
|
result = func(*args, **kwargs)
|
||||||
@@ -267,6 +283,11 @@ class ModuleInvocationDispatcher:
|
|||||||
module_name = self._module_name(module, module_id)
|
module_name = self._module_name(module, module_id)
|
||||||
try:
|
try:
|
||||||
func = getattr(module, method)
|
func = getattr(module, method)
|
||||||
|
self._record_legacy_hit(
|
||||||
|
method,
|
||||||
|
caller_type="system",
|
||||||
|
abi_source="host_module",
|
||||||
|
)
|
||||||
self._diagnose_callable(method, func, f"宿主模块 {module_id}")
|
self._diagnose_callable(method, func, f"宿主模块 {module_id}")
|
||||||
if self.is_valid_empty(result):
|
if self.is_valid_empty(result):
|
||||||
result = await self._async_call(func, *args, **kwargs)
|
result = await self._async_call(func, *args, **kwargs)
|
||||||
@@ -307,6 +328,22 @@ class ModuleInvocationDispatcher:
|
|||||||
provider_type=provider_type,
|
provider_type=provider_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _record_legacy_hit(
|
||||||
|
method: str,
|
||||||
|
*,
|
||||||
|
caller_type: str,
|
||||||
|
abi_source: str,
|
||||||
|
) -> None:
|
||||||
|
"""记录未知动态方法的兼容命中,便于按真实调用逐项迁移。"""
|
||||||
|
if not is_explicit_module_method(method):
|
||||||
|
record_metric(
|
||||||
|
"module.contract.legacy_hit",
|
||||||
|
method=method,
|
||||||
|
caller_type=caller_type,
|
||||||
|
abi_source=abi_source,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _diagnose_callable(
|
def _diagnose_callable(
|
||||||
method: str,
|
method: str,
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ METRIC_SPECS = {
|
|||||||
MetricSpec("event.handler.duration", MetricKind.HISTOGRAM, frozenset({"event_type", "handler_type", "outcome"})),
|
MetricSpec("event.handler.duration", MetricKind.HISTOGRAM, frozenset({"event_type", "handler_type", "outcome"})),
|
||||||
MetricSpec("module.provider.duration", MetricKind.HISTOGRAM, frozenset({"method", "provider_type", "outcome"})),
|
MetricSpec("module.provider.duration", MetricKind.HISTOGRAM, frozenset({"method", "provider_type", "outcome"})),
|
||||||
MetricSpec("module.provider.timeout", MetricKind.COUNTER, frozenset({"method", "provider_type"})),
|
MetricSpec("module.provider.timeout", MetricKind.COUNTER, frozenset({"method", "provider_type"})),
|
||||||
|
MetricSpec(
|
||||||
|
"module.contract.legacy_hit",
|
||||||
|
MetricKind.COUNTER,
|
||||||
|
frozenset({"method", "caller_type", "abi_source"}),
|
||||||
|
),
|
||||||
MetricSpec("scheduler.job.duration", MetricKind.HISTOGRAM, frozenset({"owner", "outcome"})),
|
MetricSpec("scheduler.job.duration", MetricKind.HISTOGRAM, frozenset({"owner", "outcome"})),
|
||||||
MetricSpec("scheduler.job.overlap_skip", MetricKind.COUNTER, frozenset({"owner"})),
|
MetricSpec("scheduler.job.overlap_skip", MetricKind.COUNTER, frozenset({"owner"})),
|
||||||
MetricSpec("scheduler.job.retry", MetricKind.COUNTER, frozenset({"owner"})),
|
MetricSpec("scheduler.job.retry", MetricKind.COUNTER, frozenset({"owner"})),
|
||||||
|
|||||||
@@ -644,6 +644,8 @@ ModuleMethodSpec(
|
|||||||
- 契约清单现覆盖静态扫描到的 211 个宿主字符串调用,并保留一个暂未被宿主调用的 `send_message` 公开能力,
|
- 契约清单现覆盖静态扫描到的 211 个宿主字符串调用,并保留一个暂未被宿主调用的 `send_message` 公开能力,
|
||||||
共 212 个显式 V2 spec。原先仅按 prefix 分类或落入默认 legacy 的宿主方法均获得稳定 family、输入合同、
|
共 212 个显式 V2 spec。原先仅按 prefix 分类或落入默认 legacy 的宿主方法均获得稳定 family、输入合同、
|
||||||
结果合同、执行、超时和错误语义;未知第三方自定义方法仍走开放 legacy fallback,不拒绝加载或执行。
|
结果合同、执行、超时和错误语义;未知第三方自定义方法仍走开放 legacy fallback,不拒绝加载或执行。
|
||||||
|
- 未知动态方法在真实 provider 命中时记录 `module.contract.legacy_hit`,区分插件/宿主调用方和 ABI 来源;
|
||||||
|
该指标只在 callable 实际存在并准备执行时递增,不改变未知第三方方法的开放 fallback、聚合或异常语义。
|
||||||
|
|
||||||
#### ARCH-241:Event Contract Registry
|
#### ARCH-241:Event Contract Registry
|
||||||
|
|
||||||
|
|||||||
@@ -222,6 +222,71 @@ def test_plugin_non_mapping_module_decl_is_reported_and_skipped() -> None:
|
|||||||
plugin_error.assert_called_once()
|
plugin_error.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_plugin_method_records_legacy_abi_hit(monkeypatch) -> None:
|
||||||
|
"""未知第三方方法继续执行,同时记录可迁移的 legacy ABI 来源。"""
|
||||||
|
hits = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.runtime.extensions.module.dispatcher.record_metric",
|
||||||
|
lambda name, **labels: hits.append((name, labels)),
|
||||||
|
)
|
||||||
|
dispatcher, _, _, _ = _dispatcher(
|
||||||
|
plugins={("P1", "插件一"): {"third_party_custom": lambda: "ok"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert dispatcher.dispatch("third_party_custom") == "ok"
|
||||||
|
assert hits == [
|
||||||
|
(
|
||||||
|
"module.contract.legacy_hit",
|
||||||
|
{
|
||||||
|
"method": "third_party_custom",
|
||||||
|
"caller_type": "plugin",
|
||||||
|
"abi_source": "third_party_plugin",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_host_method_records_legacy_abi_hit(monkeypatch) -> None:
|
||||||
|
"""宿主临时新增而未登记的方法保持执行并留下迁移信号。"""
|
||||||
|
hits = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.runtime.extensions.module.dispatcher.record_metric",
|
||||||
|
lambda name, **labels: hits.append((name, labels)),
|
||||||
|
)
|
||||||
|
|
||||||
|
class LegacyModule:
|
||||||
|
"""提供未进入清单的宿主兼容方法。"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
"""返回测试模块名称。"""
|
||||||
|
return "旧模块"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_priority() -> int:
|
||||||
|
"""返回稳定测试优先级。"""
|
||||||
|
return 1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def third_party_host() -> str:
|
||||||
|
"""返回兼容方法结果。"""
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
dispatcher, _, _, _ = _dispatcher(modules=[LegacyModule()])
|
||||||
|
|
||||||
|
assert dispatcher.dispatch("third_party_host") == "ok"
|
||||||
|
assert hits == [
|
||||||
|
(
|
||||||
|
"module.contract.legacy_hit",
|
||||||
|
{
|
||||||
|
"method": "third_party_host",
|
||||||
|
"caller_type": "system",
|
||||||
|
"abi_source": "host_module",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_async_plugin_non_mapping_module_decl_is_reported_and_skipped() -> None:
|
async def test_async_plugin_non_mapping_module_decl_is_reported_and_skipped() -> None:
|
||||||
"""异步路径下坏插件同样被隔离,嵌套补丁场景不再冒泡击穿调度。"""
|
"""异步路径下坏插件同样被隔离,嵌套补丁场景不再冒泡击穿调度。"""
|
||||||
|
|||||||
Reference in New Issue
Block a user