2020-04-01 20:32:09 +02:00
|
|
|
package httpmetrics
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"time"
|
|
|
|
|
2020-08-13 09:51:05 +02:00
|
|
|
"github.com/spyzhov/ajson"
|
2020-04-01 20:32:09 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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 {
|
2020-08-13 09:51:05 +02:00
|
|
|
jsonPath string
|
2020-04-01 20:32:09 +02:00
|
|
|
aggregator AggregatorFunc
|
|
|
|
client *http.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewJSONPathMetricsGetter initializes a new JSONPathMetricsGetter.
|
2020-08-13 09:51:05 +02:00
|
|
|
func NewJSONPathMetricsGetter(httpClient *http.Client, aggregatorFunc AggregatorFunc, jsonPath string) (*JSONPathMetricsGetter, error) {
|
|
|
|
// check that jsonPath parses
|
|
|
|
_, err := ajson.ParseJSONPath(jsonPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &JSONPathMetricsGetter{client: httpClient, aggregator: aggregatorFunc, jsonPath: jsonPath}, nil
|
2020-04-01 20:32:09 +02:00
|
|
|
}
|
|
|
|
|
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
|
2020-08-13 09:51:05 +02:00
|
|
|
root, err := ajson.Unmarshal(data)
|
2020-04-01 20:32:09 +02:00
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
2020-08-13 09:51:05 +02:00
|
|
|
nodes, err := root.JSONPath(g.jsonPath)
|
2020-04-01 20:32:09 +02:00
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
2021-10-14 09:34:15 +02:00
|
|
|
if len(nodes) == 0 {
|
2020-08-13 09:51:05 +02:00
|
|
|
return 0, fmt.Errorf("unexpected json: expected single numeric or array value")
|
|
|
|
}
|
|
|
|
|
2021-10-14 09:34:15 +02:00
|
|
|
if len(nodes) > 1 {
|
|
|
|
if g.aggregator == nil {
|
|
|
|
return 0, fmt.Errorf("no aggregator function has been specified")
|
|
|
|
}
|
|
|
|
values := make([]float64, 0, len(nodes))
|
|
|
|
for _, node := range nodes {
|
|
|
|
v, err := node.GetNumeric()
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("unexpected json: did not find numeric or array value '%s': %w", nodes, err)
|
|
|
|
}
|
|
|
|
values = append(values, v)
|
|
|
|
}
|
|
|
|
return g.aggregator(values...), nil
|
|
|
|
}
|
|
|
|
|
2020-08-13 09:51:05 +02:00
|
|
|
node := nodes[0]
|
|
|
|
if node.IsArray() {
|
2020-04-01 20:32:09 +02:00
|
|
|
if g.aggregator == nil {
|
|
|
|
return 0, fmt.Errorf("no aggregator function has been specified")
|
|
|
|
}
|
2020-08-13 09:51:05 +02:00
|
|
|
values := make([]float64, 0, len(nodes))
|
|
|
|
items, _ := node.GetArray()
|
|
|
|
for _, item := range items {
|
|
|
|
value, err := item.GetNumeric()
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("did not find numeric type: %w", err)
|
|
|
|
}
|
|
|
|
values = append(values, value)
|
2020-04-01 20:32:09 +02:00
|
|
|
}
|
2020-08-13 09:51:05 +02:00
|
|
|
return g.aggregator(values...), nil
|
|
|
|
} else if node.IsNumeric() {
|
|
|
|
res, _ := node.GetNumeric()
|
|
|
|
return res, nil
|
2020-04-01 20:32:09 +02:00
|
|
|
}
|
|
|
|
|
2020-08-13 09:51:05 +02:00
|
|
|
value, err := node.Value()
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("failed to check value of jsonPath result: %w", err)
|
2020-04-01 20:32:09 +02:00
|
|
|
}
|
2020-08-13 09:51:05 +02:00
|
|
|
return 0, fmt.Errorf("unsupported type %T", value)
|
2020-04-01 20:32:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|