104 lines
2.7 KiB
Go
104 lines
2.7 KiB
Go
// 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
|
|
}
|