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:
Solly Ross 2017-05-09 21:34:24 -04:00
commit 5bff503339
13 changed files with 2364 additions and 0 deletions

212
pkg/client/api.go Normal file
View 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
View 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
View 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
View 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"`
}