From 24fa17c2a804e1e14be4d6d7d585515fa9cc5df1 Mon Sep 17 00:00:00 2001 From: attacker Date: Thu, 18 Jun 2026 11:52:10 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9B=91=E6=8E=A7=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20=E2=80=94=20=E6=A8=A1=E5=9E=8B=E5=B1=82=20?= =?UTF-8?q?+=20Prometheus/Zabbix/SNMP=20pkg=20+=20Dockerfile=E5=8D=87?= =?UTF-8?q?=E7=BA=A7Go1.24?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 整合 /srv/监控架构优化方案 核心代码: 后端模型(4个新文件): - monitor.go: MonitorTarget(统一监控目标) + MonitorMetric(指标定义) + MetricValue(时序数据) + MetricHistory - snmp.go: SNMPDevice + SNMPTemplate + OID模板管理 - site.go: SiteTask(拨测任务) + SiteResult(拨测结果) - alert_v2.go: 增强告警(多条件规则/模板/升级策略/静默) 后端 pkg(3个包): - pkg/prometheus/client.go (531行): PromQL查询/Range/Labels/Rules - pkg/zabbix/client.go: Zabbix API (Host/Item/Trigger/History) - pkg/snmp/collector.go (621行): gosnmp Walk/BulkWalk/自动发现 基础设施: - Dockerfile: Go 1.21→1.24 (gosnmp依赖) - Agent安装弹框: SSH一键安装Tab + server上下文 - 服务器列表操作栏: 安装按钮条件显示/隐藏 Handler层因类型不兼容暂时跳过,模型和客户端包已编译通过。 --- backend/Dockerfile | 8 +- backend/go.mod | 13 +- backend/go.sum | 17 +- backend/internal/models/alert_v2.go | 400 +++++++++ backend/internal/models/monitor.go | 355 ++++++++ backend/internal/models/site.go | 266 ++++++ backend/internal/models/snmp.go | 252 ++++++ backend/pkg/prometheus/client.go | 531 ++++++++++++ backend/pkg/snmp/collector.go | 621 ++++++++++++++ backend/pkg/zabbix/client.go | 756 ++++++++++++++++++ frontend/src/components/agent/AgentWizard.tsx | 98 ++- .../src/components/server/ServerManager.tsx | 165 ++-- .../src/components/server/ServerTableRow.tsx | 43 +- 13 files changed, 3427 insertions(+), 98 deletions(-) create mode 100644 backend/internal/models/alert_v2.go create mode 100644 backend/internal/models/monitor.go create mode 100644 backend/internal/models/site.go create mode 100644 backend/internal/models/snmp.go create mode 100644 backend/pkg/prometheus/client.go create mode 100644 backend/pkg/snmp/collector.go create mode 100644 backend/pkg/zabbix/client.go diff --git a/backend/Dockerfile b/backend/Dockerfile index 5331819..a3f4588 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,13 +1,13 @@ # ============================================================ # AIOps Backend + Agent 多阶段 Dockerfile -# Stage 1: 编译后端 (Go 1.21) +# Stage 1: 编译后端 (Go 1.24) # Stage 2: 交叉编译 Agent (Go 1.24, linux-amd64/arm64 + darwin-arm64) # Stage 3: 运行时 (Alpine) # 署名 Logan.Li # ============================================================ # ==================== Stage 1: Backend Builder ==================== -FROM golang:1.21-alpine AS builder +FROM golang:1.24-alpine AS builder WORKDIR /app @@ -17,9 +17,7 @@ RUN apk add --no-cache git ca-certificates tzdata ENV TZ=Asia/Shanghai ENV GOPROXY=https://goproxy.cn,direct -# 跳过 toolchain 校验(GOTOOLCHAIN 自动下载 Go 1.22 给 Redka 用)// Logan.Li ENV GONOSUMCHECK=* -ENV GOTOOLCHAIN=go1.22.2 # 复制后端 go.mod(利用 Docker 层缓存) COPY backend/go.mod backend/go.sum ./ @@ -28,7 +26,7 @@ RUN go mod download # 复制后端源码 COPY backend/ ./ -# 构建后端二进制(Redka 需要 Go 1.22,由 GOTOOLCHAIN 自动下载) +# 构建后端二进制 RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ -ldflags="-w -s" \ -o ezops ./cmd/ diff --git a/backend/go.mod b/backend/go.mod index 4b1d050..8d2be4a 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,8 +1,6 @@ module ezops -go 1.22 - -toolchain go1.22.2 +go 1.24.0 require ( github.com/gin-gonic/gin v1.9.1 @@ -10,11 +8,12 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 + github.com/gosnmp/gosnmp v1.35.0 github.com/nalgeon/redka v0.2.0 github.com/redis/go-redis/v9 v9.7.0 github.com/spf13/viper v1.18.2 go.uber.org/zap v1.26.0 - golang.org/x/crypto v0.31.0 + golang.org/x/crypto v0.46.0 gorm.io/driver/mysql v1.5.2 gorm.io/driver/postgres v1.5.4 gorm.io/driver/sqlite v1.6.0 @@ -64,9 +63,9 @@ require ( go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.32.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/backend/go.sum b/backend/go.sum index 75f62f9..dd2ee7a 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -59,6 +59,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosnmp/gosnmp v1.35.0 h1:EuWWNPxTCdAUx2/NbQcSa3WdNxjzpy4Phv57b4MWpJM= +github.com/gosnmp/gosnmp v1.35.0/go.mod h1:2AvKZ3n9aEl5TJEo/fFmf/FGO4Nj4cVeEc5yuk88CYc= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -169,8 +171,9 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -188,8 +191,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -209,8 +213,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -219,8 +224,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -229,8 +235,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/backend/internal/models/alert_v2.go b/backend/internal/models/alert_v2.go new file mode 100644 index 0000000..156ff0c --- /dev/null +++ b/backend/internal/models/alert_v2.go @@ -0,0 +1,400 @@ +package models + +import ( + "time" +) + +// ============================================ +// AlertRuleV2 增强告警规则 +// ============================================ + +// AlertRuleV2Status 告警规则状态 +type AlertRuleV2Status string + +const ( + AlertRuleV2Enabled AlertRuleV2Status = "enabled" // 启用 + AlertRuleV2Disabled AlertRuleV2Status = "disabled" // 禁用 +) + +// AlertSeverity 告警级别 +type AlertSeverity string + +const ( + AlertSeverityCritical AlertSeverity = "critical" // 严重 + AlertSeverityWarning AlertSeverity = "warning" // 警告 + AlertSeverityNotice AlertSeverity = "notice" // 通知 + AlertSeverityInfo AlertSeverity = "info" // 信息 +) + +// AggFunction 聚合函数 +type AggFunction string + +const ( + AggFunctionAvg AggFunction = "avg" // 平均值 + AggFunctionMin AggFunction = "min" // 最小值 + AggFunctionMax AggFunction = "max" // 最大值 + AggFunctionSum AggFunction = "sum" // 求和 + AggFunctionCount AggFunction = "count" // 计数 + AggFunctionLast AggFunction = "last" // 最新值 + AggFunctionP95 AggFunction = "p95" // 95分位 + AggFunctionP99 AggFunction = "p99" // 99分位 +) + +// Operator 比较运算符 +type Operator string + +const ( + OperatorGT Operator = ">" // 大于 + OperatorGTE Operator = ">=" // 大于等于 + OperatorLT Operator = "<" // 小于 + OperatorLTE Operator = "<=" // 小于等于 + OperatorEQ Operator = "==" // 等于 + OperatorNE Operator = "!=" // 不等于 +) + +// AlertRuleV2 增强告警规则表 +// 支持多条件组合、聚合函数、静默期等高级特性 +type AlertRuleV2 struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;comment:规则名称" json:"name"` + Description string `gorm:"type:varchar(256);comment:规则描述" json:"description,omitempty"` + Severity AlertSeverity `gorm:"type:varchar(16);not null;comment:告警级别" json:"severity"` + Status AlertRuleV2Status `gorm:"type:varchar(16);default:'enabled';comment:规则状态" json:"status"` + // 关联目标 + TargetType string `gorm:"type:varchar(32);comment:目标类型" json:"target_type,omitempty"` + TargetID uint `gorm:"index;comment:目标ID(0表示全部)" json:"target_id,omitempty"` + TargetTags string `gorm:"type:varchar(256);comment:目标标签筛选" json:"target_tags,omitempty"` + // 触发条件(JSON存储支持多条件) + Conditions string `gorm:"type:text;comment:触发条件JSON数组" json:"conditions,omitempty"` + // 聚合配置 + AggWindow int `gorm:"default:300;comment:聚合窗口(秒)" json:"agg_window"` + AggFunction AggFunction `gorm:"type:varchar(16);default:'avg';comment:聚合函数" json:"agg_function"` + // 恢复配置 + RecoverCount int `gorm:"default:1;comment:恢复次数(连续N次低于阈值触发恢复)" json:"recover_count"` + RecoverWindow int `gorm:"default:300;comment:恢复检查窗口(秒)" json:"recover_window"` + // 执行配置 + Interval int `gorm:"default:60;comment:评估间隔(秒)" json:"interval"` + CheckTimes int `gorm:"default:1;comment:连续触发次数" json:"check_times"` + NotifyMode string `gorm:"type:varchar(16);default:'every';comment:通知模式: every/once/upgrade" json:"notify_mode"` + // 通知渠道 + ChannelIDs string `gorm:"type:varchar(256);comment:通知渠道ID列表JSON" json:"channel_ids,omitempty"` + TemplateID uint `gorm:"comment:通知模板ID" json:"template_id,omitempty"` + // 升级策略 + UpgradeID uint `gorm:"comment:升级策略ID" json:"upgrade_id,omitempty"` + // 静默策略 + SilenceID uint `gorm:"comment:静默规则ID" json:"silence_id,omitempty"` + // 标签和注释 + Labels string `gorm:"type:varchar(512);comment:标签JSON" json:"labels,omitempty"` + Annotations string `gorm:"type:text;comment:注释JSON(用于通知内容模板)" json:"annotations,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"` +} + +// TableName 指定表名 +func (AlertRuleV2) TableName() string { + return "alert_rules_v2" +} + +// AlertCondition 告警条件定义 +type AlertCondition struct { + Metric string `json:"metric"` // 指标名称 + Operator Operator `json:"operator"` // 比较运算符 + Threshold float64 `json:"threshold"` // 阈值 + Unit string `json:"unit"` // 单位 + Index string `json:"index"` // 实例索引 + LogicOp string `json:"logic_op"` // 逻辑关系: AND/OR + AggFunc AggFunction `json:"agg_func"` // 条件级聚合函数 + Window int `json:"window"` // 条件级聚合窗口 +} + +// AlertConditionGroup 告警条件组 +type AlertConditionGroup struct { + Conditions []AlertCondition `json:"conditions"` // 条件列表 + LogicOp string `json:"logic_op"` // 组间逻辑: AND/OR +} + +// RecoverConfig 告警恢复配置 +type RecoverConfig struct { + Count int `json:"count"` // 连续恢复次数 + Window int `json:"window"` // 恢复检查窗口(秒) +} + +// NotifyMode 通知模式 +type NotifyMode string + +const ( + NotifyModeEvery NotifyMode = "every" // 每次都通知 + NotifyModeOnce NotifyMode = "once" // 仅通知一次 + NotifyModeUpgrade NotifyMode = "upgrade" // 升级通知 +) + +// ============================================ +// AlertNotifyTemplate 告警通知模板 +// ============================================ + +// AlertNotifyTemplate 告警通知模板表 +// 定义告警通知的内容模板,支持变量替换 +type AlertNotifyTemplate struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;comment:模板名称" json:"name"` + Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` + ChannelType string `gorm:"type:varchar(32);not null;comment:适用渠道类型: email/sms/webhook/dingtalk/wecom/feishu" json:"channel_type"` + // 模板内容 + TitleTemplate string `gorm:"type:varchar(256);comment:标题模板" json:"title_template,omitempty"` + BodyTemplate string `gorm:"type:text;comment:正文模板" json:"body_template,omitempty"` + // 变量定义 + Variables string `gorm:"type:text;comment:可用变量说明JSON" json:"variables,omitempty"` + // 恢复通知模板 + RecoverTitleTemplate string `gorm:"type:varchar(256);comment:恢复标题模板" json:"recover_title_template,omitempty"` + RecoverBodyTemplate string `gorm:"type:text;comment:恢复正文模板" json:"recover_body_template,omitempty"` + IsDefault bool `gorm:"default:false;comment:是否默认模板" json:"is_default"` + Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` + 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"` +} + +// TableName 指定表名 +func (AlertNotifyTemplate) TableName() string { + return "alert_notify_templates" +} + +// TemplateVariable 模板变量定义 +type TemplateVariable struct { + Name string `json:"name"` // 变量名 + Description string `json:"description"` // 描述 + Example string `json:"example"` // 示例 +} + +// DefaultTemplateVariables 默认模板变量 +var DefaultTemplateVariables = []TemplateVariable{ + {Name: "{{.RuleName}}", Description: "规则名称", Example: "CPU使用率告警"}, + {Name: "{{.TargetName}}", Description: "目标名称", Example: "WebServer-01"}, + {Name: "{{.TargetIP}}", Description: "目标IP", Example: "192.168.1.10"}, + {Name: "{{.Severity}}", Description: "告警级别", Example: "critical"}, + {Name: "{{.MetricName}}", Description: "指标名称", Example: "cpu_usage"}, + {Name: "{{.Value}}", Description: "当前值", Example: "95.2"}, + {Name: "{{.Threshold}}", Description: "阈值", Example: "90"}, + {Name: "{{.Operator}}", Description: "运算符", Example: ">"}, + {Name: "{{.Time}}", Description: "触发时间", Example: "2024-01-15 10:30:00"}, + {Name: "{{.Duration}}", Description: "持续时间", Example: "5m"}, + {Name: "{{.Description}}", Description: "规则描述", Example: "CPU使用率超过阈值"}, + {Name: "{{.URL}}", Description: "详情链接", Example: "https://ezops.io/alerts/123"}, +} + +// ============================================ +// AlertUpgrade 告警升级策略 +// ============================================ + +// AlertUpgrade 告警升级策略表 +// 定义告警在不同阶段的升级策略 +type AlertUpgrade struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;comment:策略名称" json:"name"` + Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` + // 升级规则(JSON存储支持多级升级) + Rules string `gorm:"type:text;comment:升级规则JSON数组" json:"rules,omitempty"` + // 执行配置 + MaxLevel int `gorm:"default:3;comment:最大升级级别" json:"max_level"` + IsDefault bool `gorm:"default:false;comment:是否默认策略" json:"is_default"` + Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` + 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"` +} + +// TableName 指定表名 +func (AlertUpgrade) TableName() string { + return "alert_upgrades" +} + +// AlertUpgradeRule 告警升级规则 +type AlertUpgradeRule struct { + Level int `json:"level"` // 升级级别 + Duration int `json:"duration"` // 触发升级的持续时长(秒) + Severity string `json:"severity"` // 升级后的级别 + ChannelIDs []string `json:"channel_ids"` // 升级后通知渠道 + NotifyUsers []uint `json:"notify_users"` // 升级后通知用户 + RepeatInterval int `json:"repeat_interval"` // 重复通知间隔(秒) +} + +// AlertUpgradeRecord 告警升级记录 +type AlertUpgradeRecord struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + AlertID uint `gorm:"not null;index;comment:告警ID" json:"alert_id"` + RuleID uint `gorm:"not null;comment:规则ID" json:"rule_id"` + FromLevel int `gorm:"comment:原级别" json:"from_level"` + ToLevel int `gorm:"comment:升级后级别" json:"to_level"` + UpgradedAt time.Time `gorm:"not null;comment:升级时间" json:"upgraded_at"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` +} + +// TableName 指定表名 +func (AlertUpgradeRecord) TableName() string { + return "alert_upgrade_records" +} + +// ============================================ +// AlertSilence 告警静默规则 +// ============================================ + +// AlertSilenceType 静默类型 +type AlertSilenceType string + +const ( + AlertSilenceOneTime AlertSilenceType = "onetime" // 一次性静默 + AlertSilenceDaily AlertSilenceType = "daily" // 每日重复 + AlertSilenceWeekly AlertSilenceType = "weekly" // 每周重复 +) + +// AlertSilence 告警静默规则表 +// 定义告警静默策略,支持一次性、周期性静默 +type AlertSilence struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;comment:静默规则名称" json:"name"` + Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` + // 匹配条件 + RuleIDs string `gorm:"type:varchar(256);comment:匹配的告警规则ID列表JSON" json:"rule_ids,omitempty"` + TargetIDs string `gorm:"type:varchar(256);comment:匹配的目标ID列表JSON" json:"target_ids,omitempty"` + Matchers string `gorm:"type:text;comment:标签匹配条件JSON" json:"matchers,omitempty"` + // 时间配置 + SilenceType AlertSilenceType `gorm:"type:varchar(16);default:'onetime';comment:静默类型" json:"silence_type"` + StartTime time.Time `gorm:"not null;comment:开始时间" json:"start_time"` + EndTime time.Time `gorm:"not null;comment:结束时间" json:"end_time"` + Timezone string `gorm:"type:varchar(32);default:'Asia/Shanghai';comment:时区" json:"timezone"` + // 周期性配置 + Weekdays string `gorm:"type:varchar(64);comment:生效星期(0-6)JSON数组" json:"weekdays,omitempty"` + // 状态 + Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` + 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"` +} + +// TableName 指定表名 +func (AlertSilence) TableName() string { + return "alert_silences" +} + +// AlertSilenceMatcher 静默匹配条件 +type AlertSilenceMatcher struct { + Name string `json:"name"` // 标签名 + Value string `json:"value"` // 标签值 + IsRegex bool `json:"is_regex"` // 是否正则匹配 +} + +// AlertSilenceCheckResult 静默检查结果 +type AlertSilenceCheckResult struct { + IsSilenced bool `json:"is_silenced"` + SilenceID uint `json:"silence_id,omitempty"` + SilenceName string `json:"silence_name,omitempty"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` +} + +// ============================================ +// AlertEventV2 增强告警事件 +// ============================================ + +// AlertEventV2Status 告警事件状态 +type AlertEventV2Status string + +const ( + AlertEventV2Firing AlertEventV2Status = "firing" // 触发中 + AlertEventV2Resolved AlertEventV2Status = "resolved" // 已恢复 + AlertEventV2Silenced AlertEventV2Status = "silenced" // 已静默 +) + +// AlertEventV2 增强告警事件表 +// 记录告警触发和恢复的全过程 +type AlertEventV2 struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + RuleID uint `gorm:"not null;index:idx_rule_time;comment:规则ID" json:"rule_id"` + TargetID uint `gorm:"not null;index;comment:目标ID" json:"target_id"` + TargetName string `gorm:"type:varchar(128);comment:目标名称" json:"target_name,omitempty"` + TargetIP string `gorm:"type:varchar(64);comment:目标IP" json:"target_ip,omitempty"` + Severity AlertSeverity `gorm:"type:varchar(16);not null;index;comment:告警级别" json:"severity"` + Status AlertEventV2Status `gorm:"type:varchar(16);default:'firing';index;comment:事件状态" json:"status"` + Title string `gorm:"type:varchar(256);not null;comment:告警标题" json:"title"` + Message string `gorm:"type:text;comment:告警详情" json:"message,omitempty"` + // 触发值信息 + MetricName string `gorm:"type:varchar(64);comment:指标名称" json:"metric_name,omitempty"` + Value float64 `gorm:"comment:触发值" json:"value,omitempty"` + Threshold float64 `gorm:"comment:阈值" json:"threshold,omitempty"` + Operator string `gorm:"type:varchar(8);comment:运算符" json:"operator,omitempty"` + Unit string `gorm:"type:varchar(32);comment:单位" json:"unit,omitempty"` + // 标签和注释 + Labels string `gorm:"type:varchar(512);comment:标签JSON" json:"labels,omitempty"` + Annotations string `gorm:"type:text;comment:注释JSON" json:"annotations,omitempty"` + // 时间信息 + StartedAt time.Time `gorm:"not null;index:idx_rule_time;comment:触发时间" json:"started_at"` + ResolvedAt *time.Time `gorm:"comment:恢复时间" json:"resolved_at,omitempty"` + Duration int `gorm:"comment:持续时间(秒)" json:"duration,omitempty"` + LastEvalAt time.Time `gorm:"comment:最后评估时间" json:"last_eval_at,omitempty"` + NotifyCount int `gorm:"default:0;comment:通知次数" json:"notify_count"` + LastNotifyAt *time.Time `gorm:"comment:最近通知时间" json:"last_notify_at,omitempty"` + UpgradeLevel int `gorm:"default:0;comment:当前升级级别" json:"upgrade_level"` + // 确认信息 + 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"` + AckComment string `gorm:"type:varchar(256);comment:确认备注" json:"ack_comment,omitempty"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` +} + +// TableName 指定表名 +func (AlertEventV2) TableName() string { + return "alert_events_v2" +} + +// AlertRuleV2CreateRequest 告警规则创建请求 +type AlertRuleV2CreateRequest struct { + Name string `json:"name" binding:"required,max=128"` + Description string `json:"description"` + Severity AlertSeverity `json:"severity" binding:"required"` + TargetType string `json:"target_type"` + TargetID uint `json:"target_id"` + TargetTags string `json:"target_tags"` + Conditions []AlertCondition `json:"conditions" binding:"required,min=1"` + AggWindow int `json:"agg_window"` + AggFunction AggFunction `json:"agg_function"` + RecoverCount int `json:"recover_count"` + Interval int `json:"interval"` + CheckTimes int `json:"check_times"` + NotifyMode string `json:"notify_mode"` + ChannelIDs []string `json:"channel_ids"` + TemplateID uint `json:"template_id"` + UpgradeID uint `json:"upgrade_id"` + SilenceID uint `json:"silence_id"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` +} + +// AlertRuleV2UpdateRequest 告警规则更新请求 +type AlertRuleV2UpdateRequest struct { + Name string `json:"name" binding:"max=128"` + Description string `json:"description"` + Severity AlertSeverity `json:"severity"` + Status AlertRuleV2Status `json:"status"` + TargetType string `json:"target_type"` + TargetID uint `json:"target_id"` + TargetTags string `json:"target_tags"` + Conditions []AlertCondition `json:"conditions"` + AggWindow int `json:"agg_window"` + AggFunction AggFunction `json:"agg_function"` + RecoverCount int `json:"recover_count"` + Interval int `json:"interval"` + CheckTimes int `json:"check_times"` + NotifyMode string `json:"notify_mode"` + ChannelIDs []string `json:"channel_ids"` + TemplateID uint `json:"template_id"` + UpgradeID uint `json:"upgrade_id"` + SilenceID uint `json:"silence_id"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` +} diff --git a/backend/internal/models/monitor.go b/backend/internal/models/monitor.go new file mode 100644 index 0000000..9dbd4bc --- /dev/null +++ b/backend/internal/models/monitor.go @@ -0,0 +1,355 @@ +package models + +import ( + "time" +) + +// ============================================ +// MonitorTarget 监控目标(主机/中间件/设备/站点) +// ============================================ + +// TargetType 监控目标类型 +type TargetType string + +const ( + TargetTypeHost TargetType = "host" // 主机 + TargetTypeMiddleware TargetType = "middleware" // 中间件 + TargetTypeDevice TargetType = "device" // 网络设备 + TargetTypeSite TargetType = "site" // 站点 + TargetTypeDatabase TargetType = "database" // 数据库 + TargetTypeContainer TargetType = "container" // 容器 +) + +// TargetStatus 监控目标状态 +type TargetStatus string + +const ( + TargetStatusOnline TargetStatus = "online" // 在线 + TargetStatusOffline TargetStatus = "offline" // 离线 + TargetStatusUnknown TargetStatus = "unknown" // 未知 +) + +// MonitorTarget 监控目标表 +// 存储所有被监控的对象,包括主机、中间件、网络设备、站点等 +type MonitorTarget struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;index;comment:目标名称" json:"name"` + Type TargetType `gorm:"type:varchar(32);not null;index;comment:目标类型" json:"type"` + Status TargetStatus `gorm:"type:varchar(16);default:'unknown';index;comment:状态" json:"status"` + IP string `gorm:"type:varchar(64);comment:IP地址" json:"ip,omitempty"` + Port int `gorm:"default:0;comment:端口" json:"port,omitempty"` + AgentID string `gorm:"type:varchar(64);index;comment:Agent标识" json:"agent_id,omitempty"` + OS string `gorm:"type:varchar(64);comment:操作系统" json:"os,omitempty"` + OSVersion string `gorm:"type:varchar(64);comment:OS版本" json:"os_version,omitempty"` + Arch string `gorm:"type:varchar(16);comment:系统架构" json:"arch,omitempty"` + Kernel string `gorm:"type:varchar(64);comment:内核版本" json:"kernel,omitempty"` + HostGroupID uint `gorm:"index;comment:主机组ID" json:"host_group_id,omitempty"` + Tags string `gorm:"type:varchar(512);comment:标签JSON" json:"tags,omitempty"` + CollectMode string `gorm:"type:varchar(16);default:'agent';comment:采集模式:agent/snmp/api" json:"collect_mode"` + SNMPConfig string `gorm:"type:text;comment:SNMP配置JSON" json:"snmp_config,omitempty"` + APIConfig string `gorm:"type:text;comment:API配置JSON" json:"api_config,omitempty"` + Address string `gorm:"type:varchar(255);comment:监控地址(IP或域名)" json:"address,omitempty"` + Protocol string `gorm:"type:varchar(32);comment:协议类型(http/https/tcp/udp/icmp/snmp)" json:"protocol,omitempty"` + Timeout int `gorm:"default:30;comment:超时时间(秒)" json:"timeout,omitempty"` + Config string `gorm:"type:text;comment:通用配置JSON" json:"config,omitempty"` + Interval int `gorm:"default:60;comment:采集间隔(秒)" json:"interval"` + Labels string `gorm:"type:varchar(512);comment:标签JSON" json:"labels,omitempty"` + Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` + CreatedAt time.Time `gorm:"not null;index;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` + DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` + LastSeenAt *time.Time `gorm:"index;comment:最后在线时间" json:"last_seen_at,omitempty"` + + // 关联关系 + HostGroup *HostGroup `gorm:"foreignKey:HostGroupID" json:"host_group,omitempty"` +} + +// TableName 指定表名 +func (MonitorTarget) TableName() string { + return "monitor_targets" +} + +// ============================================ +// MonitorMetric 监控指标定义 +// ============================================ + +// MetricType 指标类型 +type MetricType string + +const ( + MetricTypeGauge MetricType = "gauge" // 瞬时值 + MetricTypeCounter MetricType = "counter" // 计数器 + MetricTypeSummary MetricType = "summary" // 摘要 +) + +// DataSourceType 数据来源类型 +type DataSourceType string + +const ( + DataSourceAgent DataSourceType = "agent" // Agent采集 + DataSourceSNMP DataSourceType = "snmp" // SNMP采集 + DataSourceAPI DataSourceType = "api" // API采集 + DataSourcePrometheus DataSourceType = "prometheus" // Prometheus + DataSourceZabbix DataSourceType = "zabbix" // Zabbix +) + +// MonitorMetric 监控指标定义表 +// 定义系统中所有可采集的监控指标元数据 +type MonitorMetric struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;uniqueIndex:uk_metric_name;comment:指标名称" json:"name"` + DisplayName string `gorm:"type:varchar(128);not null;comment:显示名称" json:"display_name"` + Description string `gorm:"type:varchar(256);comment:指标描述" json:"description,omitempty"` + MetricType MetricType `gorm:"type:varchar(16);not null;comment:指标类型: gauge/counter/summary" json:"metric_type"` + Unit string `gorm:"type:varchar(32);comment:单位" json:"unit,omitempty"` + DataSource DataSourceType `gorm:"type:varchar(16);not null;comment:数据来源" json:"data_source"` + CollectExpr string `gorm:"type:varchar(256);comment:采集表达式/OID/PromQL" json:"collect_expr,omitempty"` + Interval int `gorm:"default:60;comment:采集间隔(秒)" json:"interval"` + Labels string `gorm:"type:varchar(512);comment:指标标签定义JSON" json:"labels,omitempty"` + ValueType string `gorm:"type:varchar(16);default:'float64';comment:值类型" json:"value_type"` + Category string `gorm:"type:varchar(64);comment:指标分类" json:"category,omitempty"` + IsDefault bool `gorm:"default:true;comment:是否默认启用" json:"is_default"` + IsInternal bool `gorm:"default:false;comment:是否内置指标" json:"is_internal"` + Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` + Icon string `gorm:"type:varchar(64);comment:图标" json:"icon,omitempty"` + SortOrder int `gorm:"default:0;comment:排序" json:"sort_order"` + 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"` +} + +// TableName 指定表名 +func (MonitorMetric) TableName() string { + return "monitor_metrics" +} + +// ============================================ +// MetricValue 指标值(时序数据) +// ============================================ + +// MetricValue 指标值表(按时间分区的时序数据) +// 存储采集到的实际指标数值,支持高并发写入和时序查询 +type MetricValue struct { + ID uint64 `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + MetricID uint `gorm:"not null;index:idx_metric_time;comment:指标ID" json:"metric_id"` + TargetID uint `gorm:"not null;index:idx_target_metric;comment:目标ID" json:"target_id"` + Instance string `gorm:"type:varchar(128);index;comment:实例标识" json:"instance,omitempty"` + Value float64 `gorm:"not null;comment:指标值" json:"value"` + RawValue string `gorm:"type:text;comment:原始值(用于字符串类型)" json:"raw_value,omitempty"` + Labels string `gorm:"type:varchar(512);index;comment:标签JSON" json:"labels,omitempty"` + Timestamp int64 `gorm:"not null;index:idx_metric_time;comment:时间戳(秒)" json:"timestamp"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + + // 关联关系 + Metric *MonitorMetric `gorm:"foreignKey:MetricID" json:"metric,omitempty"` + Target *MonitorTarget `gorm:"foreignKey:TargetID" json:"target,omitempty"` +} + +// TableName 指定表名 +func (MetricValue) TableName() string { + return "metric_values" +} + +// MetricHistory 指标历史数据(简化版,供监控查询使用) +type MetricHistory struct { + ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"` + TargetID uint `gorm:"index;not null" json:"target_id"` + MetricName string `gorm:"type:varchar(128);index" json:"metric_name"` + Value float64 `json:"value"` + Timestamp int64 `gorm:"index;not null" json:"timestamp"` +} + +func (MetricHistory) TableName() string { + return "metric_histories" +} + +// MetricValueQuery 指标值查询参数 +type MetricValueQuery struct { + MetricID uint `json:"metric_id" form:"metric_id"` + TargetID uint `json:"target_id" form:"target_id"` + Instance string `json:"instance" form:"instance"` + StartTime int64 `json:"start_time" form:"start_time"` + EndTime int64 `json:"end_time" form:"end_time"` + Step int `json:"step" form:"step"` // 聚合步长(秒) + Limit int `json:"limit" form:"limit"` +} + +// MetricAggResult 指标聚合结果 +type MetricAggResult struct { + Timestamp int64 `json:"timestamp"` + Value float64 `json:"value"` + Count int64 `json:"count,omitempty"` + Min float64 `json:"min,omitempty"` + Max float64 `json:"max,omitempty"` + Avg float64 `json:"avg,omitempty"` +} + +// ============================================ +// MetricTag 指标标签 +// ============================================ + +// MetricTag 指标标签定义表 +// 定义指标维度标签,用于多维数据分析和过滤 +type MetricTag struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + MetricID uint `gorm:"not null;index:idx_metric_tag;comment:指标ID" json:"metric_id"` + TagKey string `gorm:"type:varchar(64);not null;index:idx_metric_tag;comment:标签键" json:"tag_key"` + TagValue string `gorm:"type:varchar(128);index;comment:标签值" json:"tag_value"` + Description string `gorm:"type:varchar(128);comment:描述" json:"description,omitempty"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` +} + +// TableName 指定表名 +func (MetricTag) TableName() string { + return "metric_tags" +} + +// ============================================ +// MonitorDashboard 监控大屏配置 +// ============================================ + +// DashboardType 大屏类型 +type DashboardType string + +const ( + DashboardTypeSystem DashboardType = "system" // 系统内置 + DashboardTypeCustom DashboardType = "custom" // 自定义 + DashboardTypeShared DashboardType = "shared" // 共享 +) + +// MonitorDashboard 监控大屏配置表 +// 定义监控大屏的元数据配置 +type MonitorDashboard struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;comment:大屏名称" json:"name"` + Type DashboardType `gorm:"type:varchar(16);default:'custom';comment:大屏类型" json:"type"` + Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` + Layout string `gorm:"type:text;comment:布局配置JSON" json:"layout,omitempty"` + Theme string `gorm:"type:varchar(32);default:'dark';comment:主题: dark/light" json:"theme"` + RefreshRate int `gorm:"default:30;comment:自动刷新间隔(秒)" json:"refresh_rate"` + IsDefault bool `gorm:"default:false;comment:是否默认大屏" json:"is_default"` + IsPublic bool `gorm:"default:false;comment:是否公开" json:"is_public"` + CreatedBy uint `gorm:"index;comment:创建人ID" json:"created_by"` + Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` + SortOrder int `gorm:"default:0;comment:排序" json:"sort_order"` + 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"` + + // 关联关系 + Panels []DashboardPanel `gorm:"foreignKey:DashboardID" json:"panels,omitempty"` +} + +// TableName 指定表名 +func (MonitorDashboard) TableName() string { + return "monitor_dashboards" +} + +// ============================================ +// DashboardPanel 大屏面板 +// ============================================ + +// PanelType 面板类型 +type PanelType string + +const ( + PanelTypeChart PanelType = "chart" // 图表 + PanelTypeGauge PanelType = "gauge" // 仪表盘 + PanelTypeCounter PanelType = "counter" // 计数器 + PanelTypeTable PanelType = "table" // 表格 + PanelTypeStatus PanelType = "status" // 状态 + PanelTypeTopology PanelType = "topology" // 拓扑 + PanelTypeLog PanelType = "log" // 日志 + PanelTypeAlertList PanelType = "alert_list" // 告警列表 + PanelTypeHeatmap PanelType = "heatmap" // 热力图 + PanelTypeText PanelType = "text" // 文本 +) + +// ChartType 图表类型 +type ChartType string + +const ( + ChartTypeLine ChartType = "line" // 折线 + ChartTypeBar ChartType = "bar" // 柱状 + ChartTypeArea ChartType = "area" // 面积 + ChartTypePie ChartType = "pie" // 饼图 + ChartTypeScatter ChartType = "scatter" // 散点 +) + +// DashboardPanel 大屏面板配置表 +// 定义监控大屏中的各个面板配置 +type DashboardPanel struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + DashboardID uint `gorm:"not null;index;comment:大屏ID" json:"dashboard_id"` + Title string `gorm:"type:varchar(128);not null;comment:面板标题" json:"title"` + Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` + PanelType PanelType `gorm:"type:varchar(16);not null;comment:面板类型" json:"panel_type"` + ChartType ChartType `gorm:"type:varchar(16);comment:图表类型" json:"chart_type,omitempty"` + // 位置布局配置 + X int `gorm:"default:0;comment:网格X坐标" json:"x"` + Y int `gorm:"default:0;comment:网格Y坐标" json:"y"` + Width int `gorm:"default:6;comment:宽度(网格列数)" json:"width"` + Height int `gorm:"default:4;comment:高度(网格行数)" json:"height"` + // 数据源配置 + DataSource string `gorm:"type:varchar(16);comment:数据源类型" json:"data_source,omitempty"` + MetricID uint `gorm:"comment:关联指标ID" json:"metric_id,omitempty"` + TargetID uint `gorm:"comment:关联目标ID" json:"target_id,omitempty"` + QueryExpr string `gorm:"type:text;comment:查询表达式" json:"query_expr,omitempty"` + QueryConfig string `gorm:"type:text;comment:查询配置JSON" json:"query_config,omitempty"` + // 展示配置 + TimeRange string `gorm:"type:varchar(16);default:'1h';comment:时间范围" json:"time_range"` + RefreshRate int `gorm:"default:0;comment:刷新间隔(秒),0表示继承大屏" json:"refresh_rate"` + Thresholds string `gorm:"type:text;comment:阈值配置JSON" json:"thresholds,omitempty"` + Colors string `gorm:"type:text;comment:颜色配置JSON" json:"colors,omitempty"` + Legend string `gorm:"type:varchar(64);comment:图例配置" json:"legend,omitempty"` + Unit string `gorm:"type:varchar(32);comment:显示单位" json:"unit,omitempty"` + Decimals int `gorm:"default:2;comment:小数位数" json:"decimals"` + // 状态 + Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` + SortOrder int `gorm:"default:0;comment:排序" json:"sort_order"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` +} + +// TableName 指定表名 +func (DashboardPanel) TableName() string { + return "dashboard_panels" +} + +// PanelQueryConfig 面板查询配置 +type PanelQueryConfig struct { + Metrics []string `json:"metrics"` // 指标名称列表 + Targets []uint `json:"targets"` // 目标ID列表 + Instances []string `json:"instances"` // 实例列表 + Aggregations []string `json:"aggregations"` // 聚合函数 + GroupBy []string `json:"group_by"` // 分组标签 + Filters map[string]string `json:"filters"` // 过滤条件 + Step int `json:"step"` // 步长 + LegendFormat string `json:"legend_format"` // 图例格式 +} + +// ThresholdConfig 阈值配置 +type ThresholdConfig struct { + Critical float64 `json:"critical"` // 严重 + Warning float64 `json:"warning"` // 警告 + Info float64 `json:"info"` // 提示 + Unit string `json:"unit"` // 单位 +} + +// HostGroup 主机组 +type HostGroup struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;uniqueIndex;comment:组名称" json:"name"` + Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` + ParentID uint `gorm:"index;comment:父组ID" json:"parent_id,omitempty"` + SortOrder int `gorm:"default:0;comment:排序" json:"sort_order"` + 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"` +} + +// TableName 指定表名 +func (HostGroup) TableName() string { + return "host_groups" +} diff --git a/backend/internal/models/site.go b/backend/internal/models/site.go new file mode 100644 index 0000000..811fccb --- /dev/null +++ b/backend/internal/models/site.go @@ -0,0 +1,266 @@ +package models + +import ( + "time" +) + +// ============================================ +// SiteTask 拨测任务 +// ============================================ + +// SiteTaskType 拨测任务类型 +type SiteTaskType string + +const ( + SiteTaskTypeHTTP SiteTaskType = "http" // HTTP/HTTPS拨测 + 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"` + + // 关联关系 + Results []SiteResult `gorm:"foreignKey:TaskID" json:"results,omitempty"` + Alerts []SiteAlert `gorm:"foreignKey:TaskID" json:"alerts,omitempty"` +} + +// TableName 指定表名 +func (SiteTask) TableName() string { + return "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 "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 "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"` +} diff --git a/backend/internal/models/snmp.go b/backend/internal/models/snmp.go new file mode 100644 index 0000000..15c1391 --- /dev/null +++ b/backend/internal/models/snmp.go @@ -0,0 +1,252 @@ +package models + +import ( + "time" +) + +// ============================================ +// SNMPDevice SNMP设备 +// ============================================ + +// SNMPVersion SNMP协议版本 +type SNMPVersion string + +const ( + SNMPVersionV1 SNMPVersion = "v1" // SNMP v1 + SNMPVersionV2c SNMPVersion = "v2c" // SNMP v2c + SNMPVersionV3 SNMPVersion = "v3" // SNMP v3 +) + +// SNMPAuthProtocol SNMP v3 认证协议 +type SNMPAuthProtocol string + +const ( + SNMPAuthMD5 SNMPAuthProtocol = "MD5" // MD5认证 + SNMPAuthSHA SNMPAuthProtocol = "SHA" // SHA认证 + SNMPAuthSHAAES SNMPAuthProtocol = "SHA-AES" // SHA+AES +) + +// SNMPPrivProtocol SNMP v3 加密协议 +type SNMPPrivProtocol string + +const ( + SNMPPrivDES SNMPPrivProtocol = "DES" // DES加密 + SNMPPrivAES SNMPPrivProtocol = "AES" // AES加密 + SNMPPrivAES256 SNMPPrivProtocol = "AES256" // AES256加密 +) + +// SNMPDeviceStatus SNMP设备状态 +type SNMPDeviceStatus string + +const ( + SNMPDeviceOnline SNMPDeviceStatus = "online" // 在线 + SNMPDeviceOffline SNMPDeviceStatus = "offline" // 离线 + SNMPDeviceError SNMPDeviceStatus = "error" // 错误 +) + +// SNMPDevice SNMP设备表 +// 存储通过SNMP协议监控的网络设备信息 +type SNMPDevice struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;comment:设备名称" json:"name"` + IP string `gorm:"type:varchar(64);not null;index;comment:设备IP地址" json:"ip"` + Port int `gorm:"default:161;comment:SNMP端口" json:"port"` + Version SNMPVersion `gorm:"type:varchar(4);default:'v2c';comment:SNMP版本: v1/v2c/v3" json:"version"` + Community string `gorm:"type:varchar(64);comment:共同体字符串(v1/v2c)" json:"community,omitempty"` + // SNMP v3 安全参数 + Username string `gorm:"type:varchar(64);comment:v3用户名" json:"username,omitempty"` + AuthProtocol SNMPAuthProtocol `gorm:"type:varchar(16);comment:v3认证协议" json:"auth_protocol,omitempty"` + AuthPassword string `gorm:"type:varchar(128);comment:v3认证密码" json:"auth_password,omitempty"` + PrivProtocol SNMPPrivProtocol `gorm:"type:varchar(16);comment:v3加密协议" json:"priv_protocol,omitempty"` + PrivPassword string `gorm:"type:varchar(128);comment:v3加密密码" json:"priv_password,omitempty"` + SecurityLevel string `gorm:"type:varchar(16);comment:v3安全级别: noAuthNoPriv/authNoPriv/authPriv" json:"security_level,omitempty"` + // 设备信息 + DeviceType string `gorm:"type:varchar(64);comment:设备类型: switch/router/firewall/printer/server" json:"device_type"` + Vendor string `gorm:"type:varchar(64);comment:厂商" json:"vendor,omitempty"` + Model string `gorm:"type:varchar(64);comment:型号" json:"model,omitempty"` + SysDescr string `gorm:"type:text;comment:系统描述" json:"sys_descr,omitempty"` + SysObjectID string `gorm:"type:varchar(128);comment:系统OID" json:"sys_object_id,omitempty"` + SysUptime uint64 `gorm:"comment:系统运行时间(百分之一秒)" json:"sys_uptime,omitempty"` + SysContact string `gorm:"type:varchar(128);comment:系统联系人" json:"sys_contact,omitempty"` + SysName string `gorm:"type:varchar(128);comment:系统名称" json:"sys_name,omitempty"` + SysLocation string `gorm:"type:varchar(128);comment:系统位置" json:"sys_location,omitempty"` + // 采集配置 + TemplateID uint `gorm:"index;comment:OID模板ID" json:"template_id,omitempty"` + CollectInterval int `gorm:"default:300;comment:采集间隔(秒)" json:"collect_interval"` + Timeout int `gorm:"default:10;comment:超时时间(秒)" json:"timeout"` + Retries int `gorm:"default:3;comment:重试次数" json:"retries"` + MaxRepetitions uint32 `gorm:"default:25;comment:BulkWalk最大重复数" json:"max_repetitions"` + // 状态 + Status SNMPDeviceStatus `gorm:"type:varchar(16);default:'offline';index;comment:状态" json:"status"` + LastCollectAt *time.Time `gorm:"comment:最后采集时间" json:"last_collect_at,omitempty"` + LastError string `gorm:"type:text;comment:最后错误信息" json:"last_error,omitempty"` + Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` + 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"` + + // 关联关系 + Template *SNMPTemplate `gorm:"foreignKey:TemplateID" json:"template,omitempty"` + Metrics []SNMPMetric `gorm:"foreignKey:DeviceID" json:"metrics,omitempty"` +} + +// TableName 指定表名 +func (SNMPDevice) TableName() string { + return "snmp_devices" +} + +// ============================================ +// SNMPTemplate OID模板 +// ============================================ + +// SNMPTemplate OID模板表 +// 预定义的OID模板,用于快速配置同类设备的监控 +type SNMPTemplate struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;comment:模板名称" json:"name"` + Description string `gorm:"type:varchar(256);comment:描述" json:"description,omitempty"` + DeviceType string `gorm:"type:varchar(64);comment:适用设备类型" json:"device_type,omitempty"` + Vendor string `gorm:"type:varchar(64);comment:适用厂商" json:"vendor,omitempty"` + IsDefault bool `gorm:"default:false;comment:是否默认模板" json:"is_default"` + IsSystem bool `gorm:"default:false;comment:是否系统模板" json:"is_system"` + Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` + 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"` + + // 关联关系 + OIDs []SNMPTemplateOID `gorm:"foreignKey:TemplateID" json:"oids,omitempty"` +} + +// TableName 指定表名 +func (SNMPTemplate) TableName() string { + return "snmp_templates" +} + +// SNMPTemplateOID OID模板条目表 +// 模板中具体的OID定义 +type SNMPTemplateOID struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + TemplateID uint `gorm:"not null;index;comment:模板ID" json:"template_id"` + Name string `gorm:"type:varchar(64);not null;comment:OID名称" json:"name"` + OID string `gorm:"type:varchar(256);not null;comment:OID字符串" json:"oid"` + Description string `gorm:"type:varchar(128);comment:描述" json:"description,omitempty"` + DataType string `gorm:"type:varchar(16);comment:数据类型: integer/counter/gauge/string" json:"data_type"` + Unit string `gorm:"type:varchar(32);comment:单位" json:"unit,omitempty"` + IsIndex bool `gorm:"default:false;comment:是否索引OID" json:"is_index"` + Conversion string `gorm:"type:varchar(64);comment:转换公式" json:"conversion,omitempty"` + SortOrder int `gorm:"default:0;comment:排序" json:"sort_order"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` +} + +// TableName 指定表名 +func (SNMPTemplateOID) TableName() string { + return "snmp_template_oids" +} + +// ============================================ +// SNMPMetric SNMP采集指标 +// ============================================ + +// SNMPMetric SNMP采集指标表 +// 存储从SNMP设备采集到的指标数据 +type SNMPMetric struct { + ID uint64 `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + DeviceID uint `gorm:"not null;index:idx_device_metric;comment:设备ID" json:"device_id"` + Name string `gorm:"type:varchar(64);not null;index;comment:指标名称" json:"name"` + OID string `gorm:"type:varchar(256);not null;comment:OID" json:"oid"` + Index string `gorm:"type:varchar(128);index;comment:索引值(如接口名)" json:"index,omitempty"` + Value float64 `gorm:"comment:数值" json:"value"` + RawValue string `gorm:"type:text;comment:原始字符串值" json:"raw_value,omitempty"` + Unit string `gorm:"type:varchar(32);comment:单位" json:"unit,omitempty"` + DataType string `gorm:"type:varchar(16);comment:数据类型" json:"data_type"` + Timestamp int64 `gorm:"not null;index:idx_device_metric;comment:采集时间戳" json:"timestamp"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + + // 关联关系 + Device *SNMPDevice `gorm:"foreignKey:DeviceID" json:"device,omitempty"` +} + +// TableName 指定表名 +func (SNMPMetric) TableName() string { + return "snmp_metrics" +} + +// ============================================ +// SNMPInterface 网络接口信息 +// ============================================ + +// SNMPInterface SNMP接口信息表 +// 存储网络设备的接口详细信息 +type SNMPInterface struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + DeviceID uint `gorm:"not null;index;comment:设备ID" json:"device_id"` + IfIndex int `gorm:"not null;comment:接口索引" json:"if_index"` + IfDescr string `gorm:"type:varchar(128);comment:接口描述" json:"if_descr,omitempty"` + IfName string `gorm:"type:varchar(64);comment:接口名称" json:"if_name,omitempty"` + IfAlias string `gorm:"type:varchar(128);comment:接口别名" json:"if_alias,omitempty"` + IfType int `gorm:"comment:接口类型" json:"if_type,omitempty"` + IfSpeed uint64 `gorm:"comment:接口速率(bps)" json:"if_speed,omitempty"` + IfPhysAddr string `gorm:"type:varchar(32);comment:物理地址(MAC)" json:"if_phys_addr,omitempty"` + IfAdminStatus int `gorm:"comment:管理状态: 1=up 2=down 3=testing" json:"if_admin_status,omitempty"` + IfOperStatus int `gorm:"comment:操作状态: 1=up 2=down 3=testing" json:"if_oper_status,omitempty"` + IfHighSpeed uint32 `gorm:"comment:高速接口速率(Mbps)" json:"if_high_speed,omitempty"` + Status int `gorm:"default:1;comment:状态" json:"status"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` +} + +// TableName 指定表名 +func (SNMPInterface) TableName() string { + return "snmp_interfaces" +} + +// ============================================ +// SNMPHistory SNMP采集历史 +// ============================================ + +// SNMPHistory SNMP采集历史表(按时间分区) +// 存储SNMP指标的时序历史数据,用于趋势分析和报表 +type SNMPHistory struct { + ID uint64 `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + DeviceID uint `gorm:"not null;index:idx_snmp_history;comment:设备ID" json:"device_id"` + MetricID uint `gorm:"not null;index;comment:指标定义ID" json:"metric_id"` + Name string `gorm:"type:varchar(64);not null;comment:指标名称" json:"name"` + Index string `gorm:"type:varchar(128);index;comment:索引值" json:"index,omitempty"` + Value float64 `gorm:"not null;comment:采集值" json:"value"` + Unit string `gorm:"type:varchar(32);comment:单位" json:"unit,omitempty"` + Timestamp int64 `gorm:"not null;index:idx_snmp_history;comment:时间戳" json:"timestamp"` + CreatedAt time.Time `gorm:"not null;comment:创建时间" json:"created_at"` +} + +// TableName 指定表名 +func (SNMPHistory) TableName() string { + return "snmp_history" +} + +// SNMPWalkResult SNMP Walk 结果 +type SNMPWalkResult struct { + OID string `json:"oid"` + Type string `json:"type"` + Value string `json:"value"` + Index string `json:"index,omitempty"` + Timestamp int64 `json:"timestamp"` +} + +// SNMPConnectionConfig SNMP连接配置 +type SNMPConnectionConfig struct { + IP string `json:"ip"` + Port int `json:"port"` + Version SNMPVersion `json:"version"` + Community string `json:"community,omitempty"` + Username string `json:"username,omitempty"` + AuthProtocol SNMPAuthProtocol `json:"auth_protocol,omitempty"` + AuthPassword string `json:"auth_password,omitempty"` + PrivProtocol SNMPPrivProtocol `json:"priv_protocol,omitempty"` + PrivPassword string `json:"priv_password,omitempty"` + SecurityLevel string `json:"security_level,omitempty"` + Timeout int `json:"timeout"` + Retries int `json:"retries"` + MaxRepetitions uint32 `json:"max_repetitions"` +} diff --git a/backend/pkg/prometheus/client.go b/backend/pkg/prometheus/client.go new file mode 100644 index 0000000..ee001a4 --- /dev/null +++ b/backend/pkg/prometheus/client.go @@ -0,0 +1,531 @@ +package prometheus + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" +) + +// ============================================ +// Prometheus HTTP API 客户端 +// ============================================ + +// Client Prometheus API客户端 +// 封装对Prometheus HTTP API的调用,支持即时查询、范围查询、指标发现等 +type Client struct { + baseURL string // Prometheus服务地址,如 http://localhost:9090 + httpClient *http.Client // HTTP客户端 + username string // 基本认证用户名 + password string // 基本认证密码 +} + +// ClientOption 客户端配置选项 +type ClientOption func(*Client) + +// WithTimeout 设置请求超时 +func WithTimeout(timeout time.Duration) ClientOption { + return func(c *Client) { + c.httpClient.Timeout = timeout + } +} + +// WithBasicAuth 设置基本认证 +func WithBasicAuth(username, password string) ClientOption { + return func(c *Client) { + c.username = username + c.password = password + } +} + +// NewClient 创建Prometheus客户端 +// baseURL: Prometheus服务地址,如 http://localhost:9090 +func NewClient(baseURL string, opts ...ClientOption) *Client { + c := &Client{ + baseURL: baseURL, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } + for _, opt := range opts { + opt(c) + } + return c +} + +// SetTimeout 设置请求超时 +func (c *Client) SetTimeout(timeout time.Duration) { + c.httpClient.Timeout = timeout +} + +// doRequest 执行HTTP请求并解析JSON响应 +func (c *Client) doRequest(ctx context.Context, path string, params url.Values, result interface{}) error { + u, err := url.Parse(c.baseURL + path) + if err != nil { + return fmt.Errorf("invalid URL: %w", err) + } + u.RawQuery = params.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return fmt.Errorf("create request failed: %w", err) + } + + if c.username != "" { + req.SetBasicAuth(c.username, c.password) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("HTTP request failed: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("read response body failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) + } + + if result != nil { + if err := json.Unmarshal(body, result); err != nil { + return fmt.Errorf("unmarshal response failed: %w, body: %s", err, string(body)) + } + } + return nil +} + +// ============================================ +// 查询响应数据结构 +// ============================================ + +// QueryResponse 查询响应 +type QueryResponse struct { + Status string `json:"status"` + Data struct { + ResultType string `json:"resultType"` + Result []MetricData `json:"result"` + } `json:"data"` + Error string `json:"error,omitempty"` + ErrorType string `json:"errorType,omitempty"` +} + +// MetricData 指标数据(即时查询结果) +type MetricData struct { + Metric map[string]string `json:"metric"` // 标签集合 + Value []interface{} `json:"value"` // [时间戳, 值] + Values [][]interface{} `json:"values"` // 范围查询时使用 +} + +// GetTimestamp 获取时间戳 +func (m *MetricData) GetTimestamp() int64 { + if len(m.Value) >= 1 { + if ts, ok := m.Value[0].(float64); ok { + return int64(ts) + } + if tsStr, ok := m.Value[0].(string); ok { + if ts, err := strconv.ParseFloat(tsStr, 64); err == nil { + return int64(ts) + } + } + } + return 0 +} + +// GetValue 获取值(字符串) +func (m *MetricData) GetValue() string { + if len(m.Value) >= 2 { + if v, ok := m.Value[1].(string); ok { + return v + } + } + return "" +} + +// GetFloatValue 获取浮点值 +func (m *MetricData) GetFloatValue() (float64, error) { + v := m.GetValue() + if v == "" { + return 0, fmt.Errorf("empty value") + } + return strconv.ParseFloat(v, 64) +} + +// MetricPoint 时序数据点 +type MetricPoint struct { + Timestamp int64 `json:"timestamp"` + Value float64 `json:"value"` +} + +// GetTimeSeries 将范围查询结果转换为时序数据 +func (m *MetricData) GetTimeSeries() ([]MetricPoint, error) { + var points []MetricPoint + for _, v := range m.Values { + if len(v) < 2 { + continue + } + var ts int64 + switch t := v[0].(type) { + case float64: + ts = int64(t) + case string: + if f, err := strconv.ParseFloat(t, 64); err == nil { + ts = int64(f) + } + } + var val float64 + switch s := v[1].(type) { + case string: + if f, err := strconv.ParseFloat(s, 64); err == nil { + val = f + } + case float64: + val = s + } + points = append(points, MetricPoint{Timestamp: ts, Value: val}) + } + return points, nil +} + +// ============================================ +// 即时查询 (Instant Query) +// ============================================ + +// Query 执行即时查询 +// query: PromQL查询表达式 +// ts: 可选的查询时间戳,为0时使用当前时间 +// 返回查询结果列表和错误 +func (c *Client) Query(ctx context.Context, query string, ts time.Time) ([]MetricData, error) { + if query == "" { + return nil, fmt.Errorf("query expression is required") + } + + params := url.Values{} + params.Set("query", query) + if !ts.IsZero() { + params.Set("time", strconv.FormatInt(ts.Unix(), 10)) + } + + var resp QueryResponse + if err := c.doRequest(ctx, "/api/v1/query", params, &resp); err != nil { + return nil, fmt.Errorf("query failed: %w", err) + } + + if resp.Status != "success" { + return nil, fmt.Errorf("query error: %s", resp.Error) + } + + return resp.Data.Result, nil +} + +// QuerySingle 执行即时查询并返回单个结果 +func (c *Client) QuerySingle(ctx context.Context, query string, ts time.Time) (*MetricData, error) { + results, err := c.Query(ctx, query, ts) + if err != nil { + return nil, err + } + if len(results) == 0 { + return nil, fmt.Errorf("no data returned for query: %s", query) + } + return &results[0], nil +} + +// ============================================ +// 范围查询 (Range Query) +// ============================================ + +// RangeQueryOptions 范围查询选项 +type RangeQueryOptions struct { + Start time.Time // 开始时间 + End time.Time // 结束时间 + Step time.Duration // 步长 +} + +// QueryRange 执行范围查询 +// query: PromQL查询表达式 +// opts: 范围查询选项 +// 返回时序数据列表和错误 +func (c *Client) QueryRange(ctx context.Context, query string, opts RangeQueryOptions) ([]MetricData, error) { + if query == "" { + return nil, fmt.Errorf("query expression is required") + } + + params := url.Values{} + params.Set("query", query) + params.Set("start", strconv.FormatInt(opts.Start.Unix(), 10)) + params.Set("end", strconv.FormatInt(opts.End.Unix(), 10)) + params.Set("step", strconv.FormatInt(int64(opts.Step.Seconds()), 10)) + + var resp QueryResponse + if err := c.doRequest(ctx, "/api/v1/query_range", params, &resp); err != nil { + return nil, fmt.Errorf("query range failed: %w", err) + } + + if resp.Status != "success" { + return nil, fmt.Errorf("query range error: %s", resp.Error) + } + + return resp.Data.Result, nil +} + +// QueryRangePoints 执行范围查询并返回时序数据点 +func (c *Client) QueryRangePoints(ctx context.Context, query string, opts RangeQueryOptions) (map[string][]MetricPoint, error) { + results, err := c.QueryRange(ctx, query, opts) + if err != nil { + return nil, err + } + + series := make(map[string][]MetricPoint) + for _, r := range results { + key := r.Metric["__name__"] + if key == "" { + // 使用所有标签拼接作为key + for k, v := range r.Metric { + if k != "__name__" { + key += k + "=" + v + "," + } + } + } + points, err := r.GetTimeSeries() + if err != nil { + continue + } + series[key] = points + } + return series, nil +} + +// ============================================ +// 指标元数据查询 +// ============================================ + +// LabelValuesResponse 标签值响应 +type LabelValuesResponse struct { + Status string `json:"status"` + Data []string `json:"data"` + Error string `json:"error,omitempty"` +} + +// MetricsResponse 指标列表响应 +type MetricsResponse struct { + Status string `json:"status"` + Data []string `json:"data"` + Error string `json:"error,omitempty"` +} + +// GetMetrics 获取指标名称列表 +// 返回Prometheus中所有可用的指标名称 +func (c *Client) GetMetrics(ctx context.Context) ([]string, error) { + var resp MetricsResponse + if err := c.doRequest(ctx, "/api/v1/label/__name__/values", nil, &resp); err != nil { + return nil, fmt.Errorf("get metrics failed: %w", err) + } + if resp.Status != "success" { + return nil, fmt.Errorf("get metrics error: %s", resp.Error) + } + return resp.Data, nil +} + +// GetLabelValues 获取指定标签的所有值 +// labelName: 标签名称,如 "instance", "job" +// 返回该标签的所有不重复值 +func (c *Client) GetLabelValues(ctx context.Context, labelName string, matchers ...string) ([]string, error) { + if labelName == "" { + return nil, fmt.Errorf("label name is required") + } + + params := url.Values{} + for _, m := range matchers { + params.Add("match[]", m) + } + + var resp LabelValuesResponse + if err := c.doRequest(ctx, fmt.Sprintf("/api/v1/label/%s/values", labelName), params, &resp); err != nil { + return nil, fmt.Errorf("get label values failed: %w", err) + } + if resp.Status != "success" { + return nil, fmt.Errorf("get label values error: %s", resp.Error) + } + return resp.Data, nil +} + +// ============================================ +// 指标发现 (Series Discovery) +// ============================================ + +// SeriesResponse 指标发现响应 +type SeriesResponse struct { + Status string `json:"status"` + Data []map[string]string `json:"data"` + Error string `json:"error,omitempty"` +} + +// SeriesDiscovery 查询满足条件的指标系列 +// matchers: 指标选择器,如 "up{job=\"prometheus\"}" +// 返回所有匹配的时间序列的标签集合 +func (c *Client) SeriesDiscovery(ctx context.Context, matchers []string, start, end time.Time) ([]map[string]string, error) { + if len(matchers) == 0 { + return nil, fmt.Errorf("at least one matcher is required") + } + + params := url.Values{} + for _, m := range matchers { + params.Add("match[]", m) + } + if !start.IsZero() { + params.Set("start", strconv.FormatInt(start.Unix(), 10)) + } + if !end.IsZero() { + params.Set("end", strconv.FormatInt(end.Unix(), 10)) + } + + var resp SeriesResponse + if err := c.doRequest(ctx, "/api/v1/series", params, &resp); err != nil { + return nil, fmt.Errorf("series discovery failed: %w", err) + } + if resp.Status != "success" { + return nil, fmt.Errorf("series discovery error: %s", resp.Error) + } + return resp.Data, nil +} + +// GetSeriesByMetric 通过指标名称查询所有系列 +// metricName: 指标名称 +func (c *Client) GetSeriesByMetric(ctx context.Context, metricName string) ([]map[string]string, error) { + if metricName == "" { + return nil, fmt.Errorf("metric name is required") + } + return c.SeriesDiscovery(ctx, []string{metricName}, time.Time{}, time.Time{}) +} + +// ============================================ +// 元数据和规则查询 +// ============================================ + +// Target 监控目标 +type Target struct { + Targets []map[string]string `json:"targets"` + Labels map[string]string `json:"labels"` +} + +// TargetsResponse 目标发现响应 +type TargetsResponse struct { + Status string `json:"status"` + Data struct { + ActiveTargets []ActiveTarget `json:"activeTargets"` + DroppedTargets []DroppedTarget `json:"droppedTargets"` + } `json:"data"` + Error string `json:"error,omitempty"` +} + +// ActiveTarget 活跃目标 +type ActiveTarget struct { + DiscoveredLabels map[string]string `json:"discoveredLabels"` + Labels map[string]string `json:"labels"` + ScrapePool string `json:"scrapePool"` + ScrapeURL string `json:"scrapeUrl"` + GlobalURL string `json:"globalUrl"` + LastError string `json:"lastError"` + LastScrape time.Time `json:"lastScrape"` + LastScrapeDuration float64 `json:"lastScrapeDuration"` + Health string `json:"health"` +} + +// DroppedTarget 丢弃的目标 +type DroppedTarget struct { + DiscoveredLabels map[string]string `json:"discoveredLabels"` +} + +// GetTargets 获取监控目标列表 +func (c *Client) GetTargets(ctx context.Context) (*TargetsResponse, error) { + var resp TargetsResponse + if err := c.doRequest(ctx, "/api/v1/targets", nil, &resp); err != nil { + return nil, fmt.Errorf("get targets failed: %w", err) + } + if resp.Status != "success" { + return nil, fmt.Errorf("get targets error: %s", resp.Error) + } + return &resp, nil +} + +// ============================================ +// 工具方法 +// ============================================ + +// Health 检查Prometheus服务健康状态 +func (c *Client) Health(ctx context.Context) (string, error) { + u, err := url.Parse(c.baseURL + "/-/healthy") + if err != nil { + return "", fmt.Errorf("invalid URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return "", fmt.Errorf("create request failed: %w", err) + } + + if c.username != "" { + req.SetBasicAuth(c.username, c.password) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("health check failed: %w", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("prometheus unhealthy: %d - %s", resp.StatusCode, string(body)) + } + return string(body), nil +} + +// BuildSelector 构建PromQL标签选择器 +// metricName: 指标名称 +// labels: 标签键值对 +// 返回格式化后的选择器字符串,如: node_cpu_seconds_total{mode="idle",cpu="0"} +func BuildSelector(metricName string, labels map[string]string) string { + selector := metricName + if len(labels) > 0 { + selector += "{" + first := true + for k, v := range labels { + if !first { + selector += "," + } + selector += fmt.Sprintf("%s=\"%s\"", k, v) + first = false + } + selector += "}" + } + return selector +} + +// FormatDuration 将time.Duration格式化为Prometheus持续时间字符串 +// 如: 1h30m, 5m, 10s +func FormatDuration(d time.Duration) string { + if d <= 0 { + return "0s" + } + var result string + if h := int(d.Hours()); h > 0 { + result += fmt.Sprintf("%dh", h) + d -= time.Duration(h) * time.Hour + } + if m := int(d.Minutes()); m > 0 { + result += fmt.Sprintf("%dm", m) + d -= time.Duration(m) * time.Minute + } + if s := int(d.Seconds()); s > 0 { + result += fmt.Sprintf("%ds", s) + } + return result +} diff --git a/backend/pkg/snmp/collector.go b/backend/pkg/snmp/collector.go new file mode 100644 index 0000000..48e8d71 --- /dev/null +++ b/backend/pkg/snmp/collector.go @@ -0,0 +1,621 @@ +package snmp + +import ( + "context" + "fmt" + "strconv" + "strings" + "sync" + "time" + + "github.com/gosnmp/gosnmp" +) + +// ============================================ +// SNMP 采集器 +// ============================================ + +// Collector SNMP采集器 +// 封装 gosnmp 库,提供SNMP设备的OID采集功能 +// 支持 SNMP v1/v2c/v3,支持 Get/Walk/BulkWalk 操作 +type Collector struct { + config *gosnmp.GoSNMP + mu sync.Mutex +} + +// CollectorConfig 采集器配置 +type CollectorConfig struct { + IP string // 目标IP地址 + Port uint16 // SNMP端口,默认161 + Version gosnmp.SnmpVersion // SNMP版本 + Community string // 共同体字符串(v1/v2c) + Timeout time.Duration // 超时时间 + Retries int // 重试次数 + MaxRepetitions uint8 // BulkWalk最大重复数 + + // SNMP v3 参数 + SecurityModel gosnmp.SnmpV3SecurityModel + MsgFlags gosnmp.SnmpV3MsgFlags + SecurityParameters *gosnmp.UsmSecurityParameters +} + +// WalkResult SNMP Walk结果 +type WalkResult struct { + OID string `json:"oid"` // OID + Type gosnmp.Asn1BER `json:"type"` // SNMP数据类型 + TypeName string `json:"type_name"` // 类型名称 + Value interface{} `json:"value"` // 值 + Index string `json:"index"` // 索引后缀 + Timestamp time.Time `json:"timestamp"` // 采集时间 +} + +// CollectResult 采集结果 +type CollectResult struct { + DeviceID uint `json:"device_id"` + DeviceIP string `json:"device_ip"` + Timestamp int64 `json:"timestamp"` + Success bool `json:"success"` + Error string `json:"error,omitempty"` + Metrics []SNMPMetricValue `json:"metrics"` + Duration float64 `json:"duration_ms"` // 采集耗时(ms) +} + +// SNMPMetricValue SNMP采集指标值 +type SNMPMetricValue struct { + Name string `json:"name"` + OID string `json:"oid"` + Index string `json:"index,omitempty"` + Value float64 `json:"value"` + RawValue string `json:"raw_value,omitempty"` + Unit string `json:"unit,omitempty"` + DataType string `json:"data_type"` +} + +// NewCollector 创建SNMP采集器 +// config: 采集器配置 +// 返回配置好的SNMP采集器实例和错误 +func NewCollector(config *CollectorConfig) (*Collector, error) { + if config.IP == "" { + return nil, fmt.Errorf("target IP is required") + } + + g := &gosnmp.GoSNMP{ + Target: config.IP, + Port: config.Port, + Version: config.Version, + Timeout: config.Timeout, + Retries: config.Retries, + MaxRepetitions: config.MaxRepetitions, + ExponentialTimeout: true, + } + + // 设置默认值 + if g.Port == 0 { + g.Port = 161 + } + if g.Timeout == 0 { + g.Timeout = 10 * time.Second + } + if g.Retries == 0 { + g.Retries = 3 + } + if g.MaxRepetitions == 0 { + g.MaxRepetitions = 25 + } + + // 根据版本设置认证参数 + switch config.Version { + case gosnmp.Version1: + g.Community = config.Community + if g.Community == "" { + g.Community = "public" + } + + case gosnmp.Version2c: + g.Community = config.Community + if g.Community == "" { + g.Community = "public" + } + + case gosnmp.Version3: + g.SecurityModel = config.SecurityModel + if g.SecurityModel == 0 { + g.SecurityModel = gosnmp.UserSecurityModel + } + g.MsgFlags = config.MsgFlags + if config.SecurityParameters != nil { + g.SecurityParameters = config.SecurityParameters + } + + default: + return nil, fmt.Errorf("unsupported SNMP version: %v", config.Version) + } + + return &Collector{ + config: g, + }, nil +} + +// NewCollectorV2c 快速创建SNMPv2c采集器 +// ip: 目标IP +// community: 共同体字符串 +func NewCollectorV2c(ip, community string) (*Collector, error) { + if community == "" { + community = "public" + } + return NewCollector(&CollectorConfig{ + IP: ip, + Port: 161, + Version: gosnmp.Version2c, + Community: community, + Timeout: 10 * time.Second, + Retries: 3, + }) +} + +// NewCollectorV3 快速创建SNMPv3采集器 +// ip: 目标IP +// username: 用户名 +// authProtocol: 认证协议 +// authPassword: 认证密码 +// privProtocol: 加密协议 +// privPassword: 加密密码 +// securityLevel: 安全级别 +func NewCollectorV3(ip, username string, authProtocol gosnmp.SnmpV3AuthProtocol, authPassword string, + privProtocol gosnmp.SnmpV3PrivProtocol, privPassword string, + securityLevel gosnmp.SnmpV3MsgFlags) (*Collector, error) { + + securityParams := &gosnmp.UsmSecurityParameters{ + UserName: username, + AuthenticationProtocol: authProtocol, + AuthenticationPassphrase: authPassword, + PrivacyProtocol: privProtocol, + PrivacyPassphrase: privPassword, + } + + return NewCollector(&CollectorConfig{ + IP: ip, + Port: 161, + Version: gosnmp.Version3, + Timeout: 10 * time.Second, + Retries: 3, + MsgFlags: securityLevel, + SecurityParameters: securityParams, + }) +} + +// Connect 建立SNMP连接 +func (c *Collector) Connect() error { + c.mu.Lock() + defer c.mu.Unlock() + if err := c.config.Connect(); err != nil { + return fmt.Errorf("SNMP connect to %s failed: %w", c.config.Target, err) + } + return nil +} + +// Close 关闭SNMP连接 +func (c *Collector) Close() error { + c.mu.Lock() + defer c.mu.Unlock() + return c.config.Conn.Close() +} + +// ============================================ +// SNMP Get 操作 +// ============================================ + +// Get 执行SNMP Get操作,获取指定OID的值 +// oids: OID字符串列表 +// 返回OID到值的映射和错误 +func (c *Collector) Get(ctx context.Context, oids []string) (map[string]*WalkResult, error) { + if len(oids) == 0 { + return nil, fmt.Errorf("oids are required") + } + + // 解析OID + parsedOIDs := make([]string, 0, len(oids)) + for _, oid := range oids { + parsedOIDs = append(parsedOIDs, oid) + } + + c.mu.Lock() + defer c.mu.Unlock() + + // 确保连接 + if c.config.Conn == nil { + if err := c.config.Connect(); err != nil { + return nil, fmt.Errorf("connect failed: %w", err) + } + } + + result, err := c.config.Get(parsedOIDs) + if err != nil { + return nil, fmt.Errorf("SNMP Get failed: %w", err) + } + + results := make(map[string]*WalkResult) + for _, v := range result.Variables { + r := &WalkResult{ + OID: v.Name, + Type: v.Type, + TypeName: getSNMPTypeName(v.Type), + Value: convertSNMPValue(v), + Timestamp: time.Now(), + } + results[v.Name] = r + } + + return results, nil +} + +// GetSingle 获取单个OID的值 +func (c *Collector) GetSingle(ctx context.Context, oid string) (*WalkResult, error) { + results, err := c.Get(ctx, []string{oid}) + if err != nil { + return nil, err + } + result, ok := results[oid] + if !ok { + return nil, fmt.Errorf("OID not found in response: %s", oid) + } + return result, nil +} + +// ============================================ +// SNMP Walk 操作 +// ============================================ + +// Walk 执行SNMP Walk操作,遍历指定OID下的所有子节点 +// rootOID: 根OID +// 返回遍历结果列表和错误 +func (c *Collector) Walk(ctx context.Context, rootOID string) ([]*WalkResult, error) { + if rootOID == "" { + return nil, fmt.Errorf("root OID is required") + } + + c.mu.Lock() + defer c.mu.Unlock() + + // 确保连接 + if c.config.Conn == nil { + if err := c.config.Connect(); err != nil { + return nil, fmt.Errorf("connect failed: %w", err) + } + } + + var results []*WalkResult + walkFn := func(data gosnmp.SnmpPDU) error { + index := "" + if strings.HasPrefix(data.Name, rootOID) && len(data.Name) > len(rootOID) { + suffix := strings.TrimPrefix(data.Name, rootOID) + index = strings.TrimPrefix(suffix, ".") + } + + r := &WalkResult{ + OID: data.Name, + Type: data.Type, + TypeName: getSNMPTypeName(data.Type), + Value: convertSNMPValue(data), + Index: index, + Timestamp: time.Now(), + } + results = append(results, r) + return nil + } + + if err := c.config.Walk(rootOID, walkFn); err != nil { + return nil, fmt.Errorf("SNMP Walk failed for OID %s: %w", rootOID, err) + } + + return results, nil +} + +// ============================================ +// SNMP BulkWalk 操作 +// ============================================ + +// BulkWalk 执行SNMP BulkWalk操作,高效遍历指定OID下的所有子节点 +// 相比Walk使用GetBulk请求,效率更高,适合遍历大量数据 +// rootOID: 根OID +// 返回遍历结果列表和错误 +func (c *Collector) BulkWalk(ctx context.Context, rootOID string) ([]*WalkResult, error) { + if rootOID == "" { + return nil, fmt.Errorf("root OID is required") + } + + c.mu.Lock() + defer c.mu.Unlock() + + // 确保连接 + if c.config.Conn == nil { + if err := c.config.Connect(); err != nil { + return nil, fmt.Errorf("connect failed: %w", err) + } + } + + var results []*WalkResult + walkFn := func(data gosnmp.SnmpPDU) error { + index := "" + if strings.HasPrefix(data.Name, rootOID) && len(data.Name) > len(rootOID) { + suffix := strings.TrimPrefix(data.Name, rootOID) + index = strings.TrimPrefix(suffix, ".") + } + + r := &WalkResult{ + OID: data.Name, + Type: data.Type, + TypeName: getSNMPTypeName(data.Type), + Value: convertSNMPValue(data), + Index: index, + Timestamp: time.Now(), + } + results = append(results, r) + return nil + } + + if err := c.config.BulkWalk(rootOID, walkFn); err != nil { + return nil, fmt.Errorf("SNMP BulkWalk failed for OID %s: %w", rootOID, err) + } + + return results, nil +} + +// ============================================ +// 批量采集 +// ============================================ + +// OIDDefinition OID定义 +type OIDDefinition struct { + Name string `json:"name"` // 指标名称 + OID string `json:"oid"` // OID + Unit string `json:"unit"` // 单位 + DataType string `json:"data_type"` // 数据类型 + Conversion string `json:"conversion"` // 转换公式 + IsTable bool `json:"is_table"` // 是否表OID(使用Walk) +} + +// Collect 执行批量采集 +// oids: OID定义列表 +// 返回采集结果 +func (c *Collector) Collect(ctx context.Context, oids []OIDDefinition) *CollectResult { + start := time.Now() + result := &CollectResult{ + DeviceIP: c.config.Target, + Timestamp: time.Now().Unix(), + Metrics: make([]SNMPMetricValue, 0), + } + + // 建立连接 + if err := c.Connect(); err != nil { + result.Success = false + result.Error = err.Error() + result.Duration = float64(time.Since(start).Milliseconds()) + return result + } + defer c.Close() + + for _, def := range oids { + if def.IsTable { + // 表OID使用Walk/BulkWalk + walkResults, err := c.BulkWalk(ctx, def.OID) + if err != nil { + result.Error += fmt.Sprintf("[%s walk failed: %v]", def.Name, err) + continue + } + for _, wr := range walkResults { + mv := SNMPMetricValue{ + Name: def.Name, + OID: wr.OID, + Index: wr.Index, + Unit: def.Unit, + } + if val, ok := wr.Value.(float64); ok { + mv.Value = applyConversion(val, def.Conversion) + mv.RawValue = fmt.Sprintf("%v", wr.Value) + } else { + mv.RawValue = fmt.Sprintf("%v", wr.Value) + if v, err := strconv.ParseFloat(mv.RawValue, 64); err == nil { + mv.Value = applyConversion(v, def.Conversion) + } + } + mv.DataType = def.DataType + if mv.DataType == "" { + mv.DataType = wr.TypeName + } + result.Metrics = append(result.Metrics, mv) + } + } else { + // 标量OID使用Get + wr, err := c.GetSingle(ctx, def.OID) + if err != nil { + result.Error += fmt.Sprintf("[%s get failed: %v]", def.Name, err) + continue + } + mv := SNMPMetricValue{ + Name: def.Name, + OID: wr.OID, + Unit: def.Unit, + } + if val, ok := wr.Value.(float64); ok { + mv.Value = applyConversion(val, def.Conversion) + mv.RawValue = fmt.Sprintf("%v", wr.Value) + } else { + mv.RawValue = fmt.Sprintf("%v", wr.Value) + if v, err := strconv.ParseFloat(mv.RawValue, 64); err == nil { + mv.Value = applyConversion(v, def.Conversion) + } + } + mv.DataType = def.DataType + if mv.DataType == "" { + mv.DataType = wr.TypeName + } + result.Metrics = append(result.Metrics, mv) + } + } + + result.Success = len(result.Metrics) > 0 + result.Duration = float64(time.Since(start).Milliseconds()) + return result +} + +// ============================================ +// 工具方法 +// ============================================ + +// getSNMPTypeName 获取SNMP数据类型名称 +func getSNMPTypeName(t gosnmp.Asn1BER) string { + switch t { + case gosnmp.Integer: + return "integer" + case gosnmp.OctetString: + return "string" + case gosnmp.ObjectIdentifier: + return "oid" + case gosnmp.IPAddress: + return "ipaddress" + case gosnmp.Counter32: + return "counter32" + case gosnmp.Gauge32: + return "gauge32" + case gosnmp.TimeTicks: + return "timeticks" + case gosnmp.Opaque: + return "opaque" + case gosnmp.NsapAddress: + return "nsapaddress" + case gosnmp.Counter64: + return "counter64" + case gosnmp.Uinteger32: + return "uinteger32" + case gosnmp.NoSuchObject: + return "nosuchobject" + case gosnmp.NoSuchInstance: + return "nosuchinstance" + default: + return fmt.Sprintf("unknown(%d)", t) + } +} + +// convertSNMPValue 转换SNMP值为Go类型 +func convertSNMPValue(pdu gosnmp.SnmpPDU) interface{} { + switch pdu.Type { + case gosnmp.Integer, gosnmp.Counter32, gosnmp.Gauge32, gosnmp.TimeTicks, gosnmp.Counter64, gosnmp.Uinteger32: + if val, ok := pdu.Value.(int); ok { + return float64(val) + } + if val, ok := pdu.Value.(uint); ok { + return float64(val) + } + if val, ok := pdu.Value.(uint32); ok { + return float64(val) + } + if val, ok := pdu.Value.(uint64); ok { + return float64(val) + } + if val, ok := pdu.Value.(int64); ok { + return float64(val) + } + return pdu.Value + case gosnmp.OctetString: + if val, ok := pdu.Value.([]byte); ok { + return string(val) + } + return pdu.Value + case gosnmp.OpaqueFloat: + if val, ok := pdu.Value.(float32); ok { + return float64(val) + } + return pdu.Value + case gosnmp.OpaqueDouble: + if val, ok := pdu.Value.(float64); ok { + return val + } + return pdu.Value + default: + return fmt.Sprintf("%v", pdu.Value) + } +} + +// applyConversion 应用转换公式 +// 支持简单的数学运算: *N, /N, +N, -N +func applyConversion(value float64, conversion string) float64 { + if conversion == "" { + return value + } + conversion = strings.TrimSpace(conversion) + + if strings.HasPrefix(conversion, "*") { + if f, err := strconv.ParseFloat(strings.TrimPrefix(conversion, "*"), 64); err == nil { + return value * f + } + } + if strings.HasPrefix(conversion, "/") { + if f, err := strconv.ParseFloat(strings.TrimPrefix(conversion, "/"), 64); err == nil && f != 0 { + return value / f + } + } + if strings.HasPrefix(conversion, "+") { + if f, err := strconv.ParseFloat(strings.TrimPrefix(conversion, "+"), 64); err == nil { + return value + f + } + } + if strings.HasPrefix(conversion, "-") && len(conversion) > 1 { + if f, err := strconv.ParseFloat(conversion, 64); err == nil { + return value + f // f is negative + } + } + return value +} + +// GetTargetIP 获取目标IP +func (c *Collector) GetTargetIP() string { + return c.config.Target +} + +// GetVersion 获取SNMP版本 +func (c *Collector) GetVersion() gosnmp.SnmpVersion { + return c.config.Version +} + +// ============================================ +// 预定义OID常量 (RFC1213-MIB, IF-MIB) +// ============================================ + +// 系统信息 OID +const ( + OIDSysDescr = ".1.3.6.1.2.1.1.1.0" // 系统描述 + OIDSysObjectID = ".1.3.6.1.2.1.1.2.0" // 系统对象ID + OIDSysUptime = ".1.3.6.1.2.1.1.3.0" // 系统运行时间 + OIDSysContact = ".1.3.6.1.2.1.1.4.0" // 系统联系人 + OIDSysName = ".1.3.6.1.2.1.1.5.0" // 系统名称 + OIDSysLocation = ".1.3.6.1.2.1.1.6.0" // 系统位置 +) + +// 接口信息 OID (IF-MIB) +const ( + OIDIfNumber = ".1.3.6.1.2.1.2.1.0" // 接口数量 + OIDIfTable = ".1.3.6.1.2.1.2.2" // 接口表 + OIDIfDescr = ".1.3.6.1.2.1.2.2.1.2" // 接口描述 + OIDIfType = ".1.3.6.1.2.1.2.2.1.3" // 接口类型 + OIDIfSpeed = ".1.3.6.1.2.1.2.2.1.5" // 接口速率 + OIDIfPhysAddr = ".1.3.6.1.2.1.2.2.1.6" // 物理地址 + OIDIfAdminStatus = ".1.3.6.1.2.1.2.2.1.7" // 管理状态 + OIDIfOperStatus = ".1.3.6.1.2.1.2.2.1.8" // 操作状态 + OIDIfInOctets = ".1.3.6.1.2.1.2.2.1.10" // 接收字节数 + OIDIfInUcastPkts = ".1.3.6.1.2.1.2.2.1.11" // 接收单播包数 + OIDIfInErrors = ".1.3.6.1.2.1.2.2.1.14" // 接收错误数 + OIDIfOutOctets = ".1.3.6.1.2.1.2.2.1.16" // 发送字节数 + OIDIfOutUcastPkts = ".1.3.6.1.2.1.2.2.1.17" // 发送单播包数 + OIDIfOutErrors = ".1.3.6.1.2.1.2.2.1.20" // 发送错误数 +) + +// HostResource MIB OID +const ( + OIDHrSystemUptime = ".1.3.6.1.2.1.25.1.1.0" // 系统运行时间 + OIDHrProcessorLoad = ".1.3.6.1.2.1.25.3.3.1.2" // CPU负载 + OIDHrStorageTable = ".1.3.6.1.2.1.25.2.3" // 存储表 + OIDHrStorageDescr = ".1.3.6.1.2.1.25.2.3.1.3" // 存储描述 + OIDHrStorageSize = ".1.3.6.1.2.1.25.2.3.1.5" // 存储大小 + OIDHrStorageUsed = ".1.3.6.1.2.1.25.2.3.1.6" // 已用存储 +) diff --git a/backend/pkg/zabbix/client.go b/backend/pkg/zabbix/client.go new file mode 100644 index 0000000..84c4395 --- /dev/null +++ b/backend/pkg/zabbix/client.go @@ -0,0 +1,756 @@ +package zabbix + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "sync" + "time" +) + +// ============================================ +// Zabbix API 客户端 +// ============================================ + +// Client Zabbix API客户端 +// 封装对Zabbix JSON-RPC API的调用,支持认证、主机管理、监控项查询等 +// 兼容 Zabbix 5.0 / 6.0 / 7.0 版本 +type Client struct { + baseURL string // Zabbix API地址,如 http://zabbix.example.com/api_jsonrpc.php + username string // Zabbix用户名 + password string // Zabbix密码 + authToken string // 认证令牌 + httpClient *http.Client // HTTP客户端 + mu sync.RWMutex // 保护authToken的互斥锁 +} + +// ClientOption 客户端配置选项 +type ClientOption func(*Client) + +// WithHTTPTimeout 设置HTTP超时 +func WithHTTPTimeout(timeout time.Duration) ClientOption { + return func(c *Client) { + c.httpClient.Timeout = timeout + } +} + +// WithToken 使用已有认证令牌 +func WithToken(token string) ClientOption { + return func(c *Client) { + c.authToken = token + } +} + +// NewClient 创建Zabbix客户端 +// baseURL: Zabbix API地址,如 http://zabbix.example.com/api_jsonrpc.php +func NewClient(baseURL, username, password string, opts ...ClientOption) *Client { + c := &Client{ + baseURL: baseURL, + username: username, + password: password, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } + for _, opt := range opts { + opt(c) + } + return c +} + +// ============================================ +// Zabbix JSON-RPC 请求/响应结构 +// ============================================ + +// Request Zabbix API请求 +type Request struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params interface{} `json:"params"` + Auth string `json:"auth,omitempty"` + ID int `json:"id"` +} + +// Response Zabbix API响应 +type Response struct { + JSONRPC string `json:"jsonrpc"` + Result json.RawMessage `json:"result,omitempty"` + Error *APIError `json:"error,omitempty"` + ID int `json:"id"` +} + +// APIError Zabbix API错误 +type APIError struct { + Code int `json:"code"` + Message string `json:"message"` + Data string `json:"data"` +} + +// Error 实现error接口 +func (e *APIError) Error() string { + return fmt.Sprintf("Zabbix API Error [code:%d]: %s - %s", e.Code, e.Message, e.Data) +} + +// LoginResult 登录结果 +type LoginResult struct { + Token string `json:"result"` +} + +// ============================================ +// 内部请求方法 +// ============================================ + +var requestIDCounter int +var requestIDMu sync.Mutex + +func nextRequestID() int { + requestIDMu.Lock() + defer requestIDMu.Unlock() + requestIDCounter++ + return requestIDCounter +} + +// call 执行JSON-RPC调用 +func (c *Client) call(ctx context.Context, method string, params interface{}, result interface{}) error { + req := Request{ + JSONRPC: "2.0", + Method: method, + Params: params, + ID: nextRequestID(), + } + + // 非登录请求需要认证 + if method != "user.login" { + c.mu.RLock() + token := c.authToken + c.mu.RUnlock() + if token == "" { + return fmt.Errorf("not authenticated, call Auth() first") + } + req.Auth = token + } + + body, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("marshal request failed: %w", err) + } + + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("create request failed: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json-rpc") + + httpResp, err := c.httpClient.Do(httpReq) + if err != nil { + return fmt.Errorf("HTTP request failed: %w", err) + } + defer httpResp.Body.Close() + + respBody, err := io.ReadAll(httpResp.Body) + if err != nil { + return fmt.Errorf("read response body failed: %w", err) + } + + if httpResp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected HTTP status: %d, body: %s", httpResp.StatusCode, string(respBody)) + } + + var resp Response + if err := json.Unmarshal(respBody, &resp); err != nil { + return fmt.Errorf("unmarshal response failed: %w", err) + } + + if resp.Error != nil { + // 处理会话过期,自动重试一次 + if resp.Error.Code == -32602 || resp.Error.Message == "Session terminated" { + if err := c.Auth(ctx); err != nil { + return fmt.Errorf("session expired, re-auth failed: %w", err) + } + // 重试请求 + return c.call(ctx, method, params, result) + } + return resp.Error + } + + if result != nil { + if err := json.Unmarshal(resp.Result, result); err != nil { + return fmt.Errorf("unmarshal result failed: %w", err) + } + } + + return nil +} + +// ============================================ +// 认证 +// ============================================ + +// Auth 执行Zabbix认证,获取会话令牌 +// 支持 Zabbix 5.0/6.0/7.0 的认证方式 +func (c *Client) Auth(ctx context.Context) error { + // Zabbix 6.0+ 使用 user.login + params := map[string]interface{}{ + "username": c.username, + "password": c.password, + } + + var result string + if err := c.call(ctx, "user.login", params, &result); err != nil { + // 尝试旧版API (Zabbix 5.0) + oldParams := map[string]interface{}{ + "user": c.username, + "password": c.password, + } + if err := c.call(ctx, "user.login", oldParams, &result); err != nil { + return fmt.Errorf("authentication failed: %w", err) + } + } + + c.mu.Lock() + c.authToken = result + c.mu.Unlock() + + return nil +} + +// Logout 登出并清除会话 +func (c *Client) Logout(ctx context.Context) error { + if err := c.call(ctx, "user.logout", []string{}, nil); err != nil { + return fmt.Errorf("logout failed: %w", err) + } + c.mu.Lock() + c.authToken = "" + c.mu.Unlock() + return nil +} + +// GetAuthToken 获取当前认证令牌 +func (c *Client) GetAuthToken() string { + c.mu.RLock() + defer c.mu.RUnlock() + return c.authToken +} + +// ============================================ +// 主机管理 +// ============================================ + +// Host 主机信息 +type Host struct { + HostID string `json:"hostid"` + Host string `json:"host"` + Name string `json:"name"` + Status string `json:"status"` // 0=监控 1=未监控 + Available string `json:"available"` // 0=未知 1=可用 2=不可用 + Error string `json:"error"` // 可用性错误信息 + Interfaces []HostInterface `json:"interfaces,omitempty"` + Groups []HostGroup `json:"groups,omitempty"` + Templates []Template `json:"parentTemplates,omitempty"` + Tags []Tag `json:"tags,omitempty"` + Macros []Macro `json:"macros,omitempty"` + Inventory map[string]string `json:"inventory,omitempty"` + Description string `json:"description"` + ProxyHostID string `json:"proxy_hostid"` +} + +// HostInterface 主机接口 +type HostInterface struct { + InterfaceID string `json:"interfaceid"` + HostID string `json:"hostid"` + Main string `json:"main"` // 0=否 1=是 + Type string `json:"type"` // 1=Agent 2=SNMP 3=IPMI 4=JMX + UseIP string `json:"useip"` // 0=DNS 1=IP + IP string `json:"ip"` + DNS string `json:"dns"` + Port string `json:"port"` +} + +// HostGroup 主机组 +type HostGroup struct { + GroupID string `json:"groupid"` + Name string `json:"name"` +} + +// Template 模板 +type Template struct { + TemplateID string `json:"templateid"` + Name string `json:"name"` +} + +// Tag 标签 +type Tag struct { + Tag string `json:"tag"` + Value string `json:"value"` +} + +// Macro 宏 +type Macro struct { + Macro string `json:"macro"` + Value string `json:"value"` + TypeID string `json:"type,string"` +} + +// GetHostsParams 获取主机列表参数 +type GetHostsParams struct { + HostIDs []string `json:"hostids,omitempty"` + GroupIDs []string `json:"groupids,omitempty"` + TemplateIDs []string `json:"templateids,omitempty"` + ProxyIDs []string `json:"proxyids,omitempty"` + Status int `json:"status,omitempty"` // 0=监控 1=未监控 + Filter map[string]interface{} `json:"filter,omitempty"` + Search map[string]interface{} `json:"search,omitempty"` + SearchInventory map[string]interface{} `json:"searchInventory,omitempty"` + Output []string `json:"output,omitempty"` + SelectInterfaces []string `json:"selectInterfaces,omitempty"` + SelectGroups []string `json:"selectGroups,omitempty"` + SelectParentTemplates []string `json:"selectParentTemplates,omitempty"` + SelectTags []string `json:"selectTags,omitempty"` + SelectMacros []string `json:"selectMacros,omitempty"` + SelectInventory []string `json:"selectInventory,omitempty"` + Limit int `json:"limit,omitempty"` + SortField []string `json:"sortfield,omitempty"` + SortOrder []string `json:"sortorder,omitempty"` +} + +// GetHosts 获取主机列表 +// params: 查询参数,为nil时返回所有主机 +func (c *Client) GetHosts(ctx context.Context, params *GetHostsParams) ([]Host, error) { + if params == nil { + params = &GetHostsParams{} + } + if len(params.Output) == 0 { + params.Output = []string{"hostid", "host", "name", "status", "available", "error", "description", "proxy_hostid"} + } + + var result []Host + if err := c.call(ctx, "host.get", params, &result); err != nil { + return nil, fmt.Errorf("get hosts failed: %w", err) + } + return result, nil +} + +// GetHostByID 通过ID获取主机 +func (c *Client) GetHostByID(ctx context.Context, hostID string) (*Host, error) { + params := &GetHostsParams{ + HostIDs: []string{hostID}, + SelectInterfaces: []string{"interfaceid", "hostid", "main", "type", "useip", "ip", "dns", "port"}, + SelectGroups: []string{"groupid", "name"}, + SelectParentTemplates: []string{"templateid", "name"}, + SelectTags: []string{"tag", "value"}, + } + + hosts, err := c.GetHosts(ctx, params) + if err != nil { + return nil, err + } + if len(hosts) == 0 { + return nil, fmt.Errorf("host not found: %s", hostID) + } + return &hosts[0], nil +} + +// ============================================ +// 监控项管理 +// ============================================ + +// Item 监控项信息 +type Item struct { + ItemID string `json:"itemid"` + HostID string `json:"hostid"` + Name string `json:"name"` + Key string `json:"key_"` + Type string `json:"type"` // 0=Zabbix Agent 1=SNMPv1 2=Zabbix Trapper ... + ValueType string `json:"value_type"` // 0=Float 1=String 2=Log 3=Unsigned 4=Text + Delay string `json:"delay"` // 更新间隔 + History string `json:"history"` // 历史数据保留时长 + Trends string `json:"trends"` // 趋势数据保留时长 + Status string `json:"status"` // 0=启用 1=禁用 + Units string `json:"units"` // 单位 + LastValue string `json:"lastvalue,omitempty"` + PrevValue string `json:"prevvalue,omitempty"` + LastClock string `json:"lastclock,omitempty"` + Description string `json:"description"` + Error string `json:"error"` + Tags []Tag `json:"tags,omitempty"` + // Zabbix 6.0+ + MasterItemID string `json:"master_itemid,omitempty"` + Timeout string `json:"timeout,omitempty"` + URL string `json:"url,omitempty"` +} + +// GetItemsParams 获取监控项参数 +type GetItemsParams struct { + ItemIDs []string `json:"itemids,omitempty"` + HostIDs []string `json:"hostids,omitempty"` + GroupIDs []string `json:"groupids,omitempty"` + TemplateIDs []string `json:"templateids,omitempty"` + Filter map[string]interface{} `json:"filter,omitempty"` + Search map[string]interface{} `json:"search,omitempty"` + Output []string `json:"output,omitempty"` + SelectTags []string `json:"selectTags,omitempty"` + Limit int `json:"limit,omitempty"` + SortField []string `json:"sortfield,omitempty"` + SortOrder []string `json:"sortorder,omitempty"` + WebItems int `json:"webitems,omitempty"` +} + +// GetItems 获取监控项列表 +// params: 查询参数 +func (c *Client) GetItems(ctx context.Context, params *GetItemsParams) ([]Item, error) { + if params == nil { + params = &GetItemsParams{} + } + if len(params.Output) == 0 { + params.Output = []string{"itemid", "hostid", "name", "key_", "type", "value_type", "delay", "history", "trends", "status", "units", "lastvalue", "lastclock", "description", "error"} + } + + var result []Item + if err := c.call(ctx, "item.get", params, &result); err != nil { + return nil, fmt.Errorf("get items failed: %w", err) + } + return result, nil +} + +// GetItemsByHost 获取指定主机的所有监控项 +func (c *Client) GetItemsByHost(ctx context.Context, hostID string) ([]Item, error) { + params := &GetItemsParams{ + HostIDs: []string{hostID}, + SortField: []string{"name"}, + } + return c.GetItems(ctx, params) +} + +// ============================================ +// 触发器管理 +// ============================================ + +// Trigger 触发器信息 +type Trigger struct { + TriggerID string `json:"triggerid"` + Expression string `json:"expression"` + Description string `json:"description"` + URL string `json:"url"` + Status string `json:"status"` // 0=启用 1=禁用 + Value string `json:"value"` // 0=正常 1=问题 + Priority string `json:"priority"` // 0=未分类 1=信息 2=警告 3=一般 4=严重 5=灾难 + LastChange string `json:"lastchange"` + Comments string `json:"comments"` + Error string `json:"error"` + TemplateID string `json:"templateid"` + Type string `json:"type"` // 0=普通 1=多重问题 + Tags []Tag `json:"tags,omitempty"` + Hosts []Host `json:"hosts,omitempty"` + // Zabbix 6.0+ + OpData string `json:"opdata,omitempty"` + EventName string `json:"event_name,omitempty"` + // Zabbix 7.0+ + State string `json:"state,omitempty"` // 0=正常 1=未知 +} + +// GetTriggersParams 获取触发器参数 +type GetTriggersParams struct { + TriggerIDs []string `json:"triggerids,omitempty"` + HostIDs []string `json:"hostids,omitempty"` + GroupIDs []string `json:"groupids,omitempty"` + TemplateIDs []string `json:"templateids,omitempty"` + ItemIDs []string `json:"itemids,omitempty"` + Filter map[string]interface{} `json:"filter,omitempty"` + Search map[string]interface{} `json:"search,omitempty"` + Output []string `json:"output,omitempty"` + SelectHosts []string `json:"selectHosts,omitempty"` + SelectItems []string `json:"selectItems,omitempty"` + SelectTags []string `json:"selectTags,omitempty"` + ExpandExpression bool `json:"expandExpression,omitempty"` + ExpandDescription bool `json:"expandDescription,omitempty"` + SkipDependent bool `json:"skipDependent,omitempty"` + Active bool `json:"active,omitempty"` + WithLastEventUnacknowledged bool `json:"withLastEventUnacknowledged,omitempty"` + Limit int `json:"limit,omitempty"` + SortField []string `json:"sortfield,omitempty"` + SortOrder []string `json:"sortorder,omitempty"` + MinSeverity int `json:"min_severity,omitempty"` +} + +// GetTriggers 获取触发器列表 +// params: 查询参数 +func (c *Client) GetTriggers(ctx context.Context, params *GetTriggersParams) ([]Trigger, error) { + if params == nil { + params = &GetTriggersParams{} + } + if len(params.Output) == 0 { + params.Output = []string{"triggerid", "expression", "description", "url", "status", "value", "priority", "lastchange", "comments", "error"} + } + + var result []Trigger + if err := c.call(ctx, "trigger.get", params, &result); err != nil { + return nil, fmt.Errorf("get triggers failed: %w", err) + } + return result, nil +} + +// GetTriggersByHost 获取指定主机的触发器 +func (c *Client) GetTriggersByHost(ctx context.Context, hostID string, onlyProblems bool) ([]Trigger, error) { + params := &GetTriggersParams{ + HostIDs: []string{hostID}, + SelectHosts: []string{"hostid", "host", "name"}, + SelectTags: []string{"tag", "value"}, + SortField: []string{"priority"}, + SortOrder: []string{"DESC"}, + } + if onlyProblems { + params.Filter = map[string]interface{}{ + "value": "1", // 只查询有问题状态的触发器 + } + } + return c.GetTriggers(ctx, params) +} + +// ============================================ +// 历史数据查询 +// ============================================ + +// HistoryValue 历史数据值 +type HistoryValue struct { + ItemID string `json:"itemid"` + Clock string `json:"clock"` // 时间戳 + Value string `json:"value"` // 值 + NS string `json:"ns"` // 纳秒 +} + +// HistoryData 解析后的历史数据 +type HistoryData struct { + ItemID string + Timestamp time.Time + Value float64 +} + +// GetHistoryParams 获取历史数据参数 +type GetHistoryParams struct { + History int `json:"history"` // 值类型: 0=Float 1=String 2=Log 3=Unsigned 4=Text 5=Snapshot + ItemIDs []string `json:"itemids"` + HostIDs []string `json:"hostids,omitempty"` + TimeFrom int64 `json:"time_from,omitempty"` + TimeTill int64 `json:"time_till,omitempty"` + Limit int `json:"limit,omitempty"` + SortField string `json:"sortfield,omitempty"` // clock/itemid + SortOrder string `json:"sortorder,omitempty"` // ASC/DESC +} + +// GetHistory 获取历史数据 +// params: 查询参数 +func (c *Client) GetHistory(ctx context.Context, params *GetHistoryParams) ([]HistoryValue, error) { + if params == nil || len(params.ItemIDs) == 0 { + return nil, fmt.Errorf("item IDs are required") + } + + var result []HistoryValue + if err := c.call(ctx, "history.get", params, &result); err != nil { + return nil, fmt.Errorf("get history failed: %w", err) + } + return result, nil +} + +// GetHistoryFloat 获取浮点型历史数据 +func (c *Client) GetHistoryFloat(ctx context.Context, itemID string, from, till time.Time, limit int) ([]HistoryValue, error) { + params := &GetHistoryParams{ + History: 0, // Float + ItemIDs: []string{itemID}, + TimeFrom: from.Unix(), + TimeTill: till.Unix(), + Limit: limit, + SortField: "clock", + SortOrder: "DESC", + } + return c.GetHistory(ctx, params) +} + +// GetHistoryUnsigned 获取无符号整型历史数据 +func (c *Client) GetHistoryUnsigned(ctx context.Context, itemID string, from, till time.Time, limit int) ([]HistoryValue, error) { + params := &GetHistoryParams{ + History: 3, // Unsigned + ItemIDs: []string{itemID}, + TimeFrom: from.Unix(), + TimeTill: till.Unix(), + Limit: limit, + SortField: "clock", + SortOrder: "DESC", + } + return c.GetHistory(ctx, params) +} + +// GetHistoryByItems 批量获取多个监控项的历史数据 +func (c *Client) GetHistoryByItems(ctx context.Context, itemIDs []string, valueType int, from, till time.Time, limit int) ([]HistoryValue, error) { + params := &GetHistoryParams{ + History: valueType, + ItemIDs: itemIDs, + TimeFrom: from.Unix(), + TimeTill: till.Unix(), + Limit: limit, + SortField: "clock", + SortOrder: "DESC", + } + return c.GetHistory(ctx, params) +} + +// ============================================ +// 主机组管理 +// ============================================ + +// GetGroupsParams 获取主机组参数 +type GetGroupsParams struct { + GroupIDs []string `json:"groupids,omitempty"` + HostIDs []string `json:"hostids,omitempty"` + TemplateIDs []string `json:"templateids,omitempty"` + TriggerIDs []string `json:"triggerids,omitempty"` + Filter map[string]interface{} `json:"filter,omitempty"` + Search map[string]interface{} `json:"search,omitempty"` + Output []string `json:"output,omitempty"` + SelectHosts []string `json:"selectHosts,omitempty"` + SelectTemplates []string `json:"selectTemplates,omitempty"` + Limit int `json:"limit,omitempty"` + SortField []string `json:"sortfield,omitempty"` + SortOrder []string `json:"sortorder,omitempty"` +} + +// GetGroups 获取主机组列表 +// params: 查询参数 +func (c *Client) GetGroups(ctx context.Context, params *GetGroupsParams) ([]HostGroup, error) { + if params == nil { + params = &GetGroupsParams{} + } + if len(params.Output) == 0 { + params.Output = []string{"groupid", "name"} + } + + var result []HostGroup + if err := c.call(ctx, "hostgroup.get", params, &result); err != nil { + return nil, fmt.Errorf("get groups failed: %w", err) + } + return result, nil +} + +// GetGroupByID 通过ID获取主机组 +func (c *Client) GetGroupByID(ctx context.Context, groupID string) (*HostGroup, error) { + params := &GetGroupsParams{ + GroupIDs: []string{groupID}, + } + groups, err := c.GetGroups(ctx, params) + if err != nil { + return nil, err + } + if len(groups) == 0 { + return nil, fmt.Errorf("group not found: %s", groupID) + } + return &groups[0], nil +} + +// ============================================ +// 趋势数据查询 +// ============================================ + +// TrendValue 趋势数据 +type TrendValue struct { + ItemID string `json:"itemid"` + Clock string `json:"clock"` // 小时开始时间戳 + Num string `json:"num"` // 数据点数 + ValueMin string `json:"value_min"` + ValueAvg string `json:"value_avg"` + ValueMax string `json:"value_max"` +} + +// GetTrendsParams 获取趋势数据参数 +type GetTrendsParams struct { + ItemIDs []string `json:"itemids"` + TimeFrom int64 `json:"time_from,omitempty"` + TimeTill int64 `json:"time_till,omitempty"` + Limit int `json:"limit,omitempty"` +} + +// GetTrends 获取趋势数据 +func (c *Client) GetTrends(ctx context.Context, params *GetTrendsParams) ([]TrendValue, error) { + if params == nil || len(params.ItemIDs) == 0 { + return nil, fmt.Errorf("item IDs are required") + } + + var result []TrendValue + if err := c.call(ctx, "trend.get", params, &result); err != nil { + return nil, fmt.Errorf("get trends failed: %w", err) + } + return result, nil +} + +// ============================================ +// 工具方法 +// ============================================ + +// GetVersion 获取Zabbix服务器版本 +func (c *Client) GetVersion(ctx context.Context) (string, error) { + var result string + if err := c.call(ctx, "apiinfo.version", nil, &result); err != nil { + return "", fmt.Errorf("get version failed: %w", err) + } + return result, nil +} + +// GetServerTime 获取Zabbix服务器时间 +func (c *Client) GetServerTime(ctx context.Context) (time.Time, error) { + var result struct { + TS int64 `json:"ts"` + } + if err := c.call(ctx, "script.getscriptsbyevents", map[string]interface{}{}, &result); err != nil { + // 备选方案:使用配置查询 + return time.Time{}, fmt.Errorf("get server time not directly supported: %w", err) + } + return time.Unix(result.TS, 0), nil +} + +// ParseTimestamp 解析Zabbix时间戳字符串 +func ParseTimestamp(s string) (time.Time, error) { + ts, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return time.Time{}, fmt.Errorf("parse timestamp failed: %w", err) + } + return time.Unix(ts, 0), nil +} + +// GetItemValueTypeName 获取值类型名称 +func GetItemValueTypeName(valueType int) string { + switch valueType { + case 0: + return "float" + case 1: + return "string" + case 2: + return "log" + case 3: + return "unsigned" + case 4: + return "text" + default: + return "unknown" + } +} + +// GetPriorityName 获取触发器优先级名称 +func GetPriorityName(priority int) string { + switch priority { + case 0: + return "not_classified" + case 1: + return "information" + case 2: + return "warning" + case 3: + return "average" + case 4: + return "high" + case 5: + return "disaster" + default: + return "unknown" + } +} diff --git a/frontend/src/components/agent/AgentWizard.tsx b/frontend/src/components/agent/AgentWizard.tsx index 5c8b9f2..4076496 100644 --- a/frontend/src/components/agent/AgentWizard.tsx +++ b/frontend/src/components/agent/AgentWizard.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { toast } from 'sonner'; import { useUserStore } from '@/store'; import { Dialog, @@ -10,11 +9,19 @@ import { } from '@/components/ui/dialog'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; -import { Copy, Check, Terminal, Download, Settings, Play, Cpu } from 'lucide-react'; +import { Copy, Check, Terminal, Download, Settings, Play, Cpu, Zap, Loader2, Server } from 'lucide-react'; +import { serverApi } from '@/services/api'; +import { toast } from 'sonner'; interface AgentWizardProps { open: boolean; onOpenChange: (open: boolean) => void; + /** 可选:从服务器列表传入的目标服务器信息 */ + server?: { id: number; hostname: string; ip: string } | null; + /** SSH一键安装成功回调 */ + onInstalled?: (serverId: number) => void; + /** SSH一键安装失败回调 */ + onFailed?: (serverId: number, message: string) => void; } /** 自动检测地址:origin(通过 Nginx 80 代理)用于安装脚本下载,backend 8080 用于 agent 直连 */ @@ -28,11 +35,13 @@ function detectServerAddress() { return { origin: 'http://127.0.0.1', backend: 'http://127.0.0.1:8080' }; } -export function AgentWizard({ open, onOpenChange }: AgentWizardProps) { +export function AgentWizard({ open, onOpenChange, server, onInstalled, onFailed }: AgentWizardProps) { const token = useUserStore((s) => s.token) || ''; const { origin, backend } = detectServerAddress(); const [copied, setCopied] = useState(false); const [showManualInput, setShowManualInput] = useState(false); + const [sshInstalling, setSshInstalling] = useState(false); + const [activeTab, setActiveTab] = useState('auto'); // install.sh 通过 Nginx 代理下载(origin:80 → /agent/ → backend:8080) const installCommand = `curl -fsSL ${origin}/agent/install.sh | bash -s -- --server ${backend} --token ${token}`; @@ -132,22 +141,58 @@ export function AgentWizard({ open, onOpenChange }: AgentWizardProps) { }, ]; + // SSH 一键安装(当从服务器列表传入 server 上下文时可用) + const handleSshInstall = async () => { + if (!server || !token) return; + setSshInstalling(true); + try { + const res: any = await serverApi.installAgent(server.id, token); + const installed = res?.installed || false; + if (installed) { + toast.success(`${server.hostname} Agent 安装成功`); + onInstalled?.(server.id); + onOpenChange(false); + } else { + toast.warning(`${server.hostname}: Agent 安装命令已执行,但服务未检测到运行`); + onInstalled?.(server.id); + onOpenChange(false); + } + } catch (e: any) { + const msg = e?.message || '安装失败'; + toast.error(`${server.hostname} ${msg}`); + onFailed?.(server.id, msg); + } finally { + setSshInstalling(false); + } + }; + return ( - 部署 Agent + + {server ? `部署 Agent → ${server.hostname}` : '部署 Agent'} + - 在目标主机上执行以下命令即可完成 Agent 安装与注册 + {server + ? `目标主机 ${server.hostname} (${server.ip}),选择安装方式:` + : '在目标主机上执行以下命令即可完成 Agent 安装与注册' + } - + 安装Agent + {server && ( + + + SSH一键安装 + + )} 手动安装 @@ -195,6 +240,47 @@ export function AgentWizard({ open, onOpenChange }: AgentWizardProps) { + {/* SSH 一键安装 — 仅当从服务器列表传入时显示 */} + {server && ( + +
+
+ +
+

{server.hostname}

+ {server.ip} +
+
+

+ 通过 SSH 远程连接到目标主机,自动下载并安装 Agent,完成后立即启动运行。 +

+ +
+
+ + + 系统将通过 SSH 连接 {server.ip},执行安装脚本。请确保服务器 SSH 凭据已正确配置(编辑服务器可修改密码/密钥)。 + +
+
+ )} + {/* 手动安装 */}
diff --git a/frontend/src/components/server/ServerManager.tsx b/frontend/src/components/server/ServerManager.tsx index ad47e5c..2a608e5 100644 --- a/frontend/src/components/server/ServerManager.tsx +++ b/frontend/src/components/server/ServerManager.tsx @@ -37,6 +37,9 @@ import { Cloud, FileSpreadsheet, RefreshCw, + Rocket, + ChevronLeft, + ChevronRight, Cpu, } from 'lucide-react'; import { serverApi } from '@/services/api'; @@ -73,44 +76,21 @@ export function ServerManager() { const [terminalOpen, setTerminalOpen] = useState(false); const [terminalServer, setTerminalServer] = useState<{ id: number; name: string; ip: string } | null>(null); + // 分页状态 + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [total, setTotal] = useState(0); + // Agent 安装向导状态 const [agentWizardOpen, setAgentWizardOpen] = useState(false); + const [wizardServer, setWizardServer] = useState(null); // Agent 安装状态: { [serverId]: 'installing' | 'success' | 'failed' } const [installingIds, setInstallingIds] = useState>(new Set()); const [installResults, setInstallResults] = useState>(new Map()); - const openInstallAgent = async (server: any) => { - const token = useUserStore.getState().token; - if (!token) { toast.error('请先登录'); return; } - - setInstallingIds(prev => new Set(prev).add(server.id)); - try { - const res: any = await serverApi.installAgent(server.id, token); - const installed = res?.installed || false; - setInstallResults(prev => { - const next = new Map(prev); - next.set(server.id, { - success: installed, - message: installed ? '安装成功 ✓' : '命令已执行,Agent 未检测到运行' - }); - return next; - }); - if (installed) toast.success(`${server.hostname} Agent 安装成功`); - else toast.warning(`${server.hostname}: Agent 安装命令已执行`); - } catch (e: any) { - setInstallResults(prev => { - const next = new Map(prev); - next.set(server.id, { success: false, message: e?.message || '安装失败' }); - return next; - }); - toast.error(`${server.hostname} 安装失败: ${e?.message || '未知错误'}`); - } finally { - setInstallingIds(prev => { - const next = new Set(prev); - next.delete(server.id); - return next; - }); - } + const openInstallAgent = (server: any) => { + setWizardServer(server); + setAgentWizardOpen(true); }; const handleBatchInstallAgents = async () => { @@ -151,11 +131,17 @@ export function ServerManager() { setTerminalOpen(true); }; - const loadData = async () => { + const loadData = async (kw?: string, st?: string) => { try { setLoading(true); - const res: any = await serverApi.getServers(); + const q = kw ?? searchQuery; + const s = st ?? statusFilter; + const params: any = { page, page_size: pageSize }; + if (q) params.keyword = q; + if (s && s !== 'all') params.status = s; + const res: any = await serverApi.getServers(params); setServers(res?.list || []); + setTotal(res?.total || 0); } catch (e) { console.error('加载服务器列表失败:', e); setServers([]); @@ -164,9 +150,19 @@ export function ServerManager() { } }; + // 搜索/筛选变化时重置到第1页 + const handleSearch = (val: string) => { + setSearchQuery(val); + setPage(1); + }; + const handleStatusFilter = (val: string) => { + setStatusFilter(val); + setPage(1); + }; + useEffect(() => { loadData(); - }, []); + }, [page, pageSize]); // WebSocket 实时接收 Agent 指标推送 // Logan.Li const wsRef = useRef(null); @@ -244,20 +240,12 @@ export function ServerManager() { }; }, []); - const filteredServers = servers.filter((server) => { - const matchesSearch = - (server.hostname || '').toLowerCase().includes(searchQuery.toLowerCase()) || - (server.ip || '').includes(searchQuery) || - (server.public_ip || '').includes(searchQuery); - const matchesStatus = statusFilter === 'all' || server.status === statusFilter; - return matchesSearch && matchesStatus; - }); - + // 导出 CSV const toggleSelectAll = () => { - if (selectedServers.length === filteredServers.length) { + if (selectedServers.length === servers.length) { setSelectedServers([]); } else { - setSelectedServers(filteredServers.map((s) => s.id)); + setSelectedServers(servers.map((s) => s.id)); } }; @@ -382,12 +370,12 @@ export function ServerManager() { // 导出 CSV — 将当前服务器列表导出为 CSV 文件 // Logan.Li const handleExport = () => { - if (filteredServers.length === 0) { + if (servers.length === 0) { toast.error('没有可导出的数据'); return; } const headers = ['主机名', 'IP地址', '公网IP', '端口', '操作系统', 'CPU(核)', '内存(GB)', '磁盘(GB)', '状态', '标签']; - const rows = filteredServers.map((s) => { + const rows = servers.map((s) => { const memGB = s.memory ? Math.round(s.memory / 1024) : 0; const tags = getTags(s).join(';'); return [s.hostname || '', s.ip || '', s.public_ip || '', s.port || 22, s.os || '', @@ -403,7 +391,7 @@ export function ServerManager() { a.download = `servers_${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(url); - toast.success(`已导出 ${filteredServers.length} 台服务器`); + toast.success(`已导出 ${servers.length} 台服务器`); }; const colSpanCount = 8; @@ -420,7 +408,7 @@ export function ServerManager() {
- @@ -534,7 +522,7 @@ export function ServerManager() { 0 + selectedServers.length === servers.length && servers.length > 0 } onCheckedChange={toggleSelectAll} className="border-[var(--dg-border)]" @@ -558,14 +546,14 @@ export function ServerManager() { 加载中... - ) : filteredServers.length === 0 ? ( + ) : servers.length === 0 ? ( 暂无服务器数据 ) : ( - filteredServers.map((server) => ( + servers.map((server) => ( + + {/* 分页 */} +
+
+ 每页 + + 条 + 共 {total} 条 +
+
+ + + {page} / {Math.max(1, Math.ceil(total / pageSize))} + + +
+
@@ -616,7 +646,30 @@ export function ServerManager() { /> {/* Agent 安装向导 */} - + { + setAgentWizardOpen(open); + if (!open) setWizardServer(null); + }} + server={wizardServer} + onInstalled={(serverId) => { + setInstallResults(prev => { + const next = new Map(prev); + next.set(serverId, { success: true, message: 'Agent 已安装' }); + return next; + }); + // 刷新列表以更新 agent_status + loadData(); + }} + onFailed={(serverId, msg) => { + setInstallResults(prev => { + const next = new Map(prev); + next.set(serverId, { success: false, message: msg || '安装失败' }); + return next; + }); + }} + /> {/* 终端 Dialog */}
+ {/* Agent 安装按钮 — agent_status=online 时隐藏 */} + {server.agent_status !== 'online' && ( + + )} @@ -177,24 +200,6 @@ export function ServerTableRow({ 终端 - onInstallAgent(server)} - disabled={installing} - > - {installing ? ( - - ) : installResult ? ( - installResult.success ? ( - - ) : ( - - ) - ) : ( - - )} - {installing ? '安装中...' : installResult ? installResult.message : '安装Agent'} -