package monitoring import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) // PrometheusClient is an HTTP client for the Prometheus API. type PrometheusClient struct { baseURL string httpClient *http.Client } // NewPrometheusClient creates a new Prometheus API client. func NewPrometheusClient(baseURL string) *PrometheusClient { return &PrometheusClient{ baseURL: strings.TrimRight(baseURL, "/"), httpClient: &http.Client{ Timeout: 30 * time.Second, }, } } // Query executes an instant PromQL query. If ts is zero, the current time is used. func (c *PrometheusClient) Query(ctx context.Context, promql string, ts time.Time) (*PromQueryData, error) { params := url.Values{} params.Set("query", promql) if !ts.IsZero() { params.Set("time", fmt.Sprintf("%d", ts.Unix())) } body, err := c.get(ctx, "/api/v1/query", params) if err != nil { return nil, fmt.Errorf("query failed: %w", err) } var data PromQueryData if err := json.Unmarshal(body, &data); err != nil { return nil, fmt.Errorf("failed to parse query data: %w", err) } return &data, nil } // LabelValues returns all values for a given label name. func (c *PrometheusClient) LabelValues(ctx context.Context, label string) ([]string, error) { path := fmt.Sprintf("/api/v1/label/%s/values", url.PathEscape(label)) body, err := c.get(ctx, path, nil) if err != nil { return nil, fmt.Errorf("label values failed: %w", err) } var values []string if err := json.Unmarshal(body, &values); err != nil { return nil, fmt.Errorf("failed to parse label values: %w", err) } return values, nil } // Metadata returns metadata for metrics. If metric is empty, returns metadata for all metrics. func (c *PrometheusClient) Metadata(ctx context.Context, metric string) (map[string][]PromMetadata, error) { params := url.Values{} if metric != "" { params.Set("metric", metric) } body, err := c.get(ctx, "/api/v1/metadata", params) if err != nil { return nil, fmt.Errorf("metadata failed: %w", err) } var metadata map[string][]PromMetadata if err := json.Unmarshal(body, &metadata); err != nil { return nil, fmt.Errorf("failed to parse metadata: %w", err) } return metadata, nil } // Targets returns the current scrape targets. func (c *PrometheusClient) Targets(ctx context.Context) (*PromTargetsData, error) { body, err := c.get(ctx, "/api/v1/targets", nil) if err != nil { return nil, fmt.Errorf("targets failed: %w", err) } var data PromTargetsData if err := json.Unmarshal(body, &data); err != nil { return nil, fmt.Errorf("failed to parse targets data: %w", err) } return &data, nil } // get performs a GET request and returns the "data" field from the Prometheus response envelope. func (c *PrometheusClient) get(ctx context.Context, path string, params url.Values) (json.RawMessage, error) { u := c.baseURL + path if len(params) > 0 { u += "?" + params.Encode() } req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() //nolint:errcheck // cleanup on exit body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) } var promResp PromResponse if err := json.Unmarshal(body, &promResp); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } if promResp.Status != "success" { return nil, fmt.Errorf("prometheus error (%s): %s", promResp.ErrorType, promResp.Error) } return promResp.Data, nil }