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
|
# 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 2: 交叉编译 Agent (Go 1.24, linux-amd64/arm64 + darwin-arm64)
|
||||||
# Stage 3: 运行时 (Alpine)
|
# Stage 3: 运行时 (Alpine)
|
||||||
# 署名 Logan.Li
|
# 署名 Logan.Li
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
# ==================== Stage 1: Backend Builder ====================
|
# ==================== Stage 1: Backend Builder ====================
|
||||||
FROM golang:1.21-alpine AS builder
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -17,9 +17,7 @@ RUN apk add --no-cache git ca-certificates tzdata
|
|||||||
|
|
||||||
ENV TZ=Asia/Shanghai
|
ENV TZ=Asia/Shanghai
|
||||||
ENV GOPROXY=https://goproxy.cn,direct
|
ENV GOPROXY=https://goproxy.cn,direct
|
||||||
# 跳过 toolchain 校验(GOTOOLCHAIN 自动下载 Go 1.22 给 Redka 用)// Logan.Li
|
|
||||||
ENV GONOSUMCHECK=*
|
ENV GONOSUMCHECK=*
|
||||||
ENV GOTOOLCHAIN=go1.22.2
|
|
||||||
|
|
||||||
# 复制后端 go.mod(利用 Docker 层缓存)
|
# 复制后端 go.mod(利用 Docker 层缓存)
|
||||||
COPY backend/go.mod backend/go.sum ./
|
COPY backend/go.mod backend/go.sum ./
|
||||||
@@ -28,7 +26,7 @@ RUN go mod download
|
|||||||
# 复制后端源码
|
# 复制后端源码
|
||||||
COPY backend/ ./
|
COPY backend/ ./
|
||||||
|
|
||||||
# 构建后端二进制(Redka 需要 Go 1.22,由 GOTOOLCHAIN 自动下载)
|
# 构建后端二进制
|
||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||||
-ldflags="-w -s" \
|
-ldflags="-w -s" \
|
||||||
-o ezops ./cmd/
|
-o ezops ./cmd/
|
||||||
|
|||||||
+6
-7
@@ -1,8 +1,6 @@
|
|||||||
module ezops
|
module ezops
|
||||||
|
|
||||||
go 1.22
|
go 1.24.0
|
||||||
|
|
||||||
toolchain go1.22.2
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
@@ -10,11 +8,12 @@ require (
|
|||||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
github.com/gosnmp/gosnmp v1.35.0
|
||||||
github.com/nalgeon/redka v0.2.0
|
github.com/nalgeon/redka v0.2.0
|
||||||
github.com/redis/go-redis/v9 v9.7.0
|
github.com/redis/go-redis/v9 v9.7.0
|
||||||
github.com/spf13/viper v1.18.2
|
github.com/spf13/viper v1.18.2
|
||||||
go.uber.org/zap v1.26.0
|
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/mysql v1.5.2
|
||||||
gorm.io/driver/postgres v1.5.4
|
gorm.io/driver/postgres v1.5.4
|
||||||
gorm.io/driver/sqlite v1.6.0
|
gorm.io/driver/sqlite v1.6.0
|
||||||
@@ -64,9 +63,9 @@ require (
|
|||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/net v0.33.0 // indirect
|
golang.org/x/net v0.48.0 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // 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/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 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
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.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 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
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.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.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.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.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 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
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=
|
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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
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.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.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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/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.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.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.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.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/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
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.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.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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.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.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-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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
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 { useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { useUserStore } from '@/store';
|
import { useUserStore } from '@/store';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -10,11 +9,19 @@ import {
|
|||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Button } from '@/components/ui/button';
|
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 {
|
interface AgentWizardProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
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 直连 */
|
/** 自动检测地址: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' };
|
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 token = useUserStore((s) => s.token) || '<YOUR_TOKEN>';
|
||||||
const { origin, backend } = detectServerAddress();
|
const { origin, backend } = detectServerAddress();
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [showManualInput, setShowManualInput] = 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)
|
// install.sh 通过 Nginx 代理下载(origin:80 → /agent/ → backend:8080)
|
||||||
const installCommand = `curl -fsSL ${origin}/agent/install.sh | bash -s -- --server ${backend} --token ${token}`;
|
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 (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-2xl bg-slate-900 border-slate-800 text-slate-100">
|
<DialogContent className="max-w-2xl bg-slate-900 border-slate-800 text-slate-100">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-slate-100">部署 Agent</DialogTitle>
|
<DialogTitle className="text-slate-100">
|
||||||
|
{server ? `部署 Agent → ${server.hostname}` : '部署 Agent'}
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-slate-400">
|
||||||
在目标主机上执行以下命令即可完成 Agent 安装与注册
|
{server
|
||||||
|
? `目标主机 ${server.hostname} (${server.ip}),选择安装方式:`
|
||||||
|
: '在目标主机上执行以下命令即可完成 Agent 安装与注册'
|
||||||
|
}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<Tabs defaultValue="auto" className="w-full">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<TabsList className="bg-slate-800 border border-slate-700">
|
<TabsList className="bg-slate-800 border border-slate-700">
|
||||||
<TabsTrigger value="auto" className="data-[state=active]:bg-slate-700">
|
<TabsTrigger value="auto" className="data-[state=active]:bg-slate-700">
|
||||||
<Terminal className="h-4 w-4 mr-1.5" />
|
<Terminal className="h-4 w-4 mr-1.5" />
|
||||||
安装Agent
|
安装Agent
|
||||||
</TabsTrigger>
|
</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 value="manual" className="data-[state=active]:bg-slate-700">
|
||||||
手动安装
|
手动安装
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -195,6 +240,47 @@ export function AgentWizard({ open, onOpenChange }: AgentWizardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</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">
|
<TabsContent value="manual" className="mt-4 space-y-4">
|
||||||
<div className="max-h-[420px] overflow-y-auto space-y-3 pr-1">
|
<div className="max-h-[420px] overflow-y-auto space-y-3 pr-1">
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ import {
|
|||||||
Cloud,
|
Cloud,
|
||||||
FileSpreadsheet,
|
FileSpreadsheet,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
Rocket,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
Cpu,
|
Cpu,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { serverApi } from '@/services/api';
|
import { serverApi } from '@/services/api';
|
||||||
@@ -73,44 +76,21 @@ export function ServerManager() {
|
|||||||
const [terminalOpen, setTerminalOpen] = useState(false);
|
const [terminalOpen, setTerminalOpen] = useState(false);
|
||||||
const [terminalServer, setTerminalServer] = useState<{ id: number; name: string; ip: string } | null>(null);
|
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 安装向导状态
|
// Agent 安装向导状态
|
||||||
const [agentWizardOpen, setAgentWizardOpen] = useState(false);
|
const [agentWizardOpen, setAgentWizardOpen] = useState(false);
|
||||||
|
const [wizardServer, setWizardServer] = useState<any>(null);
|
||||||
// Agent 安装状态: { [serverId]: 'installing' | 'success' | 'failed' }
|
// Agent 安装状态: { [serverId]: 'installing' | 'success' | 'failed' }
|
||||||
const [installingIds, setInstallingIds] = useState<Set<number>>(new Set());
|
const [installingIds, setInstallingIds] = useState<Set<number>>(new Set());
|
||||||
const [installResults, setInstallResults] = useState<Map<number, { success: boolean; message: string }>>(new Map());
|
const [installResults, setInstallResults] = useState<Map<number, { success: boolean; message: string }>>(new Map());
|
||||||
|
|
||||||
const openInstallAgent = async (server: any) => {
|
const openInstallAgent = (server: any) => {
|
||||||
const token = useUserStore.getState().token;
|
setWizardServer(server);
|
||||||
if (!token) { toast.error('请先登录'); return; }
|
setAgentWizardOpen(true);
|
||||||
|
|
||||||
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 handleBatchInstallAgents = async () => {
|
const handleBatchInstallAgents = async () => {
|
||||||
@@ -151,11 +131,17 @@ export function ServerManager() {
|
|||||||
setTerminalOpen(true);
|
setTerminalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async (kw?: string, st?: string) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
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 || []);
|
setServers(res?.list || []);
|
||||||
|
setTotal(res?.total || 0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载服务器列表失败:', e);
|
console.error('加载服务器列表失败:', e);
|
||||||
setServers([]);
|
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(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
}, []);
|
}, [page, pageSize]);
|
||||||
|
|
||||||
// WebSocket 实时接收 Agent 指标推送 // Logan.Li
|
// WebSocket 实时接收 Agent 指标推送 // Logan.Li
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
@@ -244,20 +240,12 @@ export function ServerManager() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filteredServers = servers.filter((server) => {
|
// 导出 CSV
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleSelectAll = () => {
|
const toggleSelectAll = () => {
|
||||||
if (selectedServers.length === filteredServers.length) {
|
if (selectedServers.length === servers.length) {
|
||||||
setSelectedServers([]);
|
setSelectedServers([]);
|
||||||
} else {
|
} else {
|
||||||
setSelectedServers(filteredServers.map((s) => s.id));
|
setSelectedServers(servers.map((s) => s.id));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -382,12 +370,12 @@ export function ServerManager() {
|
|||||||
|
|
||||||
// 导出 CSV — 将当前服务器列表导出为 CSV 文件 // Logan.Li
|
// 导出 CSV — 将当前服务器列表导出为 CSV 文件 // Logan.Li
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
if (filteredServers.length === 0) {
|
if (servers.length === 0) {
|
||||||
toast.error('没有可导出的数据');
|
toast.error('没有可导出的数据');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const headers = ['主机名', 'IP地址', '公网IP', '端口', '操作系统', 'CPU(核)', '内存(GB)', '磁盘(GB)', '状态', '标签'];
|
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 memGB = s.memory ? Math.round(s.memory / 1024) : 0;
|
||||||
const tags = getTags(s).join(';');
|
const tags = getTags(s).join(';');
|
||||||
return [s.hostname || '', s.ip || '', s.public_ip || '', s.port || 22, s.os || '',
|
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.download = `servers_${new Date().toISOString().slice(0, 10)}.csv`;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
toast.success(`已导出 ${filteredServers.length} 台服务器`);
|
toast.success(`已导出 ${servers.length} 台服务器`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const colSpanCount = 8;
|
const colSpanCount = 8;
|
||||||
@@ -420,7 +408,7 @@ export function ServerManager() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
|
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')} />
|
<RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />
|
||||||
刷新
|
刷新
|
||||||
@@ -485,11 +473,11 @@ export function ServerManager() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="搜索主机名或IP..."
|
placeholder="搜索主机名或IP..."
|
||||||
value={searchQuery}
|
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)]"
|
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>
|
</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)]">
|
<SelectTrigger className="w-32 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||||
<SelectValue placeholder="状态" />
|
<SelectValue placeholder="状态" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -534,7 +522,7 @@ export function ServerManager() {
|
|||||||
<TableHead className="w-12">
|
<TableHead className="w-12">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={
|
checked={
|
||||||
selectedServers.length === filteredServers.length && filteredServers.length > 0
|
selectedServers.length === servers.length && servers.length > 0
|
||||||
}
|
}
|
||||||
onCheckedChange={toggleSelectAll}
|
onCheckedChange={toggleSelectAll}
|
||||||
className="border-[var(--dg-border)]"
|
className="border-[var(--dg-border)]"
|
||||||
@@ -558,14 +546,14 @@ export function ServerManager() {
|
|||||||
加载中...
|
加载中...
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : filteredServers.length === 0 ? (
|
) : servers.length === 0 ? (
|
||||||
<TableRow className="border-[var(--dg-border)]">
|
<TableRow className="border-[var(--dg-border)]">
|
||||||
<TableCell colSpan={colSpanCount} className="text-center text-[var(--dg-text-muted)] py-12">
|
<TableCell colSpan={colSpanCount} className="text-center text-[var(--dg-text-muted)] py-12">
|
||||||
暂无服务器数据
|
暂无服务器数据
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
filteredServers.map((server) => (
|
servers.map((server) => (
|
||||||
<ServerTableRow
|
<ServerTableRow
|
||||||
key={server.id}
|
key={server.id}
|
||||||
server={server}
|
server={server}
|
||||||
@@ -583,6 +571,48 @@ export function ServerManager() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -616,7 +646,30 @@ export function ServerManager() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Agent 安装向导 */}
|
{/* 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 */}
|
{/* 终端 Dialog */}
|
||||||
<TerminalDialog
|
<TerminalDialog
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { TableCell, TableRow } from '@/components/ui/table';
|
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';
|
import { getStatusBadge, getTags, getUsageColor } from './serverHelpers';
|
||||||
|
|
||||||
interface ServerTableRowProps {
|
interface ServerTableRowProps {
|
||||||
@@ -151,6 +151,29 @@ export function ServerTableRow({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex items-center justify-end gap-1">
|
<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)}>
|
<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" />
|
<Terminal className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -177,24 +200,6 @@ export function ServerTableRow({
|
|||||||
<Terminal className="h-4 w-4 mr-2" />
|
<Terminal className="h-4 w-4 mr-2" />
|
||||||
终端
|
终端
|
||||||
</DropdownMenuItem>
|
</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)]" />
|
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-red-400 focus:bg-[var(--dg-bg-hover)] focus:text-red-400 cursor-pointer"
|
className="text-red-400 focus:bg-[var(--dg-bg-hover)] focus:text-red-400 cursor-pointer"
|
||||||
|
|||||||
Reference in new issue
Block a user