feat: observe legacy module contract hits

This commit is contained in:
jxxghp
2026-08-23 00:11:15 +08:00
parent 3c8cb513bb
commit dc9433f0de
4 changed files with 109 additions and 0 deletions
@@ -13,6 +13,7 @@ from app.runtime.observability import observe_duration, record_metric
from app.runtime.extensions.module.contracts import (
diagnose_module_callable,
get_module_method_contract,
is_explicit_module_method,
)
from app.schemas.exception import RateLimitExceededException
@@ -124,6 +125,11 @@ class ModuleInvocationDispatcher:
func = module_dict.get(method)
if not func:
continue
self._record_legacy_hit(
method,
caller_type="plugin",
abi_source="third_party_plugin",
)
self._diagnose_callable(method, func, f"插件 {plugin_id}")
logger.info("请求插件 %s 执行:%s ...", plugin_name, method)
if self.is_valid_empty(result):
@@ -172,6 +178,11 @@ class ModuleInvocationDispatcher:
func = module_dict.get(method)
if not func:
continue
self._record_legacy_hit(
method,
caller_type="plugin",
abi_source="third_party_plugin",
)
self._diagnose_callable(method, func, f"插件 {plugin_id}")
logger.info("请求插件 %s 执行:%s ...", plugin_name, method)
if self.is_valid_empty(result):
@@ -219,6 +230,11 @@ class ModuleInvocationDispatcher:
module_name = self._module_name(module, module_id)
try:
func = getattr(module, method)
self._record_legacy_hit(
method,
caller_type="system",
abi_source="host_module",
)
self._diagnose_callable(method, func, f"宿主模块 {module_id}")
if self.is_valid_empty(result):
result = func(*args, **kwargs)
@@ -267,6 +283,11 @@ class ModuleInvocationDispatcher:
module_name = self._module_name(module, module_id)
try:
func = getattr(module, method)
self._record_legacy_hit(
method,
caller_type="system",
abi_source="host_module",
)
self._diagnose_callable(method, func, f"宿主模块 {module_id}")
if self.is_valid_empty(result):
result = await self._async_call(func, *args, **kwargs)
@@ -307,6 +328,22 @@ class ModuleInvocationDispatcher:
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
def _diagnose_callable(
method: str,
+5
View File
@@ -37,6 +37,11 @@ METRIC_SPECS = {
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.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.overlap_skip", MetricKind.COUNTER, frozenset({"owner"})),
MetricSpec("scheduler.job.retry", MetricKind.COUNTER, frozenset({"owner"})),
@@ -644,6 +644,8 @@ ModuleMethodSpec(
- 契约清单现覆盖静态扫描到的 211 个宿主字符串调用,并保留一个暂未被宿主调用的 `send_message` 公开能力,
共 212 个显式 V2 spec。原先仅按 prefix 分类或落入默认 legacy 的宿主方法均获得稳定 family、输入合同、
结果合同、执行、超时和错误语义;未知第三方自定义方法仍走开放 legacy fallback,不拒绝加载或执行。
- 未知动态方法在真实 provider 命中时记录 `module.contract.legacy_hit`,区分插件/宿主调用方和 ABI 来源;
该指标只在 callable 实际存在并准备执行时递增,不改变未知第三方方法的开放 fallback、聚合或异常语义。
#### ARCH-241Event Contract Registry
@@ -222,6 +222,71 @@ def test_plugin_non_mapping_module_decl_is_reported_and_skipped() -> None:
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
async def test_async_plugin_non_mapping_module_decl_is_reported_and_skipped() -> None:
"""异步路径下坏插件同样被隔离,嵌套补丁场景不再冒泡击穿调度。"""