feat: v0.34 健康探针(Architecture B) + Agent v2.1 + 端口监控 + 告警联系人重构
- v0.31: AlertContact 重构(channel_type下拉+webhook_url替代三分立字段) - v0.32: Agent ProcessTop 增强(cmdline/user/cpu_pct/mem_pct) - v0.33: 端口监控 Tab(监听端口自动发现+down联动告警) - v0.34: 健康探针(Agent本地执行HTTP/TCP/Exec→结果随心跳上报) - Agent新增: ContextSwitch/FD/HealthProbe采集器 - 后端新增: health_probe handler + Agent model扩展 - HostMonitor新增Tab: 健康探针/端口监控
This commit is contained in:
1 parent
e1f75f0bc4
commit
d656520428
25 files changed
+1700
-619
No files matched your search
@@ -227,8 +227,9 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
h.writeHotMetric(c, "metrics:load1:"+req.Hostname, req.CPU.Load1, timestamp, ttl, reportTime)
|
||||
}
|
||||
|
||||
// 保存成功
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "ok"})
|
||||
// 保存成功 — 响应中携带探针配置给 Agent
|
||||
resp := h.makeProbeConfigResponse(req.Hostname, 200, "ok", req.ProbeResults)
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// writeHotMetric 将单个指标写入 Redis 有序集合并维护 TTL 和过期数据清理
|
||||
|
||||
@@ -33,12 +33,11 @@ func (h *AlertHandler) CreateContact(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
contact := models.AlertContact{
|
||||
Name: req.Name,
|
||||
Phone: req.Phone,
|
||||
Email: req.Email,
|
||||
Dingtalk: req.Dingtalk,
|
||||
Feishu: req.Feishu,
|
||||
Wechat: req.Wechat,
|
||||
Name: req.Name,
|
||||
Phone: req.Phone,
|
||||
Email: req.Email,
|
||||
ChannelType: req.ChannelType,
|
||||
WebhookURL: req.WebhookURL,
|
||||
}
|
||||
if err := h.db.Create(&contact).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败"})
|
||||
@@ -64,9 +63,8 @@ func (h *AlertHandler) UpdateContact(c *gin.Context) {
|
||||
if req.Name != "" { updates["name"] = req.Name }
|
||||
if req.Phone != "" { updates["phone"] = req.Phone }
|
||||
if req.Email != "" { updates["email"] = req.Email }
|
||||
if req.Dingtalk != "" { updates["dingtalk"] = req.Dingtalk }
|
||||
if req.Feishu != "" { updates["feishu"] = req.Feishu }
|
||||
if req.Wechat != "" { updates["wechat"] = req.Wechat }
|
||||
if req.ChannelType != "" { updates["channel_type"] = req.ChannelType }
|
||||
if req.WebhookURL != "" { updates["webhook_url"] = req.WebhookURL }
|
||||
h.db.Model(&contact).Updates(updates)
|
||||
h.db.First(&contact, id)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: contact})
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ────────────────────────────────────────
|
||||
// HealthProbe CRUD(健康探针配置管理)
|
||||
// ────────────────────────────────────────
|
||||
|
||||
// ListHealthProbes GET /api/v1/agent/health-probes
|
||||
func (h *AgentHandler) ListHealthProbes(c *gin.Context) {
|
||||
query := h.db.Model(&models.HealthProbe{})
|
||||
|
||||
if hostname := c.Query("hostname"); hostname != "" {
|
||||
query = query.Where("hostname = ?", hostname)
|
||||
}
|
||||
if enabled := c.Query("enabled"); enabled != "" {
|
||||
if enabled == "true" {
|
||||
query = query.Where("enabled = ?", true)
|
||||
} else if enabled == "false" {
|
||||
query = query.Where("enabled = ?", false)
|
||||
}
|
||||
}
|
||||
|
||||
var probes []models.HealthProbe
|
||||
query.Order("created_at DESC").Limit(500).Find(&probes)
|
||||
|
||||
if probes == nil {
|
||||
probes = []models.HealthProbe{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: probes,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateHealthProbe POST /api/v1/agent/health-probes
|
||||
func (h *AgentHandler) CreateHealthProbe(c *gin.Context) {
|
||||
var probe models.HealthProbe
|
||||
if err := c.ShouldBindJSON(&probe); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if probe.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "探针名称不能为空"})
|
||||
return
|
||||
}
|
||||
if probe.Hostname == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "目标主机不能为空"})
|
||||
return
|
||||
}
|
||||
if probe.ProbeType == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "探针类型不能为空"})
|
||||
return
|
||||
}
|
||||
// 验证探针类型配置
|
||||
switch probe.ProbeType {
|
||||
case models.ProbeTypeHTTP:
|
||||
if probe.HTTPURL == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "HTTP 探针必须填写 URL"})
|
||||
return
|
||||
}
|
||||
case models.ProbeTypeTCP:
|
||||
if probe.TCPHost == "" || probe.TCPPort == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "TCP 探针必须填写 Host 和 Port"})
|
||||
return
|
||||
}
|
||||
case models.ProbeTypeExec:
|
||||
if probe.ExecCommand == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "Exec 探针必须填写执行命令"})
|
||||
return
|
||||
}
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "不支持的探针类型: " + string(probe.ProbeType)})
|
||||
return
|
||||
}
|
||||
|
||||
probe.ID = 0
|
||||
probe.Enabled = true
|
||||
if probe.Timeout == 0 {
|
||||
probe.Timeout = 10
|
||||
}
|
||||
if probe.Interval == 0 {
|
||||
probe.Interval = 60
|
||||
}
|
||||
|
||||
if err := h.db.Create(&probe).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: probe})
|
||||
}
|
||||
|
||||
// UpdateHealthProbe PUT /api/v1/agent/health-probes/:id
|
||||
func (h *AgentHandler) UpdateHealthProbe(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var probe models.HealthProbe
|
||||
if err := h.db.First(&probe, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "探针不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var input models.HealthProbe
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
if input.Name != "" {
|
||||
updates["name"] = input.Name
|
||||
}
|
||||
if input.Hostname != "" {
|
||||
updates["hostname"] = input.Hostname
|
||||
}
|
||||
if input.ProbeType != "" {
|
||||
updates["probe_type"] = input.ProbeType
|
||||
}
|
||||
if input.HTTPURL != "" {
|
||||
updates["http_url"] = input.HTTPURL
|
||||
}
|
||||
if input.TCPHost != "" {
|
||||
updates["tcp_host"] = input.TCPHost
|
||||
}
|
||||
if input.TCPPort != 0 {
|
||||
updates["tcp_port"] = input.TCPPort
|
||||
}
|
||||
if input.ExecCommand != "" {
|
||||
updates["exec_command"] = input.ExecCommand
|
||||
}
|
||||
if input.Timeout > 0 {
|
||||
updates["timeout"] = input.Timeout
|
||||
}
|
||||
if input.Interval > 0 {
|
||||
updates["interval"] = input.Interval
|
||||
}
|
||||
// enabled 布尔处理:显式传入
|
||||
updates["enabled"] = input.Enabled
|
||||
if input.HTTPMethod != "" {
|
||||
updates["http_method"] = input.HTTPMethod
|
||||
}
|
||||
if input.HTTPHeaders != "" {
|
||||
updates["http_headers"] = input.HTTPHeaders
|
||||
}
|
||||
if input.HTTPExpectCode > 0 {
|
||||
updates["http_expect_code"] = input.HTTPExpectCode
|
||||
}
|
||||
if input.HTTPExpectBody != "" {
|
||||
updates["http_expect_body"] = input.HTTPExpectBody
|
||||
}
|
||||
if input.Description != "" {
|
||||
updates["description"] = input.Description
|
||||
}
|
||||
|
||||
if err := h.db.Model(&probe).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
h.db.First(&probe, id)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: probe})
|
||||
}
|
||||
|
||||
// DeleteHealthProbe DELETE /api/v1/agent/health-probes/:id
|
||||
func (h *AgentHandler) DeleteHealthProbe(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.db.Delete(&models.HealthProbe{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// GetHealthProbesForAgent Agent 拉取本机探针配置(Agent 调用,无需 JWT)
|
||||
// GET /api/v1/agent/health-probes/config?hostname=xxx
|
||||
func (h *AgentHandler) GetHealthProbesForAgent(c *gin.Context) {
|
||||
hostname := c.Query("hostname")
|
||||
if hostname == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "hostname 不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
var probes []models.HealthProbe
|
||||
h.db.Where("hostname = ? AND enabled = ?", hostname, true).
|
||||
Order("id ASC").
|
||||
Find(&probes)
|
||||
|
||||
if probes == nil {
|
||||
probes = []models.HealthProbe{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: probes,
|
||||
})
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────
|
||||
// ReceiveReport 增强:处理探针结果 + 返回配置
|
||||
// ────────────────────────────────────────
|
||||
|
||||
// processProbeResults 处理 Agent 上报的探针结果,更新探针状态
|
||||
func (h *AgentHandler) processProbeResults(hostname string, results []models.HealthProbeResult) {
|
||||
now := time.Now()
|
||||
for _, r := range results {
|
||||
updates := map[string]interface{}{
|
||||
"last_status": string(r.Status),
|
||||
"last_checked_at": now,
|
||||
"last_latency_ms": r.LatencyMs,
|
||||
}
|
||||
|
||||
if r.Status == models.ProbeStatusDown {
|
||||
// 失败时增加连续失败计数
|
||||
h.db.Model(&models.HealthProbe{}).
|
||||
Where("id = ? AND hostname = ?", r.ProbeID, hostname).
|
||||
UpdateColumn("fail_count", gorm.Expr("fail_count + 1"))
|
||||
} else {
|
||||
// 成功时清零
|
||||
updates["fail_count"] = 0
|
||||
}
|
||||
|
||||
h.db.Model(&models.HealthProbe{}).
|
||||
Where("id = ? AND hostname = ?", r.ProbeID, hostname).
|
||||
Updates(updates)
|
||||
|
||||
// ═══ 探针告警:Agent 本地探测 down → 平台创建告警事件 ═══
|
||||
if r.Status == models.ProbeStatusDown {
|
||||
// 检查是否存在未恢复的同探针告警
|
||||
var existingAlert models.AgentAlert
|
||||
h.db.Where("rule_name = ? AND hostname = ? AND metric = ? AND resolved = ?",
|
||||
r.Name, hostname, "health_probe", false).
|
||||
First(&existingAlert)
|
||||
|
||||
if existingAlert.ID == 0 {
|
||||
// 获取 fail_count 判断严重级别
|
||||
var probe models.HealthProbe
|
||||
if err := h.db.First(&probe, r.ProbeID).Error; err == nil {
|
||||
severity := "warning"
|
||||
if probe.FailCount >= 3 {
|
||||
severity = "critical"
|
||||
}
|
||||
alert := models.AgentAlert{
|
||||
RuleName: r.Name,
|
||||
Hostname: hostname,
|
||||
Metric: "health_probe",
|
||||
Severity: severity,
|
||||
Message: "健康探针 [" + r.Name + "] 检测异常: " + r.Message + " (类型=" + r.ProbeType + ", 延迟=" + strconv.Itoa(r.LatencyMs) + "ms)",
|
||||
Threshold: 0,
|
||||
Resolved: false,
|
||||
}
|
||||
h.db.Create(&alert)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 探针恢复正常:自动恢复告警
|
||||
h.db.Model(&models.AgentAlert{}).
|
||||
Where("rule_name = ? AND hostname = ? AND metric = ? AND resolved = ?",
|
||||
r.Name, hostname, "health_probe", false).
|
||||
Updates(map[string]interface{}{
|
||||
"resolved": true,
|
||||
"resolved_at": now,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// probeConfigResponse ReceiveReport 的响应结构(含探针配置)
|
||||
type probeConfigResponse struct {
|
||||
models.Response
|
||||
ProbeConfigs []models.HealthProbe `json:"probe_configs,omitempty"`
|
||||
}
|
||||
|
||||
// makeProbeConfigResponse 构建带探针配置的响应
|
||||
func (h *AgentHandler) makeProbeConfigResponse(hostname string, respCode int, respMsg string, probeResults []models.HealthProbeResult) probeConfigResponse {
|
||||
// 处理上报的探针结果
|
||||
if len(probeResults) > 0 {
|
||||
h.processProbeResults(hostname, probeResults)
|
||||
}
|
||||
|
||||
// 拉取该主机的新探针配置(供 Agent 下次执行)
|
||||
var probes []models.HealthProbe
|
||||
h.db.Where("hostname = ? AND enabled = ?", hostname, true).
|
||||
Order("id ASC").
|
||||
Find(&probes)
|
||||
if probes == nil {
|
||||
probes = []models.HealthProbe{}
|
||||
}
|
||||
|
||||
return probeConfigResponse{
|
||||
Response: models.Response{
|
||||
Code: respCode,
|
||||
Message: respMsg,
|
||||
},
|
||||
ProbeConfigs: probes,
|
||||
}
|
||||
}
|
||||
@@ -674,6 +674,8 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
||||
agentGroup.POST("/tasks/:task_id/result", taskHandler.SubmitResult)
|
||||
agentGroup.POST("/aggregate", agentHandler.AggregateMetrics)
|
||||
agentGroup.POST("/exec", agentInstallHandler.RemoteExec)
|
||||
// 健康探针:Agent 拉取本机配置(无需 JWT)
|
||||
agentGroup.GET("/health-probes/config", agentHandler.GetHealthProbesForAgent)
|
||||
}
|
||||
// Agent 数据查询 + 任务管理(需要 JWT)
|
||||
agentQuery := authorized.Group("/agent")
|
||||
@@ -702,6 +704,12 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
||||
agentQuery.POST("/uninstall", agentInstallHandler.UninstallAgent)
|
||||
agentQuery.POST("/batch-uninstall", agentInstallHandler.BatchUninstallAgent)
|
||||
agentQuery.POST("/batch-upgrade", agentInstallHandler.BatchUpgradeAgent)
|
||||
|
||||
// 健康探针管理(CRUD)
|
||||
agentQuery.GET("/health-probes", agentHandler.ListHealthProbes)
|
||||
agentQuery.POST("/health-probes", agentHandler.CreateHealthProbe)
|
||||
agentQuery.PUT("/health-probes/:id", agentHandler.UpdateHealthProbe)
|
||||
agentQuery.DELETE("/health-probes/:id", agentHandler.DeleteHealthProbe)
|
||||
}
|
||||
|
||||
// Agent 二进制下载(无需鉴权,Agent 升级时使用)
|
||||
|
||||
@@ -87,7 +87,8 @@ type AgentReportRequest struct {
|
||||
ProcessTop []ProcessMetric `json:"process_top,omitempty"`
|
||||
FD *FDMetric `json:"fd,omitempty"`
|
||||
ContextSwitches *CSMetric `json:"context_switches,omitempty"`
|
||||
Heartbeat HeartbeatMetric `json:"heartbeat"`
|
||||
Heartbeat HeartbeatMetric `json:"heartbeat"`
|
||||
ProbeResults []HealthProbeResult `json:"probe_results,omitempty"` // v2.1 健康探针结果
|
||||
}
|
||||
|
||||
// ──── v2.0 新增上报类型 ────
|
||||
@@ -101,6 +102,10 @@ type SwapMetric struct {
|
||||
type ProcessMetric struct {
|
||||
PID int `json:"pid"`
|
||||
Name string `json:"name"`
|
||||
Cmdline string `json:"cmdline"`
|
||||
User string `json:"user"`
|
||||
CPUPct float64 `json:"cpu_pct"`
|
||||
MemPct float64 `json:"mem_pct"`
|
||||
MemoryMB float64 `json:"memory_mb"`
|
||||
FDCount int `json:"fd_count"`
|
||||
Threads int `json:"threads"`
|
||||
|
||||
@@ -400,16 +400,15 @@ type AcknowledgeAlertRequest struct {
|
||||
// ============================================
|
||||
|
||||
type AlertContact struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
|
||||
Name string `gorm:"type:varchar(128);not null;comment:联系人姓名" json:"name"`
|
||||
Phone string `gorm:"type:varchar(32);comment:手机号" json:"phone,omitempty"`
|
||||
Email string `gorm:"type:varchar(128);comment:邮箱" json:"email,omitempty"`
|
||||
Dingtalk string `gorm:"type:varchar(256);comment:钉钉webhook地址" json:"dingtalk,omitempty"`
|
||||
Feishu string `gorm:"type:varchar(256);comment:飞书webhook地址" json:"feishu,omitempty"`
|
||||
Wechat string `gorm:"type:varchar(256);comment:企业微信webhook地址" json:"wechat,omitempty"`
|
||||
CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"`
|
||||
ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
|
||||
Name string `gorm:"type:varchar(128);not null;comment:联系人姓名" json:"name"`
|
||||
Phone string `gorm:"type:varchar(32);comment:手机号" json:"phone,omitempty"`
|
||||
Email string `gorm:"type:varchar(128);comment:邮箱" json:"email,omitempty"`
|
||||
ChannelType string `gorm:"type:varchar(32);default:'feishu';comment:渠道类型: feishu/dingtalk/wechat/webhook" json:"channel_type,omitempty"`
|
||||
WebhookURL string `gorm:"type:varchar(512);comment:Webhook地址" json:"webhook_url,omitempty"`
|
||||
CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"`
|
||||
}
|
||||
|
||||
func (AlertContact) TableName() string {
|
||||
@@ -436,22 +435,20 @@ func (AlertContactGroup) TableName() string {
|
||||
|
||||
// CreateContactRequest 创建联系人请求
|
||||
type CreateContactRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Dingtalk string `json:"dingtalk,omitempty"`
|
||||
Feishu string `json:"feishu,omitempty"`
|
||||
Wechat string `json:"wechat,omitempty"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
ChannelType string `json:"channel_type" binding:"required,oneof=feishu dingtalk wechat webhook"`
|
||||
WebhookURL string `json:"webhook_url" binding:"required"`
|
||||
}
|
||||
|
||||
// UpdateContactRequest 更新联系人请求
|
||||
type UpdateContactRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Dingtalk string `json:"dingtalk,omitempty"`
|
||||
Feishu string `json:"feishu,omitempty"`
|
||||
Wechat string `json:"wechat,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
ChannelType string `json:"channel_type,omitempty" binding:"omitempty,oneof=feishu dingtalk wechat webhook"`
|
||||
WebhookURL string `json:"webhook_url,omitempty"`
|
||||
}
|
||||
|
||||
// CreateContactGroupRequest 创建联系组请求
|
||||
|
||||
@@ -353,3 +353,73 @@ type HostGroup struct {
|
||||
func (HostGroup) TableName() string {
|
||||
return "asset_host_groups"
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HealthProbe 健康探针配置(Agent 本地执行)
|
||||
// ============================================
|
||||
|
||||
// ProbeType 探针类型
|
||||
type ProbeType string
|
||||
|
||||
const (
|
||||
ProbeTypeHTTP ProbeType = "http_get" // HTTP GET 探测
|
||||
ProbeTypeTCP ProbeType = "tcp_socket" // TCP 端口连接探测
|
||||
ProbeTypeExec ProbeType = "exec_command" // 命令执行探测
|
||||
)
|
||||
|
||||
// ProbeStatus 探针结果状态
|
||||
type ProbeStatus string
|
||||
|
||||
const (
|
||||
ProbeStatusUp ProbeStatus = "up" // 健康
|
||||
ProbeStatusDown ProbeStatus = "down" // 异常
|
||||
)
|
||||
|
||||
// HealthProbe 健康探针配置表
|
||||
// 存储在平台 DB,Agent 拉取配置后在目标主机本地执行,结果随心跳上报
|
||||
type HealthProbe struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"type:varchar(128);not null;index" json:"name"`
|
||||
Hostname string `gorm:"type:varchar(255);index;not null" json:"hostname"`
|
||||
ProbeType ProbeType `gorm:"type:varchar(32);not null" json:"probe_type"`
|
||||
// HTTP 探针配置
|
||||
HTTPURL string `gorm:"type:varchar(512)" json:"http_url,omitempty"`
|
||||
HTTPMethod string `gorm:"type:varchar(10);default:GET" json:"http_method,omitempty"`
|
||||
HTTPHeaders string `gorm:"type:text" json:"http_headers,omitempty"`
|
||||
HTTPExpectCode int `gorm:"default:200" json:"http_expect_code,omitempty"`
|
||||
HTTPExpectBody string `gorm:"type:varchar(256)" json:"http_expect_body,omitempty"`
|
||||
// TCP 探针配置
|
||||
TCPHost string `gorm:"type:varchar(255)" json:"tcp_host,omitempty"`
|
||||
TCPPort int `json:"tcp_port,omitempty"`
|
||||
// Exec 探针配置
|
||||
ExecCommand string `gorm:"type:text" json:"exec_command,omitempty"`
|
||||
// 通用配置
|
||||
Timeout int `gorm:"default:10" json:"timeout"`
|
||||
Interval int `gorm:"default:60" json:"interval"`
|
||||
Enabled bool `gorm:"default:true;index" json:"enabled"`
|
||||
LastStatus ProbeStatus `gorm:"type:varchar(8);default:''" json:"last_status,omitempty"`
|
||||
LastCheckedAt *time.Time `json:"last_checked_at,omitempty"`
|
||||
LastLatencyMs int `json:"last_latency_ms"`
|
||||
FailCount int `json:"fail_count"`
|
||||
Description string `gorm:"type:varchar(256)" json:"description,omitempty"`
|
||||
CreatedAt time.Time `gorm:"not null" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"not null" json:"updated_at"`
|
||||
DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (HealthProbe) TableName() string {
|
||||
return "monitor_health_probes"
|
||||
}
|
||||
|
||||
// HealthProbeResult Agent 上报的探针执行结果(作为心跳的一部分)
|
||||
type HealthProbeResult struct {
|
||||
ProbeID uint `json:"probe_id"`
|
||||
Name string `json:"name"`
|
||||
ProbeType string `json:"probe_type"`
|
||||
Status ProbeStatus `json:"status"` // up / down
|
||||
LatencyMs int `json:"latency_ms"` // 响应延迟(ms)
|
||||
StatusCode int `json:"status_code,omitempty"` // HTTP 状态码
|
||||
Message string `json:"message,omitempty"` // 错误/附加信息
|
||||
}
|
||||
|
||||
@@ -133,6 +133,7 @@ func AutoMigrate(db *gorm.DB) error {
|
||||
&models.AlertUpgrade{}, // 告警升级策略 // Logan.Li
|
||||
&models.AlertUpgradeRecord{}, // 告警升级记录 // Logan.Li
|
||||
&models.AlertSilence{}, // 告警静默 // Logan.Li
|
||||
&models.HealthProbe{}, // 健康探针配置 // Logan.Li
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
|
||||
Reference in new issue
Block a user