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 +}