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
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user