|
|
@@ -1,12 +1,13 @@
|
|
|
# -*- coding:utf-8 -*-
|
|
|
"""
|
|
|
-bin/cdc-monitor.py 单测:复制槽判定 / YARN 存活 / 消息渲染 / main 串联。
|
|
|
+bin/cdc-monitor.py 单测:复制槽判定 / Flink 作业存活 / 消息渲染 / main 串联。
|
|
|
|
|
|
-不连真 PG(pgdb.connect + query 走替身)、不跑真 yarn(subprocess 走替身)、
|
|
|
+不连真 PG(pgdb.connect + query 走替身)、不打真 JobManager(urlopen 走替身)、
|
|
|
不发真消息(Alerter 走替身)。
|
|
|
脚本路径含连字符,用 importlib.util 动态加载为模块。
|
|
|
"""
|
|
|
import importlib.util
|
|
|
+import json
|
|
|
import os
|
|
|
import re
|
|
|
import sys
|
|
|
@@ -20,6 +21,7 @@ SCRIPT_PATH = os.path.join(PROJECT_ROOT, 'bin', 'cdc-monitor.py')
|
|
|
|
|
|
GB = 1024 ** 3
|
|
|
LAG_CRIT = 5 * GB
|
|
|
+JM = 'cdhmaster02:8081'
|
|
|
|
|
|
|
|
|
def _load_script():
|
|
|
@@ -41,6 +43,22 @@ def _stub_pg(monkeypatch, rows, conn=None):
|
|
|
return conn
|
|
|
|
|
|
|
|
|
+def _stub_flink(monkeypatch, jobs):
|
|
|
+ """替身 JobManager:/jobs/overview 返回给定作业列表。返回捕获的请求参数。"""
|
|
|
+ captured = {}
|
|
|
+
|
|
|
+ def _open(url, timeout=None):
|
|
|
+ captured['url'] = url
|
|
|
+ captured['timeout'] = timeout
|
|
|
+ resp = MagicMock()
|
|
|
+ resp.read.return_value = json.dumps({'jobs': jobs}).encode('utf-8')
|
|
|
+ resp.__enter__.return_value = resp
|
|
|
+ return resp
|
|
|
+
|
|
|
+ monkeypatch.setattr(MON.urllib.request, 'urlopen', _open)
|
|
|
+ return captured
|
|
|
+
|
|
|
+
|
|
|
# ---------- 复制槽 ----------
|
|
|
|
|
|
def test_cdc_slot_inactive_alerts(monkeypatch):
|
|
|
@@ -104,32 +122,62 @@ def test_db_failure_becomes_alert(monkeypatch):
|
|
|
assert '数据源 ini 不存在' in alerts[0][1]
|
|
|
|
|
|
|
|
|
-# ---------- YARN ----------
|
|
|
+# ---------- Flink 作业存活 ----------
|
|
|
|
|
|
def test_all_jobs_running_no_alert(monkeypatch):
|
|
|
- monkeypatch.setattr(MON.subprocess, 'check_output',
|
|
|
- lambda *a, **kw: b'app-1 st-cdc-large RUNNING\napp-2 st-cdc-small RUNNING')
|
|
|
- assert MON.check_yarn(['st-cdc-large', 'st-cdc-small']) == []
|
|
|
+ captured = _stub_flink(monkeypatch, [
|
|
|
+ {'name': 'st-cdc-large', 'state': 'RUNNING'},
|
|
|
+ {'name': 'st-cdc-small', 'state': 'RUNNING'},
|
|
|
+ ])
|
|
|
+ assert MON.check_flink(JM, ['st-cdc-large', 'st-cdc-small']) == []
|
|
|
+ assert captured['url'] == 'http://cdhmaster02:8081/jobs/overview'
|
|
|
|
|
|
|
|
|
def test_missing_job_alerts(monkeypatch):
|
|
|
- monkeypatch.setattr(MON.subprocess, 'check_output',
|
|
|
- lambda *a, **kw: b'app-1 st-cdc-large RUNNING')
|
|
|
- assert MON.check_yarn(['st-cdc-large', 'st-cdc-small']) == [
|
|
|
- ('作业 st-cdc-small', '不在 YARN RUNNING 列表')]
|
|
|
+ _stub_flink(monkeypatch, [{'name': 'st-cdc-large', 'state': 'RUNNING'}])
|
|
|
+ assert MON.check_flink(JM, ['st-cdc-large', 'st-cdc-small']) == [
|
|
|
+ ('作业 st-cdc-small', '不在 Flink 集群上')]
|
|
|
+
|
|
|
+
|
|
|
+def test_restarting_job_alerts(monkeypatch):
|
|
|
+ """2026-09-07 的故障形态:作业名一直在列表里,只判存在会漏报 7 小时。"""
|
|
|
+ _stub_flink(monkeypatch, [
|
|
|
+ {'name': 'st-cdc-large', 'state': 'RESTARTING'},
|
|
|
+ {'name': 'st-cdc-small', 'state': 'RESTARTING'},
|
|
|
+ ])
|
|
|
+ assert MON.check_flink(JM, ['st-cdc-large', 'st-cdc-small']) == [
|
|
|
+ ('作业 st-cdc-large', '状态 RESTARTING'),
|
|
|
+ ('作业 st-cdc-small', '状态 RESTARTING'),
|
|
|
+ ]
|
|
|
+
|
|
|
+
|
|
|
+def test_canceled_job_alerts(monkeypatch):
|
|
|
+ """取消的作业仍留在 /jobs/overview 里。"""
|
|
|
+ _stub_flink(monkeypatch, [{'name': 'st-cdc-large', 'state': 'CANCELED'}])
|
|
|
+ assert MON.check_flink(JM, ['st-cdc-large']) == [
|
|
|
+ ('作业 st-cdc-large', '状态 CANCELED')]
|
|
|
+
|
|
|
+
|
|
|
+def test_stale_record_does_not_mask_running(monkeypatch):
|
|
|
+ """同名作业的历史记录不能盖掉在跑的那条,RUNNING 优先。"""
|
|
|
+ _stub_flink(monkeypatch, [
|
|
|
+ {'name': 'st-cdc-large', 'state': 'CANCELED'},
|
|
|
+ {'name': 'st-cdc-large', 'state': 'RUNNING'},
|
|
|
+ ])
|
|
|
+ assert MON.check_flink(JM, ['st-cdc-large']) == []
|
|
|
|
|
|
|
|
|
-def test_yarn_failure_becomes_single_alert(monkeypatch):
|
|
|
- def _boom(*a, **kw):
|
|
|
- raise OSError('yarn: command not found')
|
|
|
- monkeypatch.setattr(MON.subprocess, 'check_output', _boom)
|
|
|
- alerts = MON.check_yarn(['st-cdc-large', 'st-cdc-small'])
|
|
|
+def test_flink_failure_becomes_single_alert(monkeypatch):
|
|
|
+ def _boom(url, timeout=None):
|
|
|
+ raise OSError('Connection refused')
|
|
|
+ monkeypatch.setattr(MON.urllib.request, 'urlopen', _boom)
|
|
|
+ alerts = MON.check_flink(JM, ['st-cdc-large', 'st-cdc-small'])
|
|
|
assert len(alerts) == 1 # 查不到就是一条,不按作业数量刷屏
|
|
|
- assert alerts[0][0] == 'YARN'
|
|
|
- assert 'command not found' in alerts[0][1]
|
|
|
+ assert alerts[0][0] == 'Flink 集群'
|
|
|
+ assert 'Connection refused' in alerts[0][1]
|
|
|
|
|
|
|
|
|
-# ---------- 渲染:走一圈全部五类告警 ----------
|
|
|
+# ---------- 渲染:走一圈全部六类告警 ----------
|
|
|
|
|
|
FIXED_NOW = datetime(2026, 8, 26, 15, 21, 27)
|
|
|
|
|
|
@@ -151,26 +199,27 @@ def test_render_all_alert_kinds():
|
|
|
alerts = [
|
|
|
('槽 cdc_large', '空闲,采集作业可能已停'),
|
|
|
('槽 cdc_small', '滞后 6.0 GB'),
|
|
|
- ('作业 st-cdc-large', '不在 YARN RUNNING 列表'),
|
|
|
+ ('作业 st-cdc-large', '不在 Flink 集群上'),
|
|
|
+ ('作业 st-cdc-small', '状态 RESTARTING'),
|
|
|
('主库', '复制槽查询失败:connection refused'),
|
|
|
- ('YARN', '查询失败:timeout'),
|
|
|
+ ('Flink 集群', '查询失败:timeout'),
|
|
|
]
|
|
|
out = MON.render(alerts, now=FIXED_NOW)
|
|
|
lines = out.split('\n')
|
|
|
assert lines[0] == '### <font color="warning">CDC 监控告警</font>'
|
|
|
assert lines[1] == '> 检测时间:2026-08-26 15:21:27'
|
|
|
- assert len(lines) == 7
|
|
|
+ assert len(lines) == 8
|
|
|
for (key, desc), line in zip(alerts, lines[2:]):
|
|
|
assert line == '> {0}:<font color="warning">{1}</font>'.format(key, desc)
|
|
|
|
|
|
|
|
|
# ---------- main 串联 ----------
|
|
|
|
|
|
-def _run_main(monkeypatch, slot_alerts, yarn_alerts):
|
|
|
+def _run_main(monkeypatch, slot_alerts, flink_alerts):
|
|
|
"""跑 main,返回替身 alerter 实例供断言。"""
|
|
|
monkeypatch.setattr(sys, 'argv', ['cdc-monitor.py'])
|
|
|
monkeypatch.setattr(MON, 'check_slots', lambda ds, lag: list(slot_alerts))
|
|
|
- monkeypatch.setattr(MON, 'check_yarn', lambda jobs: list(yarn_alerts))
|
|
|
+ monkeypatch.setattr(MON, 'check_flink', lambda jm, jobs: list(flink_alerts))
|
|
|
instance = MagicMock()
|
|
|
monkeypatch.setattr(MON, 'Alerter', MagicMock(return_value=instance))
|
|
|
MON.main()
|
|
|
@@ -186,7 +235,7 @@ def test_main_sends_once_with_all_alerts(monkeypatch):
|
|
|
instance = _run_main(
|
|
|
monkeypatch,
|
|
|
[('槽 cdc_large', '空闲,采集作业可能已停')],
|
|
|
- [('作业 st-cdc-small', '不在 YARN RUNNING 列表')])
|
|
|
+ [('作业 st-cdc-small', '状态 RESTARTING')])
|
|
|
instance.send_markdown.assert_called_once()
|
|
|
content = instance.send_markdown.call_args[0][0]
|
|
|
assert '槽 cdc_large' in content
|
|
|
@@ -197,21 +246,37 @@ def test_main_builds_alerter_before_checks(monkeypatch):
|
|
|
"""健康时也要构造 Alerter:配置坏了要立刻暴露,不能等真出事才发现发不出去。"""
|
|
|
monkeypatch.setattr(sys, 'argv', ['cdc-monitor.py'])
|
|
|
monkeypatch.setattr(MON, 'check_slots', lambda ds, lag: [])
|
|
|
- monkeypatch.setattr(MON, 'check_yarn', lambda jobs: [])
|
|
|
+ monkeypatch.setattr(MON, 'check_flink', lambda jm, jobs: [])
|
|
|
fake_cls = MagicMock()
|
|
|
monkeypatch.setattr(MON, 'Alerter', fake_cls)
|
|
|
MON.main()
|
|
|
fake_cls.assert_called_once_with(channel='default')
|
|
|
|
|
|
|
|
|
+def test_main_defaults_cover_both_jobs(monkeypatch):
|
|
|
+ """默认值必须带上小表组,否则它挂了不报警。"""
|
|
|
+ seen = {}
|
|
|
+ monkeypatch.setattr(sys, 'argv', ['cdc-monitor.py'])
|
|
|
+ monkeypatch.setattr(MON, 'check_slots', lambda ds, lag: [])
|
|
|
+ monkeypatch.setattr(MON, 'check_flink',
|
|
|
+ lambda jm, jobs: seen.update(jm=jm, jobs=jobs) or [])
|
|
|
+ monkeypatch.setattr(MON, 'Alerter', MagicMock())
|
|
|
+
|
|
|
+ MON.main()
|
|
|
+
|
|
|
+ assert seen['jm'] == 'cdhmaster02:8081'
|
|
|
+ assert seen['jobs'] == ['st-cdc-large', 'st-cdc-small']
|
|
|
+
|
|
|
+
|
|
|
def test_main_passes_cli_overrides(monkeypatch):
|
|
|
seen = {}
|
|
|
monkeypatch.setattr(sys, 'argv', [
|
|
|
'cdc-monitor.py', '-ds', 'postgresql/other', '-channel', 'realtime',
|
|
|
- '-jobs', 'a, b ,', '-lag-gb', '2'])
|
|
|
+ '-jm', 'other-host:9081', '-jobs', 'a, b ,', '-lag-gb', '2'])
|
|
|
monkeypatch.setattr(MON, 'check_slots',
|
|
|
lambda ds, lag: seen.update(ds=ds, lag=lag) or [])
|
|
|
- monkeypatch.setattr(MON, 'check_yarn', lambda jobs: seen.update(jobs=jobs) or [])
|
|
|
+ monkeypatch.setattr(MON, 'check_flink',
|
|
|
+ lambda jm, jobs: seen.update(jm=jm, jobs=jobs) or [])
|
|
|
fake_cls = MagicMock()
|
|
|
monkeypatch.setattr(MON, 'Alerter', fake_cls)
|
|
|
|
|
|
@@ -219,5 +284,6 @@ def test_main_passes_cli_overrides(monkeypatch):
|
|
|
|
|
|
assert seen['ds'] == 'postgresql/other'
|
|
|
assert seen['lag'] == 2 * GB
|
|
|
+ assert seen['jm'] == 'other-host:9081'
|
|
|
assert seen['jobs'] == ['a', 'b'] # 逗号分隔去空白、丢空项
|
|
|
fake_cls.assert_called_once_with(channel='realtime')
|