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:
总工
2026-09-14 01:10:58 +08:00
co-authored by Cursor multica-agent
parent f014eb11bd
commit abd4d22a67
6 changed files with 178 additions and 6 deletions
+18 -4
View File
@@ -7,6 +7,7 @@ from datahub.adapters import RESERVED
from datahub.auth import AuthService
from datahub.db import HubDB
from datahub import lineage as lineage_module
from datahub import observability
from datahub.pipeline import OFFICIAL_DATASETS, STOCKS_DATASET, Pipeline
from datahub.scheduler import Scheduler
from datahub.serving import ApiError
@@ -114,6 +115,8 @@ class AdminAPI:
# static registries in source_catalog.py / lineage.py.
# ------------------------------------------------------------------
def providers_status(self, provider: str = "", limit: int = 50) -> dict[str, Any]:
if not observability.is_enabled(self.db):
return {"enabled": False, "health": [], "recent_calls": []}
limit = max(1, min(int(limit or 50), 200))
health_sql = "SELECT * FROM provider_health"
params: tuple[Any, ...] = ()
@@ -127,19 +130,30 @@ class AdminAPI:
calls_sql += " WHERE provider = ?"
calls_sql += " ORDER BY id DESC LIMIT ?"
recent = self.db.fetchall(calls_sql, (*params, limit))
return {"health": health, "recent_calls": recent}
return {"enabled": True, "health": health, "recent_calls": recent}
def source_catalog(self) -> dict[str, Any]:
return {"items": source_catalog.snapshot(self.db, self.auth)}
if not observability.is_enabled(self.db):
return {"enabled": False, "items": []}
return {"enabled": True, "items": source_catalog.snapshot(self.db, self.auth)}
def lineage(self, trade_date: str = "") -> dict[str, Any]:
day = yyyymmdd(trade_date) if trade_date else yyyymmdd(now_shanghai())
return {"trade_date": day, "items": lineage_module.snapshot(self.db, day)}
if not observability.is_enabled(self.db):
return {"enabled": False, "trade_date": day, "items": []}
return {"enabled": True, "trade_date": day, "items": lineage_module.snapshot(self.db, day)}
def lineage_affected(self, provider: str = "", interface: str = "") -> dict[str, Any]:
if not provider:
raise ApiError("INVALID_ARGUMENT", "provider is required")
return {"provider": provider, "interface": interface, "items": lineage_module.affected(self.db, provider, interface)}
if not observability.is_enabled(self.db):
return {"enabled": False, "provider": provider, "interface": interface, "items": []}
return {
"enabled": True,
"provider": provider,
"interface": interface,
"items": lineage_module.affected(self.db, provider, interface),
}
def jobs(self) -> dict[str, Any]:
runs = self.db.fetchall("SELECT * FROM job_runs ORDER BY id DESC LIMIT 100")