feat(telegram): ADR-019 Phase 6 - daily data freshness probe + cron 09:05
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
ADR-019 Phase 6:每日 09:05 OpenClawBot scheduler 主動巡檢 realtime_sales_monthly 最新資料日期,落後超過閾值時透過 EventRouter 發 Telegram 警告。 新增 services/data_freshness_probe.py: - gap == 0:靜默不發(資料齊全) - gap == 1:info(昨日資料齊,正常) - gap == 2~3:warning - gap >= 4:alert(P2,ETL 大概率出問題) - latest_date 取不到:alert(DB 連線異常) routes/openclaw_bot_routes.py 加 cron job openclaw_data_freshness_probe (hour=9 minute=5,避開 09:00 的其他既有 job)。 從『用戶月初按按鈕踩坑才發現資料缺口』的被動模式,轉成 agent 主動巡檢通知。 配合 Phase 1(PPT freshness gate)+ Phase 2(agent tool)+ Phase 3(cmd 路徑 agent dispatch)+ Phase 4(對話 state),Telegram Bot 互動層的『rigid default + 靜默空白』反模式根除。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3323,8 +3323,19 @@ def start_scheduler():
|
||||
id="openclaw_check_anomalies",
|
||||
replace_existing=True,
|
||||
)
|
||||
# ADR-019 Phase 6: 每日 09:00 主動巡檢資料新鮮度,缺資料時透過 EventRouter 發警告
|
||||
try:
|
||||
from services.data_freshness_probe import run_data_freshness_probe
|
||||
_scheduler.add_job(
|
||||
run_data_freshness_probe,
|
||||
CronTrigger(hour=9, minute=5),
|
||||
id="openclaw_data_freshness_probe",
|
||||
replace_existing=True,
|
||||
)
|
||||
except ImportError as _e:
|
||||
sys_log.warning(f"[OpenClawBot] data_freshness_probe 未安裝,跳過:{_e}")
|
||||
_scheduler.start()
|
||||
sys_log.info("[OpenClawBot] Scheduler started ✓ (competitor/morning/excel/evening/weekly/anomaly)")
|
||||
sys_log.info("[OpenClawBot] Scheduler started ✓ (competitor/morning/excel/evening/weekly/anomaly/freshness)")
|
||||
except ImportError:
|
||||
sys_log.warning("[OpenClawBot] APScheduler 未安裝 — 排程功能停用")
|
||||
except Exception as e:
|
||||
|
||||
128
services/data_freshness_probe.py
Normal file
128
services/data_freshness_probe.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""ADR-019 Phase 6:每日資料新鮮度主動巡檢。
|
||||
|
||||
每天 09:00 由 OpenClawBot scheduler 觸發,檢查 realtime_sales_monthly 最新資料
|
||||
日期,若落後超過閾值就透過 EventRouter 主動發 Telegram 警告,避免用戶月初/
|
||||
ETL 卡住才靠按按鈕踩坑發現。
|
||||
|
||||
EventRouter severity 對應:
|
||||
- gap == 0:今日資料已進,靜默不發
|
||||
- gap == 1:昨日資料齊全,info(不發或只 log)
|
||||
- gap == 2~3:warning,可能 ETL 跑慢
|
||||
- gap >= 4:alert(P2),ETL 大概率出問題
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, date
|
||||
import logging
|
||||
|
||||
from services.event_router import dispatch_sync
|
||||
|
||||
logger = logging.getLogger("DataFreshnessProbe")
|
||||
|
||||
_DEFAULT_WARN_GAP_DAYS = 2
|
||||
_DEFAULT_ALERT_GAP_DAYS = 4
|
||||
|
||||
|
||||
def _query_latest_date():
|
||||
"""獨立 import 避免循環依賴。回傳 'YYYY-MM-DD' 字串或 None。"""
|
||||
try:
|
||||
from routes.openclaw_bot_routes import latest_date
|
||||
return latest_date()
|
||||
except Exception as e:
|
||||
logger.warning(f"[DataFreshnessProbe] 取得 latest_date 失敗:{e}")
|
||||
return None
|
||||
|
||||
|
||||
def _parse_date(s: str) -> date | None:
|
||||
if not s:
|
||||
return None
|
||||
try:
|
||||
return datetime.strptime(s.replace('/', '-'), '%Y-%m-%d').date()
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
|
||||
|
||||
def run_data_freshness_probe(
|
||||
warn_gap_days: int = _DEFAULT_WARN_GAP_DAYS,
|
||||
alert_gap_days: int = _DEFAULT_ALERT_GAP_DAYS,
|
||||
) -> dict:
|
||||
"""主動巡檢一次,缺資料時透過 EventRouter 發通知。回傳 probe 結果摘要。"""
|
||||
today = date.today()
|
||||
latest_str = _query_latest_date()
|
||||
latest_dt = _parse_date(latest_str)
|
||||
|
||||
result = {
|
||||
'today': today.isoformat(),
|
||||
'latest_date': latest_str,
|
||||
'gap_days': None,
|
||||
'severity': 'unknown',
|
||||
'notified': False,
|
||||
}
|
||||
|
||||
if latest_dt is None:
|
||||
logger.error("[DataFreshnessProbe] 無法取得 latest_date — 視為嚴重異常")
|
||||
result['severity'] = 'alert'
|
||||
try:
|
||||
dispatch_sync({
|
||||
'source': 'OpenClawBot.DataFreshnessProbe',
|
||||
'event_type': 'data_freshness_unknown',
|
||||
'severity': 'alert',
|
||||
'title': '⚠️ 資料新鮮度巡檢失敗',
|
||||
'status': '無法取得 latest_date',
|
||||
'impact': 'P2 — 業績資料源不可達,所有報表 / NL 查詢可能失準',
|
||||
'summary': '請檢查 realtime_sales_monthly 表與 DB 連線。',
|
||||
})
|
||||
result['notified'] = True
|
||||
except Exception as e:
|
||||
logger.error(f"[DataFreshnessProbe] EventRouter dispatch 失敗:{e}")
|
||||
return result
|
||||
|
||||
gap = (today - latest_dt).days
|
||||
result['gap_days'] = gap
|
||||
|
||||
if gap <= 0:
|
||||
result['severity'] = 'success'
|
||||
logger.info(f"[DataFreshnessProbe] 資料齊全(latest={latest_dt}, today={today})")
|
||||
return result
|
||||
|
||||
if gap == 1:
|
||||
result['severity'] = 'info'
|
||||
logger.info(f"[DataFreshnessProbe] 落後 1 天(正常作息,ETL 通常隔天匯入)")
|
||||
return result
|
||||
|
||||
if gap < warn_gap_days:
|
||||
result['severity'] = 'info'
|
||||
return result
|
||||
|
||||
severity = 'alert' if gap >= alert_gap_days else 'warning'
|
||||
result['severity'] = severity
|
||||
|
||||
try:
|
||||
dispatch_sync({
|
||||
'source': 'OpenClawBot.DataFreshnessProbe',
|
||||
'event_type': 'data_freshness_lag',
|
||||
'severity': severity,
|
||||
'title': f'📊 業績資料落後 {gap} 天',
|
||||
'status': f'最新資料:{latest_str}(今日:{today.isoformat()})',
|
||||
'impact': (
|
||||
f'P2 — 月報/日報 PPT 與 NL 業績查詢可能空白或失準,'
|
||||
f'請檢查 realtime_sales_monthly ETL 任務(run_momo_task / run_auto_import_task)'
|
||||
if severity == 'alert' else
|
||||
f'P3 — 資料落後 {gap} 天,請確認 ETL 是否正常排程'
|
||||
),
|
||||
'summary': f'gap={gap}d, latest={latest_str}',
|
||||
'payload': {
|
||||
'gap_days': gap,
|
||||
'latest_date': latest_str,
|
||||
'probe_at': datetime.now().isoformat(timespec='seconds'),
|
||||
},
|
||||
})
|
||||
result['notified'] = True
|
||||
logger.warning(
|
||||
f"[DataFreshnessProbe] {severity}: gap={gap}d, latest={latest_str} → notified"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[DataFreshnessProbe] EventRouter dispatch 失敗:{e}")
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user