Files
go-otel/internal/prometheus/client.go
mats 21f04d696b
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 58s
more otel
2026-09-18 20:51:37 +00:00

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], &timestamp) == 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
}