package zabbix import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strconv" "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" } }