feat: 监控架构优化 — 模型层 + Prometheus/Zabbix/SNMP pkg + Dockerfile升级Go1.24
整合 /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层因类型不兼容暂时跳过,模型和客户端包已编译通过。
This commit is contained in:
1 parent
f3a9e5e8d0
commit
24fa17c2a8
13 files changed
+3427
-98
No files matched your search
+3
-5
@@ -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/
|
||||
|
||||
+6
-7
@@ -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
|
||||
|
||||
+12
-5
@@ -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=
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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" // 已用存储
|
||||
)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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) || '<YOUR_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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl bg-slate-900 border-slate-800 text-slate-100">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100">部署 Agent</DialogTitle>
|
||||
<DialogTitle className="text-slate-100">
|
||||
{server ? `部署 Agent → ${server.hostname}` : '部署 Agent'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-400">
|
||||
在目标主机上执行以下命令即可完成 Agent 安装与注册
|
||||
{server
|
||||
? `目标主机 ${server.hostname} (${server.ip}),选择安装方式:`
|
||||
: '在目标主机上执行以下命令即可完成 Agent 安装与注册'
|
||||
}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs defaultValue="auto" className="w-full">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="bg-slate-800 border border-slate-700">
|
||||
<TabsTrigger value="auto" className="data-[state=active]:bg-slate-700">
|
||||
<Terminal className="h-4 w-4 mr-1.5" />
|
||||
安装Agent
|
||||
</TabsTrigger>
|
||||
{server && (
|
||||
<TabsTrigger value="ssh" className="data-[state=active]:bg-slate-700">
|
||||
<Zap className="h-4 w-4 mr-1.5" />
|
||||
SSH一键安装
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="manual" className="data-[state=active]:bg-slate-700">
|
||||
手动安装
|
||||
</TabsTrigger>
|
||||
@@ -195,6 +240,47 @@ export function AgentWizard({ open, onOpenChange }: AgentWizardProps) {
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* SSH 一键安装 — 仅当从服务器列表传入时显示 */}
|
||||
{server && (
|
||||
<TabsContent value="ssh" className="mt-4 space-y-4">
|
||||
<div className="rounded-lg bg-slate-950 border border-slate-800 p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Server className="h-5 w-5 text-blue-400" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-200">{server.hostname}</p>
|
||||
<code className="text-xs text-slate-400">{server.ip}</code>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-400 mb-4">
|
||||
通过 SSH 远程连接到目标主机,自动下载并安装 Agent,完成后立即启动运行。
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleSshInstall}
|
||||
disabled={sshInstalling}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
{sshInstalling ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
SSH 连接中,正在安装...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Zap className="h-4 w-4 mr-2" />
|
||||
一键安装 Agent
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 text-xs text-slate-500">
|
||||
<Terminal className="h-3.5 w-3.5 mt-0.5 shrink-0" />
|
||||
<span>
|
||||
系统将通过 SSH 连接 {server.ip},执行安装脚本。请确保服务器 SSH 凭据已正确配置(编辑服务器可修改密码/密钥)。
|
||||
</span>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{/* 手动安装 */}
|
||||
<TabsContent value="manual" className="mt-4 space-y-4">
|
||||
<div className="max-h-[420px] overflow-y-auto space-y-3 pr-1">
|
||||
|
||||
@@ -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<any>(null);
|
||||
// Agent 安装状态: { [serverId]: 'installing' | 'success' | 'failed' }
|
||||
const [installingIds, setInstallingIds] = useState<Set<number>>(new Set());
|
||||
const [installResults, setInstallResults] = useState<Map<number, { success: boolean; message: string }>>(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<WebSocket | null>(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() {
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
|
||||
onClick={loadData}
|
||||
onClick={() => loadData()}
|
||||
>
|
||||
<RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />
|
||||
刷新
|
||||
@@ -485,11 +473,11 @@ export function ServerManager() {
|
||||
<Input
|
||||
placeholder="搜索主机名或IP..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
className="pl-9 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<Select value={statusFilter} onValueChange={handleStatusFilter}>
|
||||
<SelectTrigger className="w-32 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue placeholder="状态" />
|
||||
</SelectTrigger>
|
||||
@@ -534,7 +522,7 @@ export function ServerManager() {
|
||||
<TableHead className="w-12">
|
||||
<Checkbox
|
||||
checked={
|
||||
selectedServers.length === filteredServers.length && filteredServers.length > 0
|
||||
selectedServers.length === servers.length && servers.length > 0
|
||||
}
|
||||
onCheckedChange={toggleSelectAll}
|
||||
className="border-[var(--dg-border)]"
|
||||
@@ -558,14 +546,14 @@ export function ServerManager() {
|
||||
加载中...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : filteredServers.length === 0 ? (
|
||||
) : servers.length === 0 ? (
|
||||
<TableRow className="border-[var(--dg-border)]">
|
||||
<TableCell colSpan={colSpanCount} className="text-center text-[var(--dg-text-muted)] py-12">
|
||||
暂无服务器数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredServers.map((server) => (
|
||||
servers.map((server) => (
|
||||
<ServerTableRow
|
||||
key={server.id}
|
||||
server={server}
|
||||
@@ -583,6 +571,48 @@ export function ServerManager() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-[var(--dg-border)]">
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--dg-text-muted)]">
|
||||
<span>每页</span>
|
||||
<Select value={String(pageSize)} onValueChange={(v) => { setPageSize(Number(v)); setPage(1); }}>
|
||||
<SelectTrigger className="w-16 h-8 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="20">20</SelectItem>
|
||||
<SelectItem value="30">30</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span>条</span>
|
||||
<span className="ml-4">共 {total} 条</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
|
||||
disabled={page <= 1}
|
||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)] px-2">
|
||||
{page} / {Math.max(1, Math.ceil(total / pageSize))}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
|
||||
disabled={page >= Math.ceil(total / pageSize)}
|
||||
onClick={() => setPage(p => p + 1)}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -616,7 +646,30 @@ export function ServerManager() {
|
||||
/>
|
||||
|
||||
{/* Agent 安装向导 */}
|
||||
<AgentWizard open={agentWizardOpen} onOpenChange={setAgentWizardOpen} />
|
||||
<AgentWizard
|
||||
open={agentWizardOpen}
|
||||
onOpenChange={(open) => {
|
||||
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 */}
|
||||
<TerminalDialog
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Cpu, Edit, Loader2, MoreHorizontal, Server as ServerIcon, Terminal, Trash2, CheckCircle, XCircle, Network, HardDrive } from 'lucide-react';
|
||||
import { Cpu, Edit, Loader2, MoreHorizontal, Server as ServerIcon, Terminal, Trash2, CheckCircle, XCircle, Network, HardDrive, Rocket } from 'lucide-react';
|
||||
import { getStatusBadge, getTags, getUsageColor } from './serverHelpers';
|
||||
|
||||
interface ServerTableRowProps {
|
||||
@@ -151,6 +151,29 @@ export function ServerTableRow({
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
{/* Agent 安装按钮 — agent_status=online 时隐藏 */}
|
||||
{server.agent_status !== 'online' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]"
|
||||
onClick={() => onInstallAgent(server)}
|
||||
disabled={installing}
|
||||
title={installResult?.success ? 'Agent 已安装' : installResult ? '安装失败,点击重试' : '安装 Agent'}
|
||||
>
|
||||
{installing ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : installResult ? (
|
||||
installResult.success ? (
|
||||
<CheckCircle className="h-4 w-4 text-emerald-400" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-red-400" />
|
||||
)
|
||||
) : (
|
||||
<Rocket className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]" onClick={() => onOpenTerminal(server)}>
|
||||
<Terminal className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -177,24 +200,6 @@ export function ServerTableRow({
|
||||
<Terminal className="h-4 w-4 mr-2" />
|
||||
终端
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
||||
onClick={() => onInstallAgent(server)}
|
||||
disabled={installing}
|
||||
>
|
||||
{installing ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : installResult ? (
|
||||
installResult.success ? (
|
||||
<CheckCircle className="h-4 w-4 mr-2 text-emerald-400" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 mr-2 text-red-400" />
|
||||
)
|
||||
) : (
|
||||
<Cpu className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{installing ? '安装中...' : installResult ? installResult.message : '安装Agent'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
<DropdownMenuItem
|
||||
className="text-red-400 focus:bg-[var(--dg-bg-hover)] focus:text-red-400 cursor-pointer"
|
||||
|
||||
Reference in new issue
Block a user