2020-04-01 20:32:09 +02:00
|
|
|
package httpmetrics
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/oliveagle/jsonpath"
|
|
|
|
)
|
|
|
|
|
|
|
|
// JSONPathMetricsGetter is a metrics getter which looks up pod metrics by
|
|
|
|
// querying the pods metrics endpoint and lookup the metric value as defined by
|
|
|
|
// the json path query.
|
|
|
|
type JSONPathMetricsGetter struct {
|
|
|
|
jsonPath *jsonpath.Compiled
|
|
|
|
aggregator AggregatorFunc
|
|
|
|
client *http.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewJSONPathMetricsGetter initializes a new JSONPathMetricsGetter.
|
|
|
|
func NewJSONPathMetricsGetter(httpClient *http.Client, aggregatorFunc AggregatorFunc, compiledPath *jsonpath.Compiled) *JSONPathMetricsGetter {
|
|
|
|
return &JSONPathMetricsGetter{client: httpClient, aggregator: aggregatorFunc, jsonPath: compiledPath}
|
|
|
|
}
|
|
|
|
|
2020-05-15 20:48:46 -06:00
|
|
|
var DefaultRequestTimeout = 15 * time.Second
|
|
|
|
var DefaultConnectTimeout = 15 * time.Second
|
|
|
|
|
|
|
|
func CustomMetricsHTTPClient(requestTimeout time.Duration, connectTimeout time.Duration) *http.Client {
|
2020-04-01 20:32:09 +02:00
|
|
|
client := &http.Client{
|
|
|
|
Transport: &http.Transport{
|
|
|
|
DialContext: (&net.Dialer{
|
2020-05-15 20:48:46 -06:00
|
|
|
Timeout: connectTimeout,
|
2020-04-01 20:32:09 +02:00
|
|
|
}).DialContext,
|
|
|
|
MaxIdleConns: 50,
|
|
|
|
IdleConnTimeout: 90 * time.Second,
|
|
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
|
|
},
|
2020-05-15 20:48:46 -06:00
|
|
|
Timeout: requestTimeout,
|
2020-04-01 20:32:09 +02:00
|
|
|
}
|
|
|
|
return client
|
|
|
|
}
|
|
|
|
|
2020-05-15 20:48:46 -06:00
|
|
|
func DefaultMetricsHTTPClient() *http.Client {
|
|
|
|
return CustomMetricsHTTPClient(DefaultRequestTimeout, DefaultConnectTimeout)
|
|
|
|
}
|
|
|
|
|
2020-04-01 20:32:09 +02:00
|
|
|
// GetMetric gets metric from pod by fetching json metrics from the pods metric
|
|
|
|
// endpoint and extracting the desired value using the specified json path
|
|
|
|
// query.
|
|
|
|
func (g *JSONPathMetricsGetter) GetMetric(metricsURL url.URL) (float64, error) {
|
|
|
|
data, err := g.fetchMetrics(metricsURL)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// parse data
|
|
|
|
var jsonData interface{}
|
|
|
|
err = json.Unmarshal(data, &jsonData)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
res, err := g.jsonPath.Lookup(jsonData)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
switch res := res.(type) {
|
|
|
|
case int:
|
|
|
|
return float64(res), nil
|
|
|
|
case float32:
|
|
|
|
return float64(res), nil
|
|
|
|
case float64:
|
|
|
|
return res, nil
|
|
|
|
case []interface{}:
|
|
|
|
if g.aggregator == nil {
|
|
|
|
return 0, fmt.Errorf("no aggregator function has been specified")
|
|
|
|
}
|
|
|
|
s, err := castSlice(res)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
return g.aggregator(s...), nil
|
|
|
|
default:
|
|
|
|
return 0, fmt.Errorf("unsupported type %T", res)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// castSlice takes a slice of interface and returns a slice of float64 if all
|
|
|
|
// values in slice were castable, else returns an error
|
|
|
|
func castSlice(in []interface{}) ([]float64, error) {
|
|
|
|
var out []float64
|
|
|
|
|
|
|
|
for _, v := range in {
|
|
|
|
switch v := v.(type) {
|
|
|
|
case int:
|
|
|
|
out = append(out, float64(v))
|
|
|
|
case float32:
|
|
|
|
out = append(out, float64(v))
|
|
|
|
case float64:
|
|
|
|
out = append(out, v)
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("slice was returned by JSONPath, but value inside is unsupported: %T", v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return out, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (g *JSONPathMetricsGetter) fetchMetrics(metricsURL url.URL) ([]byte, error) {
|
|
|
|
request, err := http.NewRequest(http.MethodGet, metricsURL.String(), nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := g.client.Do(request)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
data, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
return nil, fmt.Errorf("unsuccessful response: %s", resp.Status)
|
|
|
|
}
|
|
|
|
|
|
|
|
return data, nil
|
|
|
|
}
|