Files
Logan.Li 539db36f2a
AIOps CI / Backend (test, vet, build) (push) Canceled after 0s
AIOps CI / Agent (test, vet, build) (push) Canceled after 0s
AIOps CI / Frontend (typecheck, lint, build) (push) Canceled after 0s
AIOps CI / API integration (SQLite + redka) (push) Canceled after 0s
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
2026-10-18 21:41:00 +00:00

338 lines
17 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package model
import (
"time"
)
// ============================================
// SiteTask 拨测任务
// ============================================
// SiteTaskType 拨测任务类型
type SiteTaskType string
const (
SiteTaskTypeHTTP SiteTaskType = "http" // HTTP/HTTPS Web 拨测
SiteTaskTypeSSL SiteTaskType = "ssl" // SSL/TLS 证书拨测
SiteTaskTypeTCP SiteTaskType = "tcp" // TCP拨测
SiteTaskTypePing SiteTaskType = "ping" // ICMP Ping拨测
SiteTaskTypeDNS SiteTaskType = "dns" // DNS拨测
SiteTaskTypeUDP SiteTaskType = "udp" // UDP拨测
SiteTaskTypeFTP SiteTaskType = "ftp" // FTP拨测
SiteTaskTypeSMTP SiteTaskType = "smtp" // SMTP拨测
SiteTaskTypeTraceroute SiteTaskType = "traceroute" // 路由追踪
)
// SiteTaskMethod HTTP请求方法
type SiteTaskMethod string
const (
SiteTaskMethodGET SiteTaskMethod = "GET"
SiteTaskMethodPOST SiteTaskMethod = "POST"
SiteTaskMethodHEAD SiteTaskMethod = "HEAD"
)
// SiteTaskStatus 拨测任务状态
type SiteTaskStatus string
const (
SiteTaskStatusRunning SiteTaskStatus = "running" // 运行中
SiteTaskStatusPaused SiteTaskStatus = "paused" // 已暂停
SiteTaskStatusError SiteTaskStatus = "error" // 异常
)
// SiteTask 站点拨测任务表
// 定义对网站、服务等进行主动拨测的任务配置
type SiteTask struct {
ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
Name string `gorm:"type:varchar(128);not null;comment:任务名称" json:"name"`
URL string `gorm:"type:varchar(512);not null;comment:拨测URL/地址" json:"url"`
TaskType SiteTaskType `gorm:"type:varchar(16);not null;comment:拨测类型" json:"task_type"`
Method SiteTaskMethod `gorm:"type:varchar(8);default:'GET';comment:HTTP方法" json:"method"`
// HTTP 拨测配置
Headers string `gorm:"type:text;comment:请求头JSON" json:"headers,omitempty"`
Body string `gorm:"type:text;comment:请求体" json:"body,omitempty"`
Timeout int `gorm:"default:10;comment:超时时间(秒)" json:"timeout"`
FollowRedirect bool `gorm:"default:true;comment:是否跟随重定向" json:"follow_redirect"`
VerifySSL bool `gorm:"default:true;comment:是否验证SSL证书" json:"verify_ssl"`
// 期望响应配置
ExpectedStatus int `gorm:"default:200;comment:期望状态码" json:"expected_status"`
ExpectedContent string `gorm:"type:text;comment:期望响应内容(支持正则)" json:"expected_content,omitempty"`
ExpectedLength int `gorm:"default:0;comment:期望响应长度(0表示不检查)" json:"expected_length"`
// 执行配置
Frequency int `gorm:"default:60;comment:拨测频率(秒)" json:"frequency"`
Nodes string `gorm:"type:varchar(256);comment:执行节点列表JSON" json:"nodes,omitempty"`
// DNS 配置
DNSResolver string `gorm:"type:varchar(64);comment:DNS解析服务器" json:"dns_resolver,omitempty"`
// 状态
Status SiteTaskStatus `gorm:"type:varchar(16);default:'running';comment:任务状态" json:"status"`
LastResult string `gorm:"type:varchar(16);comment:最近结果: success/fail" json:"last_result,omitempty"`
LastCheckAt *time.Time `gorm:"comment:最近检查时间" json:"last_check_at,omitempty"`
SuccessRate float64 `gorm:"type:decimal(5,2);default:100.00;comment:成功率(%)" json:"success_rate"`
AvgResponse float64 `gorm:"comment:平均响应时间(ms)" json:"avg_response,omitempty"`
Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"`
CreatedBy uint `gorm:"index;comment:创建人ID" json:"created_by"`
CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"`
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"`
}
// TableName 指定表名
func (SiteTask) TableName() string {
return "monitor_site_tasks"
}
// ============================================
// SiteResult 拨测结果
// ============================================
// SiteResultStatus 拨测结果状态
type SiteResultStatus string
const (
SiteResultSuccess SiteResultStatus = "success" // 成功
SiteResultFail SiteResultStatus = "fail" // 失败
SiteResultTimeout SiteResultStatus = "timeout" // 超时
)
// SiteResult 拨测结果表
// 存储每次拨测的详细结果数据
type SiteResult struct {
ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
TaskID uint `gorm:"not null;index:idx_task_time;comment:任务ID" json:"task_id"`
Status SiteResultStatus `gorm:"type:varchar(16);not null;comment:结果状态" json:"status"`
HTTPStatusCode int `gorm:"comment:HTTP状态码" json:"http_status_code,omitempty"`
// 时间指标
DNSTime float64 `gorm:"comment:DNS解析时间(ms)" json:"dns_time,omitempty"`
ConnectTime float64 `gorm:"comment:连接时间(ms)" json:"connect_time,omitempty"`
TLSTime float64 `gorm:"comment:TLS握手时间(ms)" json:"tls_time,omitempty"`
TTFB float64 `gorm:"comment:首字节时间(ms)" json:"ttfb,omitempty"`
ResponseTime float64 `gorm:"comment:响应时间(ms)" json:"response_time,omitempty"`
TotalTime float64 `gorm:"not null;comment:总耗时(ms)" json:"total_time"`
// 响应信息
ResponseSize int `gorm:"comment:响应大小(bytes)" json:"response_size,omitempty"`
ResponseBody string `gorm:"type:text;comment:响应内容摘要" json:"response_body,omitempty"`
// 网络信息
RemoteIP string `gorm:"type:varchar(64);comment:远端IP" json:"remote_ip,omitempty"`
LocalNode string `gorm:"type:varchar(64);comment:执行节点" json:"local_node,omitempty"`
// 错误信息
ErrorMessage string `gorm:"type:text;comment:错误信息" json:"error_message,omitempty"`
ErrorDetail string `gorm:"type:text;comment:错误详情" json:"error_detail,omitempty"`
// SSL 证书信息
SSLCertExpiry *time.Time `gorm:"comment:SSL证书过期时间" json:"ssl_cert_expiry,omitempty"`
SSLCertIssuer string `gorm:"type:varchar(256);comment:SSL证书签发者" json:"ssl_cert_issuer,omitempty"`
SSLCertSubject string `gorm:"type:varchar(256);comment:SSL证书主题" json:"ssl_cert_subject,omitempty"`
SSLDaysRemaining int `gorm:"comment:SSL证书剩余天数" json:"ssl_days_remaining,omitempty"`
// 检查内容匹配
ContentMatch bool `gorm:"comment:内容匹配结果" json:"content_match,omitempty"`
// 时间戳
CheckedAt time.Time `gorm:"not null;index:idx_task_time;comment:拨测时间" json:"checked_at"`
CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"`
// 关联关系
Task *SiteTask `gorm:"foreignKey:TaskID" json:"task,omitempty"`
}
// TableName 指定表名
func (SiteResult) TableName() string {
return "monitor_site_results"
}
// ============================================
// SiteAlert 拨测告警
// ============================================
// SiteAlertStatus 拨测告警状态
type SiteAlertStatus string
const (
SiteAlertStatusActive SiteAlertStatus = "active" // 活跃
SiteAlertStatusResolved SiteAlertStatus = "resolved" // 已恢复
SiteAlertStatusMuted SiteAlertStatus = "muted" // 已静默
)
// SiteAlertSeverity 拨测告警级别
type SiteAlertSeverity string
const (
SiteAlertCritical SiteAlertSeverity = "critical" // 严重
SiteAlertWarning SiteAlertSeverity = "warning" // 警告
SiteAlertInfo SiteAlertSeverity = "info" // 通知
)
// SiteAlert 拨测告警表
// 记录拨测任务触发的告警事件
type SiteAlert struct {
ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
TaskID uint `gorm:"not null;index;comment:任务ID" json:"task_id"`
Severity SiteAlertSeverity `gorm:"type:varchar(16);not null;comment:告警级别" json:"severity"`
Status SiteAlertStatus `gorm:"type:varchar(16);default:'active';index;comment:告警状态" json:"status"`
Title string `gorm:"type:varchar(256);not null;comment:告警标题" json:"title"`
Message string `gorm:"type:text;comment:告警详情" json:"message,omitempty"`
// 触发条件
TriggerType string `gorm:"type:varchar(32);comment:触发类型: timeout/status_error/response_time/ssl_expiry" json:"trigger_type"`
TriggerValue string `gorm:"type:varchar(128);comment:触发值" json:"trigger_value,omitempty"`
Threshold float64 `gorm:"comment:阈值" json:"threshold,omitempty"`
// 响应信息
ResponseTime float64 `gorm:"comment:响应时间(ms)" json:"response_time,omitempty"`
HTTPStatus int `gorm:"comment:HTTP状态码" json:"http_status,omitempty"`
ErrorMessage string `gorm:"type:text;comment:错误信息" json:"error_message,omitempty"`
// 状态变化
StartedAt time.Time `gorm:"not null;comment:告警开始时间" json:"started_at"`
ResolvedAt *time.Time `gorm:"comment:告警恢复时间" json:"resolved_at,omitempty"`
Duration int `gorm:"comment:持续时间(秒)" json:"duration,omitempty"`
NotifyCount int `gorm:"default:0;comment:通知次数" json:"notify_count"`
LastNotifyAt *time.Time `gorm:"comment:最近通知时间" json:"last_notify_at,omitempty"`
Acknowledged bool `gorm:"default:false;comment:是否已确认" json:"acknowledged"`
AcknowledgedBy uint `gorm:"comment:确认人ID" json:"acknowledged_by,omitempty"`
AcknowledgedAt *time.Time `gorm:"comment:确认时间" json:"acknowledged_at,omitempty"`
CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"`
// 关联关系
Task *SiteTask `gorm:"foreignKey:TaskID" json:"task,omitempty"`
}
// TableName 指定表名
func (SiteAlert) TableName() string {
return "monitor_site_alerts"
}
// SiteTaskCreateRequest 拨测任务创建请求
type SiteTaskCreateRequest struct {
Name string `json:"name" binding:"required,max=128"`
URL string `json:"url" binding:"required,max=512,url"`
TaskType SiteTaskType `json:"task_type" binding:"required"`
Method SiteTaskMethod `json:"method"`
Headers string `json:"headers"`
Body string `json:"body"`
Timeout int `json:"timeout"`
FollowRedirect bool `json:"follow_redirect"`
VerifySSL bool `json:"verify_ssl"`
ExpectedStatus int `json:"expected_status"`
ExpectedContent string `json:"expected_content"`
ExpectedLength int `json:"expected_length"`
Frequency int `json:"frequency"`
Nodes []string `json:"nodes"`
DNSResolver string `json:"dns_resolver"`
Description string `json:"description"`
}
// SiteTaskUpdateRequest 拨测任务更新请求
type SiteTaskUpdateRequest struct {
Name string `json:"name" binding:"max=128"`
URL string `json:"url" binding:"max=512,url"`
Method SiteTaskMethod `json:"method"`
Headers string `json:"headers"`
Body string `json:"body"`
Timeout int `json:"timeout"`
FollowRedirect bool `json:"follow_redirect"`
VerifySSL bool `json:"verify_ssl"`
ExpectedStatus int `json:"expected_status"`
ExpectedContent string `json:"expected_content"`
Frequency int `json:"frequency"`
Nodes []string `json:"nodes"`
Status SiteTaskStatus `json:"status"`
Description string `json:"description"`
}
// SiteTaskStatistics 拨测任务统计
type SiteTaskStatistics struct {
TaskID uint `json:"task_id"`
TotalChecks int64 `json:"total_checks"`
SuccessCount int64 `json:"success_count"`
FailCount int64 `json:"fail_count"`
TimeoutCount int64 `json:"timeout_count"`
SuccessRate float64 `json:"success_rate"`
AvgResponseTime float64 `json:"avg_response_time"`
MinResponseTime float64 `json:"min_response_time"`
MaxResponseTime float64 `json:"max_response_time"`
Last24hUptime float64 `json:"last_24h_uptime"`
Last7dUptime float64 `json:"last_7d_uptime"`
Last30dUptime float64 `json:"last_30d_uptime"`
}
// SiteResultQuery 拨测结果查询参数
type SiteResultQuery struct {
TaskID uint `json:"task_id" form:"task_id"`
Status string `json:"status" form:"status"`
StartTime int64 `json:"start_time" form:"start_time"`
EndTime int64 `json:"end_time" form:"end_time"`
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"
)