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