From a6ec720df3658a570b721a019f3eb7ef4dfa0a65 Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Mon, 20 Apr 2026 18:10:36 +0300 Subject: [PATCH] feat: Add Prometheus service client with instant query execution, alert retrieval, and health checking - Add PrometheusService struct with baseURL/http.Client (10s timeout) and PROMETHEUS_URL env config (default: http://prometheus:9090) - Add Available method with /-/ready endpoint health check - Add Query method with instant PromQL execution via /api/v1/query, URL escaping, error handling - Add promResponse/promVectorData/PromVectorSample structs for query result parsing with metric/value fields --- .../internal/monitoring/prometheus.go | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 apps/api-server/internal/monitoring/prometheus.go diff --git a/apps/api-server/internal/monitoring/prometheus.go b/apps/api-server/internal/monitoring/prometheus.go new file mode 100644 index 0000000..8246e31 --- /dev/null +++ b/apps/api-server/internal/monitoring/prometheus.go @@ -0,0 +1,133 @@ +package monitoring + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +// PrometheusService queries a Prometheus HTTP API for real system metrics +// and alerts. Configure via PROMETHEUS_URL (e.g. http://prometheus:9090). +type PrometheusService struct { + baseURL string + http *http.Client +} + +func NewPrometheusService() *PrometheusService { + base := os.Getenv("PROMETHEUS_URL") + if base == "" { + base = "http://prometheus:9090" + } + return &PrometheusService{ + baseURL: strings.TrimRight(base, "/"), + http: &http.Client{Timeout: 10 * time.Second}, + } +} + +// Available reports whether the configured Prometheus instance is reachable. +func (p *PrometheusService) Available(ctx context.Context) bool { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.baseURL+"/-/ready", nil) + if err != nil { + return false + } + resp, err := p.http.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + return resp.StatusCode >= 200 && resp.StatusCode < 300 +} + +type promResponse struct { + Status string `json:"status"` + Data json.RawMessage `json:"data"` + Error string `json:"error,omitempty"` +} + +type PromVectorSample struct { + Metric map[string]string `json:"metric"` + Value [2]any `json:"value"` +} + +type promVectorData struct { + ResultType string `json:"resultType"` + Result []PromVectorSample `json:"result"` +} + +// Query runs an instant PromQL query. +func (p *PrometheusService) Query(ctx context.Context, promQL string) ([]PromVectorSample, error) { + u := fmt.Sprintf("%s/api/v1/query?query=%s", p.baseURL, url.QueryEscape(promQL)) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) + if err != nil { + return nil, err + } + resp, err := p.http.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("prometheus query failed: %d %s", resp.StatusCode, string(b)) + } + var out promResponse + if err := json.Unmarshal(b, &out); err != nil { + return nil, err + } + if out.Status != "success" { + return nil, fmt.Errorf("prometheus query error: %s", out.Error) + } + var data promVectorData + if err := json.Unmarshal(out.Data, &data); err != nil { + return nil, err + } + return data.Result, nil +} + +// Alert represents an active Prometheus alert. +type PromAlert struct { + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` + State string `json:"state"` + ActiveAt time.Time `json:"activeAt"` + Value string `json:"value"` +} + +type promAlertsData struct { + Alerts []PromAlert `json:"alerts"` +} + +// Alerts returns currently firing/pending alerts. +func (p *PrometheusService) Alerts(ctx context.Context) ([]PromAlert, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.baseURL+"/api/v1/alerts", nil) + if err != nil { + return nil, err + } + resp, err := p.http.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("prometheus alerts failed: %d %s", resp.StatusCode, string(b)) + } + var out promResponse + if err := json.Unmarshal(b, &out); err != nil { + return nil, err + } + if out.Status != "success" { + return nil, fmt.Errorf("prometheus alerts error: %s", out.Error) + } + var data promAlertsData + if err := json.Unmarshal(out.Data, &data); err != nil { + return nil, err + } + return data.Alerts, nil +}