HEL-543 返工: 补充 observability 运行时可关闭开关 (DATAHUB_OBSERVABILITY)
总工复核 🔴:安全边界要求新增观测功能必须可关闭、关闭后现有功能完全照旧, 但此前实现没有任何运行时开关。修复: - settings.py: 新增 Settings.observability_enabled 字段,沿用既有 DATAHUB_SCHEDULER 的环境变量模式,读取 DATAHUB_OBSERVABILITY (0/false/off 关闭,默认开启)。 - hub.py: Hub.__init__ 把 settings.observability_enabled 挂到 self.db 上,让 pipeline/realtime_serve/steward/admin_api 已经 在传的 db 参数直接带上开关,零额外改造。 - observability.py: 新增 is_enabled(db),缺失该属性时默认按启用处理 (向后兼容裸 HubDB 用例/测试)。关闭时 observe() 变成纯 透传(不计时、不分类、不碰数据库),record_call() 直接 no-op。 - admin_api.py: 4 个新只读端点关闭时返回明确的 {"enabled": false, ...空结构} 而不是静默返回旧数据。 - 新增 10 个测试:开关默认值/环境变量解析、关闭后 observe() 的透传语义 (含异常原样重新抛出)、关闭后 record_call() 零写入、关闭后重新开启恢复 记录、4 个 admin 端点在关闭态的响应结构。 全量测试 183/183 通过(新增 10 个,含此前 173 个零回归)。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
f014eb11bd
commit
abd4d22a67
@@ -210,5 +210,97 @@ class ObserveTests(unittest.TestCase):
|
||||
self.assertIs(result, sentinel)
|
||||
|
||||
|
||||
class _ToggleDB(HubDB):
|
||||
"""A real HubDB subclass so we can flip the HEL-543 kill switch the same
|
||||
way Hub.__init__ does, without needing a full Hub/Settings wiring."""
|
||||
|
||||
|
||||
class KillSwitchTests(unittest.TestCase):
|
||||
"""HEL-543 total-review 🔴: the observability side channel must be
|
||||
disable-able at runtime, and disabling it must leave existing behavior
|
||||
completely unchanged (pure passthrough, zero db access)."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.db = _ToggleDB(Path(self.tmp.name) / "hub.db")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_is_enabled_defaults_true_when_attribute_absent(self):
|
||||
# A bare HubDB (as used throughout the rest of this test suite, and
|
||||
# by any pre-HEL-543 call site) must default to enabled.
|
||||
self.assertTrue(observability.is_enabled(self.db))
|
||||
self.assertTrue(observability.is_enabled(None))
|
||||
|
||||
def test_disabled_record_call_writes_nothing(self):
|
||||
self.db.observability_enabled = False
|
||||
observability.record_call(self.db, "eastmoney", "indices", status="ok", latency_ms=1)
|
||||
self.assertEqual(self.db.fetchall("SELECT * FROM provider_call_log"), [])
|
||||
self.assertEqual(self.db.fetchall("SELECT * FROM provider_health"), [])
|
||||
|
||||
def test_disabled_observe_is_a_pure_passthrough_on_success(self):
|
||||
self.db.observability_enabled = False
|
||||
sentinel = {"ts_code": "600000.SH"}
|
||||
calls = {"n": 0}
|
||||
|
||||
def fn():
|
||||
calls["n"] += 1
|
||||
return sentinel
|
||||
|
||||
result = observability.observe(self.db, "eastmoney", "indices", fn)
|
||||
self.assertIs(result, sentinel)
|
||||
self.assertEqual(calls["n"], 1)
|
||||
self.assertEqual(self.db.fetchall("SELECT * FROM provider_call_log"), [])
|
||||
|
||||
def test_disabled_observe_still_reraises_the_exact_exception(self):
|
||||
self.db.observability_enabled = False
|
||||
boom = RuntimeError("upstream exploded")
|
||||
|
||||
def fn():
|
||||
raise boom
|
||||
|
||||
with self.assertRaises(RuntimeError) as ctx:
|
||||
observability.observe(self.db, "eastmoney", "indices", fn)
|
||||
self.assertIs(ctx.exception, boom)
|
||||
self.assertEqual(self.db.fetchall("SELECT * FROM provider_call_log"), [])
|
||||
|
||||
def test_re_enabling_resumes_recording(self):
|
||||
self.db.observability_enabled = False
|
||||
observability.observe(self.db, "eastmoney", "indices", lambda: {"ok": True})
|
||||
self.assertEqual(self.db.fetchall("SELECT * FROM provider_call_log"), [])
|
||||
self.db.observability_enabled = True
|
||||
observability.observe(self.db, "eastmoney", "indices", lambda: {"ok": True})
|
||||
self.assertEqual(len(self.db.fetchall("SELECT * FROM provider_call_log")), 1)
|
||||
|
||||
|
||||
class SettingsToggleTests(unittest.TestCase):
|
||||
"""The kill switch follows the same env-var pattern as DATAHUB_SCHEDULER."""
|
||||
|
||||
def test_defaults_to_enabled(self):
|
||||
from datahub.settings import load_settings
|
||||
|
||||
settings = load_settings(env={})
|
||||
self.assertTrue(settings.observability_enabled)
|
||||
|
||||
def test_datahub_observability_zero_disables(self):
|
||||
from datahub.settings import load_settings
|
||||
|
||||
settings = load_settings(env={"DATAHUB_OBSERVABILITY": "0"})
|
||||
self.assertFalse(settings.observability_enabled)
|
||||
|
||||
def test_datahub_observability_off_disables(self):
|
||||
from datahub.settings import load_settings
|
||||
|
||||
settings = load_settings(env={"DATAHUB_OBSERVABILITY": "off"})
|
||||
self.assertFalse(settings.observability_enabled)
|
||||
|
||||
def test_datahub_observability_one_keeps_enabled(self):
|
||||
from datahub.settings import load_settings
|
||||
|
||||
settings = load_settings(env={"DATAHUB_OBSERVABILITY": "1"})
|
||||
self.assertTrue(settings.observability_enabled)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user