prometheus-adapter/pkg/client/api.go
2022-11-28 23:17:16 +01:00

242 lines
6.6 KiB
Go

// Copyright 2017 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package prometheus provides bindings to the Prometheus HTTP API:
// http://prometheus.io/docs/querying/api/
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"time"
"github.com/prometheus/common/model"
"k8s.io/klog/v2"
)
// APIClient is a raw client to the Prometheus Query API.
// It knows how to appropriately deal with generic Prometheus API
// responses, but does not know the specifics of different endpoints.
// You can use this to call query endpoints not represented in Client.
type GenericAPIClient interface {
// Do makes a request to the Prometheus HTTP API against a particular endpoint. Query
// parameters should be in `query`, not `endpoint`. An error will be returned on HTTP
// status errors or errors making or unmarshalling the request, as well as when the
// response has a Status of ResponseError.
Do(ctx context.Context, verb, endpoint string, query url.Values) (APIResponse, error)
}
// httpAPIClient is a GenericAPIClient implemented in terms of an underlying http.Client.
type httpAPIClient struct {
client *http.Client
baseURL *url.URL
headers http.Header
}
func (c *httpAPIClient) Do(ctx context.Context, verb, endpoint string, query url.Values) (APIResponse, error) {
u := *c.baseURL
u.Path = path.Join(c.baseURL.Path, endpoint)
var reqBody io.Reader
if verb == http.MethodGet {
u.RawQuery = query.Encode()
} else if verb == http.MethodPost {
reqBody = strings.NewReader(query.Encode())
}
req, err := http.NewRequestWithContext(ctx, verb, u.String(), reqBody)
if err != nil {
return APIResponse{}, fmt.Errorf("error constructing HTTP request to Prometheus: %v", err)
}
for key, values := range c.headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
if verb == http.MethodPost {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
resp, err := c.client.Do(req)
defer func() {
if resp != nil {
resp.Body.Close()
}
}()
if err != nil {
return APIResponse{}, err
}
if klog.V(6).Enabled() {
klog.Infof("%s %s %s", verb, u.String(), resp.Status)
}
code := resp.StatusCode
// codes that aren't 2xx, 400, 422, or 503 won't return JSON objects
if code/100 != 2 && code != 400 && code != 422 && code != 503 {
return APIResponse{}, &Error{
Type: ErrBadResponse,
Msg: fmt.Sprintf("unknown response code %d", code),
}
}
var body io.Reader = resp.Body
if klog.V(8).Enabled() {
data, err := io.ReadAll(body)
if err != nil {
return APIResponse{}, fmt.Errorf("unable to log response body: %v", err)
}
klog.Infof("Response Body: %s", string(data))
body = bytes.NewReader(data)
}
var res APIResponse
if err = json.NewDecoder(body).Decode(&res); err != nil {
return APIResponse{}, &Error{
Type: ErrBadResponse,
Msg: err.Error(),
}
}
if res.Status == ResponseError {
return res, &Error{
Type: res.ErrorType,
Msg: res.Error,
}
}
return res, nil
}
// NewGenericAPIClient builds a new generic Prometheus API client for the given base URL and HTTP Client.
func NewGenericAPIClient(client *http.Client, baseURL *url.URL, headers http.Header) GenericAPIClient {
return &httpAPIClient{
client: client,
baseURL: baseURL,
headers: headers,
}
}
const (
queryURL = "/api/v1/query"
queryRangeURL = "/api/v1/query_range"
seriesURL = "/api/v1/series"
)
// queryClient is a Client that connects to the Prometheus HTTP API.
type queryClient struct {
api GenericAPIClient
verb string
}
// NewClientForAPI creates a Client for the given generic Prometheus API client.
func NewClientForAPI(client GenericAPIClient, verb string) Client {
return &queryClient{
api: client,
verb: verb,
}
}
// NewClient creates a Client for the given HTTP client and base URL (the location of the Prometheus server).
func NewClient(client *http.Client, baseURL *url.URL, headers http.Header, verb string) Client {
genericClient := NewGenericAPIClient(client, baseURL, headers)
return NewClientForAPI(genericClient, verb)
}
func (h *queryClient) Series(ctx context.Context, interval model.Interval, selectors ...Selector) ([]Series, error) {
vals := url.Values{}
if interval.Start != 0 {
vals.Set("start", interval.Start.String())
}
if interval.End != 0 {
vals.Set("end", interval.End.String())
}
for _, selector := range selectors {
vals.Add("match[]", string(selector))
}
res, err := h.api.Do(ctx, h.verb, seriesURL, vals)
if err != nil {
return nil, err
}
var seriesRes []Series
err = json.Unmarshal(res.Data, &seriesRes)
return seriesRes, err
}
func (h *queryClient) Query(ctx context.Context, t model.Time, query Selector) (QueryResult, error) {
vals := url.Values{}
vals.Set("query", string(query))
if t != 0 {
vals.Set("time", t.String())
}
if timeout, hasTimeout := timeoutFromContext(ctx); hasTimeout {
vals.Set("timeout", model.Duration(timeout).String())
}
res, err := h.api.Do(ctx, h.verb, queryURL, vals)
if err != nil {
return QueryResult{}, err
}
var queryRes QueryResult
err = json.Unmarshal(res.Data, &queryRes)
return queryRes, err
}
func (h *queryClient) QueryRange(ctx context.Context, r Range, query Selector) (QueryResult, error) {
vals := url.Values{}
vals.Set("query", string(query))
if r.Start != 0 {
vals.Set("start", r.Start.String())
}
if r.End != 0 {
vals.Set("end", r.End.String())
}
if r.Step != 0 {
vals.Set("step", model.Duration(r.Step).String())
}
if timeout, hasTimeout := timeoutFromContext(ctx); hasTimeout {
vals.Set("timeout", model.Duration(timeout).String())
}
res, err := h.api.Do(ctx, h.verb, queryRangeURL, vals)
if err != nil {
return QueryResult{}, err
}
var queryRes QueryResult
err = json.Unmarshal(res.Data, &queryRes)
return queryRes, err
}
// timeoutFromContext checks the context for a deadline and calculates a "timeout" duration from it,
// when present
func timeoutFromContext(ctx context.Context) (time.Duration, bool) {
if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
return time.Since(deadline), true
}
return time.Duration(0), false
}