// Package prometheus provides optional Prometheus range queries for rollout impact. package prometheus import ( "context" "encoding/json" "fmt" "net/http" "net/url" "os" "strconv" "strings" "time" ) type Point struct { Timestamp float64 `json:"timestamp"` Value string `json:"value"` } type Client struct { baseURL string queryTemplate string httpClient *http.Client } func NewFromEnv() *Client { baseURL := strings.TrimRight(os.Getenv("OTEL_PROMETHEUS_URL"), "/") if baseURL == "" { return nil } query := os.Getenv("OTEL_PROMETHEUS_ERROR_QUERY") if query == "" { query = `sum(rate(http_server_requests_total{service_name=%q,http_response_status_code=~"5.."}[5m]))` } return &Client{baseURL: baseURL, queryTemplate: query, httpClient: &http.Client{Timeout: 10 * time.Second}} } func (c *Client) Configured() bool { return c != nil } func (c *Client) ErrorRate(ctx context.Context, service, release string, start, end time.Time) ([]Point, error) { if c == nil { return nil, fmt.Errorf("prometheus is not configured") } query := renderQuery(c.queryTemplate, service, release) params := url.Values{ "query": {query}, "start": {strconv.FormatInt(start.Unix(), 10)}, "end": {strconv.FormatInt(end.Unix(), 10)}, "step": {"300"}, } req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/api/v1/query_range?"+params.Encode(), nil) if err != nil { return nil, err } response, err := c.httpClient.Do(req) if err != nil { return nil, err } defer response.Body.Close() if response.StatusCode/100 != 2 { return nil, fmt.Errorf("prometheus returned HTTP %s", response.Status) } var payload struct { Status string `json:"status"` Error string `json:"error"` Data struct { Result []struct { Values [][]json.RawMessage `json:"values"` } `json:"result"` } `json:"data"` } if err := json.NewDecoder(response.Body).Decode(&payload); err != nil { return nil, err } if payload.Status != "success" { return nil, fmt.Errorf("prometheus query failed: %s", payload.Error) } points := make([]Point, 0) for _, series := range payload.Data.Result { for _, value := range series.Values { if len(value) != 2 { continue } var timestamp float64 var sample string if json.Unmarshal(value[0], ×tamp) == nil && json.Unmarshal(value[1], &sample) == nil { points = append(points, Point{Timestamp: timestamp, Value: sample}) } } } return points, nil } func renderQuery(template, service, release string) string { values := []string{strconv.Quote(service), strconv.Quote(release)} for _, value := range values { if strings.Contains(template, "%q") { template = strings.Replace(template, "%q", value, 1) } } return template }