mirror of
https://github.com/kubernetes-sigs/prometheus-adapter.git
synced 2026-04-05 17:27:51 +00:00
Initial Functionality
The initial functionality works. There's still a number of TODOs to clean up, and some edge cases to work around, and some errors that could be handled better.
This commit is contained in:
commit
5bff503339
13 changed files with 2364 additions and 0 deletions
212
pkg/client/api.go
Normal file
212
pkg/client/api.go
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
// 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"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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)
|
||||
u.RawQuery = query.Encode()
|
||||
req, err := http.NewRequest(verb, u.String(), nil)
|
||||
if err != nil {
|
||||
// TODO: fix this to return Error?
|
||||
return APIResponse{}, fmt.Errorf("error constructing HTTP request to Prometheus: %v", err)
|
||||
}
|
||||
req.WithContext(ctx)
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
defer func() {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return APIResponse{}, err
|
||||
}
|
||||
|
||||
if glog.V(6) {
|
||||
glog.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 glog.V(8) {
|
||||
data, err := ioutil.ReadAll(body)
|
||||
if err != nil {
|
||||
return APIResponse{}, fmt.Errorf("unable to log response body: %v", err)
|
||||
}
|
||||
glog.Infof("Response Body: %s", string(data))
|
||||
body = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
var res APIResponse
|
||||
if err = json.NewDecoder(body).Decode(&res); err != nil {
|
||||
// TODO: return what the body actually was?
|
||||
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) GenericAPIClient {
|
||||
return &httpAPIClient{
|
||||
client: client,
|
||||
baseURL: baseURL,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// NewClientForAPI creates a Client for the given generic Prometheus API client.
|
||||
func NewClientForAPI(client GenericAPIClient) Client {
|
||||
return &queryClient{
|
||||
api: client,
|
||||
}
|
||||
}
|
||||
|
||||
// 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) Client {
|
||||
genericClient := NewGenericAPIClient(client, baseURL)
|
||||
return NewClientForAPI(genericClient)
|
||||
}
|
||||
|
||||
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, "GET", 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())
|
||||
}
|
||||
// TODO: get timeout from context...
|
||||
|
||||
res, err := h.api.Do(ctx, "GET", 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())
|
||||
}
|
||||
// TODO: get timeout from context...
|
||||
|
||||
res, err := h.api.Do(ctx, "GET", queryRangeURL, vals)
|
||||
if err != nil {
|
||||
return QueryResult{}, err
|
||||
}
|
||||
|
||||
var queryRes QueryResult
|
||||
err = json.Unmarshal(res.Data, &queryRes)
|
||||
return queryRes, err
|
||||
}
|
||||
68
pkg/client/helpers.go
Normal file
68
pkg/client/helpers.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes 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 client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LabelNeq produces a not-equal label selector expression.
|
||||
// Label is passed verbatim, and value is double-quote escaped
|
||||
// using Go's escaping is used on value (as per the PromQL rules).
|
||||
func LabelNeq(label string, value string) string {
|
||||
return fmt.Sprintf("%s!=%q", label, value)
|
||||
}
|
||||
|
||||
// LabelEq produces a equal label selector expression.
|
||||
// Label is passed verbatim, and value is double-quote escaped
|
||||
// using Go's escaping is used on value (as per the PromQL rules).
|
||||
func LabelEq(label string, value string) string {
|
||||
return fmt.Sprintf("%s=%q", label, value)
|
||||
}
|
||||
|
||||
// LabelMatches produces a regexp-matching label selector expression.
|
||||
// It has similar constraints to LabelNeq.
|
||||
func LabelMatches(label string, expr string) string {
|
||||
return fmt.Sprintf("%s=~%q", label, expr)
|
||||
}
|
||||
|
||||
// LabelNotMatches produces a inverse regexp-matching label selector expression (the opposite of LabelMatches).
|
||||
func LabelNotMatches(label string, expr string) string {
|
||||
return fmt.Sprintf("%s!~%q", label, expr)
|
||||
}
|
||||
|
||||
// NameMatches produces a label selector expression that checks that the series name matches the given expression.
|
||||
// It's a convinience wrapper around LabelMatches.
|
||||
func NameMatches(expr string) string {
|
||||
return LabelMatches("__name__", expr)
|
||||
}
|
||||
|
||||
// NameNotMatches produces a label selector expression that checks that the series name doesn't matches the given expression.
|
||||
// It's a convinience wrapper around LabelNotMatches.
|
||||
func NameNotMatches(expr string) string {
|
||||
return LabelNotMatches("__name__", expr)
|
||||
}
|
||||
|
||||
// MatchSeries takes a series name, and optionally some label expressions, and returns a series selector.
|
||||
// TODO: validate series name and expressions?
|
||||
func MatchSeries(name string, labelExpressions ...string) Selector {
|
||||
if len(labelExpressions) == 0 {
|
||||
return Selector(name)
|
||||
}
|
||||
|
||||
return Selector(fmt.Sprintf("%s{%s}", name, strings.Join(labelExpressions, ",")))
|
||||
}
|
||||
121
pkg/client/interfaces.go
Normal file
121
pkg/client/interfaces.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes 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 client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
// NB: the official prometheus API client at https://github.com/prometheus/client_golang
|
||||
// is rather lackluster -- as of the time of writing of this file, it lacked support
|
||||
// for querying the series metadata, which we need for the adapter. Instead, we use
|
||||
// this client.
|
||||
|
||||
// Selector represents a series selector
|
||||
type Selector string
|
||||
|
||||
// Range represents a sliced time range with increments.
|
||||
type Range struct {
|
||||
// Start and End are the boundaries of the time range.
|
||||
Start, End model.Time
|
||||
// Step is the maximum time between two slices within the boundaries.
|
||||
Step time.Duration
|
||||
}
|
||||
|
||||
// TODO: support timeout in the client?
|
||||
type Client interface {
|
||||
// Series lists the time series matching the given series selectors
|
||||
Series(ctx context.Context, interval model.Interval, selectors ...Selector) ([]Series, error)
|
||||
// Query runs a non-range query at the given time.
|
||||
Query(ctx context.Context, t model.Time, query Selector) (QueryResult, error)
|
||||
// QueryRange runs a range query at the given time.
|
||||
QueryRange(ctx context.Context, r Range, query Selector) (QueryResult, error)
|
||||
}
|
||||
|
||||
// QueryResult is the result of a query.
|
||||
// Type will always be set, as well as one of the other fields, matching the type.
|
||||
type QueryResult struct {
|
||||
Type model.ValueType
|
||||
|
||||
Vector *model.Vector
|
||||
Scalar *model.Scalar
|
||||
Matrix *model.Matrix
|
||||
}
|
||||
|
||||
func (qr *QueryResult) UnmarshalJSON(b []byte) error {
|
||||
v := struct {
|
||||
Type model.ValueType `json:"resultType"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
}{}
|
||||
|
||||
err := json.Unmarshal(b, &v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
qr.Type = v.Type
|
||||
|
||||
switch v.Type {
|
||||
case model.ValScalar:
|
||||
var sv model.Scalar
|
||||
err = json.Unmarshal(v.Result, &sv)
|
||||
qr.Scalar = &sv
|
||||
|
||||
case model.ValVector:
|
||||
var vv model.Vector
|
||||
err = json.Unmarshal(v.Result, &vv)
|
||||
qr.Vector = &vv
|
||||
|
||||
case model.ValMatrix:
|
||||
var mv model.Matrix
|
||||
err = json.Unmarshal(v.Result, &mv)
|
||||
qr.Matrix = &mv
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("unexpected value type %q", v.Type)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Series represents a description of a series: a name and a set of labels.
|
||||
// Series is roughly equivalent to model.Metrics, but has easy access to name
|
||||
// and the set of non-name labels.
|
||||
type Series struct {
|
||||
Name string
|
||||
Labels model.LabelSet
|
||||
}
|
||||
|
||||
func (s *Series) UnmarshalJSON(data []byte) error {
|
||||
var rawMetric model.Metric
|
||||
err := json.Unmarshal(data, &rawMetric)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if name, ok := rawMetric[model.MetricNameLabel]; ok {
|
||||
s.Name = string(name)
|
||||
delete(rawMetric, model.MetricNameLabel)
|
||||
}
|
||||
|
||||
s.Labels = model.LabelSet(rawMetric)
|
||||
|
||||
return nil
|
||||
}
|
||||
62
pkg/client/types.go
Normal file
62
pkg/client/types.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrorType is the type of the API error.
|
||||
type ErrorType string
|
||||
|
||||
const (
|
||||
ErrBadData ErrorType = "bad_data"
|
||||
ErrTimeout = "timeout"
|
||||
ErrCanceled = "canceled"
|
||||
ErrExec = "execution"
|
||||
ErrBadResponse = "bad_response"
|
||||
)
|
||||
|
||||
// Error is an error returned by the API.
|
||||
type Error struct {
|
||||
Type ErrorType
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Type, e.Msg)
|
||||
}
|
||||
|
||||
// ResponseStatus is the type of response from the API: succeeded or error.
|
||||
type ResponseStatus string
|
||||
const (
|
||||
ResponseSucceeded ResponseStatus = "succeeded"
|
||||
ResponseError = "error"
|
||||
)
|
||||
|
||||
// APIResponse represents the raw response returned by the API.
|
||||
type APIResponse struct {
|
||||
// Status indicates whether this request was successful or whether it errored out.
|
||||
Status ResponseStatus `json:"status"`
|
||||
// Data contains the raw data response for this request.
|
||||
Data json.RawMessage `json:"data"`
|
||||
|
||||
// ErrorType is the type of error, if this is an error response.
|
||||
ErrorType ErrorType `json:"errorType"`
|
||||
// Error is the error message, if this is an error response.
|
||||
Error string `json:"error"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue