package client
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
log "github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/prometheus/model/rulefmt"
weaveworksClient "github.com/weaveworks/common/http/client"
"github.com/weaveworks/common/instrument"
"github.com/weaveworks/common/user"
)
const (
rulerAPIPath = "/prometheus/config/v1/rules"
legacyAPIPath = "/api/v1/rules"
)
var (
ErrNoConfig = errors.New("No config exists for this user")
ErrResourceNotFound = errors.New("requested resource not found")
)
type Config struct {
ID string
Address string
UseLegacyRoutes bool
HTTPClientConfig config.HTTPClientConfig
}
type Interface interface {
CreateRuleGroup(ctx context.Context, namespace string, rg rulefmt.RuleGroup) error
DeleteRuleGroup(ctx context.Context, namespace, groupName string) error
ListRules(ctx context.Context, namespace string) (map[string][]rulefmt.RuleGroup, error)
}
type MimirClient struct {
id string
endpoint *url.URL
client weaveworksClient.Requester
apiPath string
logger log.Logger
}
func New(logger log.Logger, cfg Config, timingHistogram *prometheus.HistogramVec) (*MimirClient, error) {
endpoint, err := url.Parse(cfg.Address)
if err != nil {
return nil, err
}
client, err := config.NewClientFromConfig(cfg.HTTPClientConfig, "GrafanaAgent", config.WithHTTP2Disabled())
if err != nil {
return nil, err
}
path := rulerAPIPath
if cfg.UseLegacyRoutes {
path = legacyAPIPath
}
collector := instrument.NewHistogramCollector(timingHistogram)
timedClient := weaveworksClient.NewTimedClient(client, collector)
return &MimirClient{
id: cfg.ID,
endpoint: endpoint,
client: timedClient,
apiPath: path,
logger: logger,
}, nil
}
func (r *MimirClient) doRequest(operation, path, method string, payload []byte) (*http.Response, error) {
req, err := buildRequest(operation, path, method, *r.endpoint, payload)
if err != nil {
return nil, err
}
if r.id != "" {
req.Header.Add(user.OrgIDHeaderName, r.id)
}
resp, err := r.client.Do(req)
if err != nil {
return nil, err
}
if err := checkResponse(resp); err != nil {
_ = resp.Body.Close()
return nil, fmt.Errorf("error %s %s: %w", method, path, err)
}
return resp, nil
}
func checkResponse(r *http.Response) error {
if 200 <= r.StatusCode && r.StatusCode <= 299 {
return nil
}
var msg, errMsg string
scanner := bufio.NewScanner(io.LimitReader(r.Body, 512))
if scanner.Scan() {
msg = scanner.Text()
}
if msg == "" {
errMsg = fmt.Sprintf("server returned HTTP status %s", r.Status)
} else {
errMsg = fmt.Sprintf("server returned HTTP status %s: %s", r.Status, msg)
}
if r.StatusCode == http.StatusNotFound {
return ErrResourceNotFound
}
return errors.New(errMsg)
}
func joinPath(baseURLPath, targetPath string) string {
return strings.TrimSuffix(baseURLPath, "/") + targetPath
}
func buildRequest(op, p, m string, endpoint url.URL, payload []byte) (*http.Request, error) {
pURL, err := url.Parse(p)
if err != nil {
return nil, err
}
if pURL.RawPath != "" || endpoint.RawPath != "" {
endpoint.RawPath = joinPath(endpoint.EscapedPath(), pURL.EscapedPath())
}
endpoint.Path = joinPath(endpoint.Path, pURL.Path)
r, err := http.NewRequest(m, endpoint.String(), bytes.NewBuffer(payload))
if err != nil {
return nil, err
}
r = r.WithContext(context.WithValue(r.Context(), weaveworksClient.OperationNameContextKey, op))
return r, nil
}