feat(site): 站点拨测支持接口返回内容监控(文本 + JSON 字段)
需求:监控接口返回的数据本身是否变化(如 Cloudflare ips-v4 的 IP 段清单),
而不是只看服务是否可用。两者是不同的问题:服务 200 OK 但内容变了,拨测发现不了。
后端
- SiteTask 新增 content_check_mode(off/text/json)、content_paths(JSONPath)、
content_ignore_rules、content_min_length、content_confirm_times、content_sort_lines
- 新增 monitor_site_baselines(基线多版本)与 monitor_site_content_changes(变更记录)
- pkg/siteprobe/content.go:零依赖 JSONPath($.a.b[0]、$.a[*].b)、文本/JSON 规范化、
sha256 哈希、LCS 行级 diff、字段级 diff
- 核心判定区分「采集异常」与「内容变更」:状态码非 2xx、空响应、短于最小长度、
非合法 JSON 均不算变更,避免 CDN 拦截页造成误报
- 连续 N 次(默认 2)不一致才落变更记录,抑制抖动
- 基线首次自动建立(用户选择自动接受),之后变更需人工「接受为新基线」并留审计
- 补齐历史遗留死字段:expected_content(正则断言)与 expected_length 此前
只有表结构、执行器从未校验,现已实现
- 告警引擎新增 content_changed 指标,人工处理后自动恢复
- 迁移 0006 用 AutoMigrate 实现幂等(空库 0000 已建表,静态 ALTER 会重复列)
前端
- 任务表单新增内容监控配置区(模式/JSONPath/忽略规则/最小长度/确认次数/排序)
- 详情页新增内容监控面板:当前基线、待确认变更、并排 diff、版本历史、
接受/忽略操作
验证
- 9 项单元测试(JSONPath、时间戳忽略、顺序无关、采集异常、字段级监控、diff)
- 端到端:Cloudflare ips-v4 建基线 v1 → 模拟变更 → 第 2 轮告警 → 接受为 v2
- JSON 模式实测快照仅含指定字段:{"$.db_driver":"mysql","$.version":"v1.0.2"}
// Logan.Li
This commit is contained in:
1 parent
286d7cd906
commit
539db36f2a
18 files changed
+1704
-3
No files matched your search
@@ -76,6 +76,24 @@ type SiteTask struct {
|
||||
UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"`
|
||||
DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"`
|
||||
|
||||
// ── 内容监控(Content Monitor)────────────────────────────────
|
||||
// 区别于"可用性拨测":服务活着但返回内容变了,这里负责发现。 // Logan.Li
|
||||
ContentCheckMode string `gorm:"type:varchar(8);default:'off';comment:内容检查模式 off/text/json" json:"content_check_mode"`
|
||||
// ContentPaths JSON 模式要参与比对字段的 JSONPath,如 ["$.data.status","$.data.ips"]
|
||||
ContentPaths string `gorm:"type:text;comment:JSONPath字段列表(JSON数组)" json:"content_paths,omitempty"`
|
||||
// ContentIgnoreRules 忽略规则:文本按正则替换为空,避免时间戳/nonce 造成误报
|
||||
ContentIgnoreRules string `gorm:"type:text;comment:忽略规则(JSON数组,正则)" json:"content_ignore_rules,omitempty"`
|
||||
// ContentMinLength 小于该长度判为"采集异常"而非"内容变更",防止错误页误报
|
||||
ContentMinLength int `gorm:"default:0;comment:最小有效长度(字节)" json:"content_min_length"`
|
||||
// ContentConfirmTimes 连续 N 次不一致才判定变更,抑制抖动
|
||||
ContentConfirmTimes int `gorm:"default:2;comment:连续确认次数" json:"content_confirm_times"`
|
||||
// ContentSortLines 纯文本是否排序后比对(顺序无关的清单类接口应开启)
|
||||
ContentSortLines bool `gorm:"default:false;comment:文本排序后比对" json:"content_sort_lines"`
|
||||
// CurrentBaselineID 当前生效的基线版本
|
||||
CurrentBaselineID uint `gorm:"default:0;comment:当前基线ID" json:"current_baseline_id"`
|
||||
// PendingChangeCount 待确认的变更次数(用于防抖计数)
|
||||
PendingChangeCount int `gorm:"default:0;comment:连续变更计数" json:"pending_change_count"`
|
||||
|
||||
// 关联关系
|
||||
Results []SiteResult `gorm:"foreignKey:TaskID" json:"results,omitempty"`
|
||||
Alerts []SiteAlert `gorm:"foreignKey:TaskID" json:"alerts,omitempty"`
|
||||
@@ -265,3 +283,55 @@ type SiteResultQuery struct {
|
||||
Limit int `json:"limit" form:"limit"`
|
||||
Offset int `json:"offset" form:"offset"`
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 内容监控:基线版本 + 变更记录
|
||||
// ============================================
|
||||
|
||||
// SiteBaseline 内容基线(一个任务的多版本快照)。
|
||||
//
|
||||
// 首次采集自动建立 v1(source=auto),之后每次"接受新基线"新增一版(source=manual)。
|
||||
// 保留完整规范化内容,用于并排 diff 与回溯。 // Logan.Li
|
||||
type SiteBaseline struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
TaskID uint `gorm:"not null;index:idx_baseline_task;comment:任务ID" json:"task_id"`
|
||||
Version int `gorm:"not null;comment:版本号(从1递增)" json:"version"`
|
||||
ContentHash string `gorm:"type:varchar(64);not null;comment:规范化内容sha256" json:"content_hash"`
|
||||
ContentText string `gorm:"type:longtext;comment:规范化后的内容快照" json:"content_text,omitempty"`
|
||||
Source string `gorm:"type:varchar(16);default:'auto';comment:来源 auto/manual" json:"source"`
|
||||
AcceptedBy string `gorm:"type:varchar(128);comment:确认人" json:"accepted_by"`
|
||||
Note string `gorm:"type:varchar(512);comment:备注" json:"note"`
|
||||
CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名 // Logan.Li
|
||||
func (SiteBaseline) TableName() string { return "monitor_site_baselines" }
|
||||
|
||||
// SiteContentChange 内容变更记录。
|
||||
//
|
||||
// Status 流转:pending(已发现,待处理)→ accepted(接受为新基线)/ ignored(误报忽略)。
|
||||
// DiffPayload 存结构化差异,前端并排视图直接渲染。 // Logan.Li
|
||||
type SiteContentChange struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
TaskID uint `gorm:"not null;index:idx_change_task" json:"task_id"`
|
||||
FromBaselineID uint `gorm:"not null;comment:变更前基线" json:"from_baseline_id"`
|
||||
FromVersion int `gorm:"comment:变更前版本号" json:"from_version"`
|
||||
ContentHash string `gorm:"type:varchar(64);comment:新内容hash" json:"content_hash"`
|
||||
ContentText string `gorm:"type:longtext;comment:新内容快照" json:"content_text,omitempty"`
|
||||
DiffType string `gorm:"type:varchar(16);comment:text_line/json_field" json:"diff_type"`
|
||||
DiffPayload string `gorm:"type:longtext;comment:结构化diff(JSON)" json:"diff_payload,omitempty"`
|
||||
Status string `gorm:"type:varchar(16);default:'pending';index;comment:状态" json:"status"`
|
||||
HandledBy string `gorm:"type:varchar(128);comment:处理人" json:"handled_by,omitempty"`
|
||||
DetectedAt time.Time `gorm:"not null;comment:发现时间" json:"detected_at"`
|
||||
HandledAt *time.Time `json:"handled_at,omitempty"`
|
||||
}
|
||||
|
||||
// TableName 指定表名 // Logan.Li
|
||||
func (SiteContentChange) TableName() string { return "monitor_site_content_changes" }
|
||||
|
||||
// 内容变更状态常量 // Logan.Li
|
||||
const (
|
||||
ContentChangePending = "pending"
|
||||
ContentChangeAccepted = "accepted"
|
||||
ContentChangeIgnored = "ignored"
|
||||
)
|
||||
Reference in new issue
Block a user