mirror of
https://github.com/zalando-incubator/kube-metrics-adapter.git
synced 2025-05-15 09:46:37 +00:00
Merge pull request #783 from Adirael/feature/support-json-eval
Add support for ajson scripting engine
This commit is contained in:
@ -116,6 +116,7 @@ metadata:
|
|||||||
annotations:
|
annotations:
|
||||||
# metric-config.<metricType>.<metricName>.<collectorType>/<configKey>
|
# metric-config.<metricType>.<metricName>.<collectorType>/<configKey>
|
||||||
metric-config.pods.requests-per-second.json-path/json-key: "$.http_server.rps"
|
metric-config.pods.requests-per-second.json-path/json-key: "$.http_server.rps"
|
||||||
|
metric-config.pods.requests-per-second.json-path/json-eval: "ceil($['active processes'] / $['total processes'] * 100)" # cannot use both json-eval and json-key
|
||||||
metric-config.pods.requests-per-second.json-path/path: /metrics
|
metric-config.pods.requests-per-second.json-path/path: /metrics
|
||||||
metric-config.pods.requests-per-second.json-path/port: "9090"
|
metric-config.pods.requests-per-second.json-path/port: "9090"
|
||||||
metric-config.pods.requests-per-second.json-path/scheme: "https"
|
metric-config.pods.requests-per-second.json-path/scheme: "https"
|
||||||
@ -158,6 +159,10 @@ The json-path query support depends on the
|
|||||||
See the README for possible queries. It's expected that the metric you query
|
See the README for possible queries. It's expected that the metric you query
|
||||||
returns something that can be turned into a `float64`.
|
returns something that can be turned into a `float64`.
|
||||||
|
|
||||||
|
The `json-eval` configuration option allows for more complex calculations to be
|
||||||
|
performed on the extracted metric. The `json-eval` expression is evaluated using
|
||||||
|
[ajson's script engine](https://github.com/spyzhov/ajson?tab=readme-ov-file#script-engine).
|
||||||
|
|
||||||
The other configuration options `path`, `port` and `scheme` specify where the metrics
|
The other configuration options `path`, `port` and `scheme` specify where the metrics
|
||||||
endpoint is exposed on the pod. The `path` and `port` options do not have default values
|
endpoint is exposed on the pod. The `path` and `port` options do not have default values
|
||||||
so they must be defined. The `scheme` is optional and defaults to `http`.
|
so they must be defined. The `scheme` is optional and defaults to `http`.
|
||||||
@ -825,6 +830,7 @@ metadata:
|
|||||||
annotations:
|
annotations:
|
||||||
# metric-config.<metricType>.<metricName>.<collectorType>/<configKey>
|
# metric-config.<metricType>.<metricName>.<collectorType>/<configKey>
|
||||||
metric-config.external.unique-metric-name.json-path/json-key: "$.some-metric.value"
|
metric-config.external.unique-metric-name.json-path/json-key: "$.some-metric.value"
|
||||||
|
metric-config.external.unique-metric-name.json-path/json-eval: ceil($['active processes'] / $['total processes'] * 100) # cannot use both json-eval and json-key
|
||||||
metric-config.external.unique-metric-name.json-path/endpoint: "http://metric-source.app-namespace:8080/metrics"
|
metric-config.external.unique-metric-name.json-path/endpoint: "http://metric-source.app-namespace:8080/metrics"
|
||||||
metric-config.external.unique-metric-name.json-path/aggregator: "max"
|
metric-config.external.unique-metric-name.json-path/aggregator: "max"
|
||||||
metric-config.external.unique-metric-name.json-path/interval: "60s" # optional
|
metric-config.external.unique-metric-name.json-path/interval: "60s" # optional
|
||||||
@ -852,6 +858,8 @@ The HTTP collector similar to the Pod Metrics collector. The following
|
|||||||
configuration values are supported:
|
configuration values are supported:
|
||||||
|
|
||||||
- `json-key` to specify the JSON path of the metric to be queried
|
- `json-key` to specify the JSON path of the metric to be queried
|
||||||
|
- `json-eval` to specify an evaluate string to [evaluate on the script engine](https://github.com/spyzhov/ajson?tab=readme-ov-file#script-engine),
|
||||||
|
cannot be used in conjunction with `json-key`
|
||||||
- `endpoint` the fully formed path to query for the metric. In the above example a Kubernetes _Service_
|
- `endpoint` the fully formed path to query for the metric. In the above example a Kubernetes _Service_
|
||||||
in the namespace `app-namespace` is called.
|
in the namespace `app-namespace` is called.
|
||||||
- `aggregator` is only required if the metric is an array of values and specifies how the values
|
- `aggregator` is only required if the metric is an array of values and specifies how the values
|
||||||
|
@ -19,6 +19,7 @@ const (
|
|||||||
HTTPMetricNameLegacy = "http"
|
HTTPMetricNameLegacy = "http"
|
||||||
HTTPEndpointAnnotationKey = "endpoint"
|
HTTPEndpointAnnotationKey = "endpoint"
|
||||||
HTTPJsonPathAnnotationKey = "json-key"
|
HTTPJsonPathAnnotationKey = "json-key"
|
||||||
|
HTTPJsonEvalAnnotationKey = "json-eval"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HTTPCollectorPlugin struct{}
|
type HTTPCollectorPlugin struct{}
|
||||||
@ -31,14 +32,27 @@ func (p *HTTPCollectorPlugin) NewCollector(_ context.Context, hpa *autoscalingv2
|
|||||||
collector := &HTTPCollector{
|
collector := &HTTPCollector{
|
||||||
namespace: hpa.Namespace,
|
namespace: hpa.Namespace,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
value string
|
value string
|
||||||
ok bool
|
ok bool
|
||||||
|
|
||||||
|
jsonPath string
|
||||||
|
jsonEval string
|
||||||
)
|
)
|
||||||
if value, ok = config.Config[HTTPJsonPathAnnotationKey]; !ok {
|
|
||||||
return nil, fmt.Errorf("config value %s not found", HTTPJsonPathAnnotationKey)
|
if value, ok = config.Config[HTTPJsonPathAnnotationKey]; ok {
|
||||||
|
jsonPath = value
|
||||||
|
}
|
||||||
|
if value, ok = config.Config[HTTPJsonEvalAnnotationKey]; ok {
|
||||||
|
jsonEval = value
|
||||||
|
}
|
||||||
|
if jsonPath == "" && jsonEval == "" {
|
||||||
|
return nil, fmt.Errorf("config value %s or %s not found", HTTPJsonPathAnnotationKey, HTTPJsonEvalAnnotationKey)
|
||||||
|
}
|
||||||
|
if jsonPath != "" && jsonEval != "" {
|
||||||
|
return nil, fmt.Errorf("config value %s and %s cannot be used together", HTTPJsonPathAnnotationKey, HTTPJsonEvalAnnotationKey)
|
||||||
}
|
}
|
||||||
jsonPath := value
|
|
||||||
|
|
||||||
if value, ok = config.Config[HTTPEndpointAnnotationKey]; !ok {
|
if value, ok = config.Config[HTTPEndpointAnnotationKey]; !ok {
|
||||||
return nil, fmt.Errorf("config value %s not found", HTTPEndpointAnnotationKey)
|
return nil, fmt.Errorf("config value %s not found", HTTPEndpointAnnotationKey)
|
||||||
@ -62,7 +76,7 @@ func (p *HTTPCollectorPlugin) NewCollector(_ context.Context, hpa *autoscalingv2
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jsonPathGetter, err := httpmetrics.NewJSONPathMetricsGetter(httpmetrics.DefaultMetricsHTTPClient(), aggFunc, jsonPath)
|
jsonPathGetter, err := httpmetrics.NewJSONPathMetricsGetter(httpmetrics.DefaultMetricsHTTPClient(), aggFunc, jsonPath, jsonEval)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -16,18 +16,22 @@ import (
|
|||||||
// the json path query.
|
// the json path query.
|
||||||
type JSONPathMetricsGetter struct {
|
type JSONPathMetricsGetter struct {
|
||||||
jsonPath string
|
jsonPath string
|
||||||
|
jsonEval string
|
||||||
aggregator AggregatorFunc
|
aggregator AggregatorFunc
|
||||||
client *http.Client
|
client *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewJSONPathMetricsGetter initializes a new JSONPathMetricsGetter.
|
// NewJSONPathMetricsGetter initializes a new JSONPathMetricsGetter.
|
||||||
func NewJSONPathMetricsGetter(httpClient *http.Client, aggregatorFunc AggregatorFunc, jsonPath string) (*JSONPathMetricsGetter, error) {
|
func NewJSONPathMetricsGetter(httpClient *http.Client, aggregatorFunc AggregatorFunc, jsonPath string, jsonEval string) (*JSONPathMetricsGetter, error) {
|
||||||
// check that jsonPath parses
|
// check that jsonPath parses
|
||||||
_, err := ajson.ParseJSONPath(jsonPath)
|
if jsonPath != "" {
|
||||||
if err != nil {
|
_, err := ajson.ParseJSONPath(jsonPath)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &JSONPathMetricsGetter{client: httpClient, aggregator: aggregatorFunc, jsonPath: jsonPath}, nil
|
|
||||||
|
return &JSONPathMetricsGetter{client: httpClient, aggregator: aggregatorFunc, jsonPath: jsonPath, jsonEval: jsonEval}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var DefaultRequestTimeout = 15 * time.Second
|
var DefaultRequestTimeout = 15 * time.Second
|
||||||
@ -67,9 +71,19 @@ func (g *JSONPathMetricsGetter) GetMetric(metricsURL url.URL) (float64, error) {
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes, err := root.JSONPath(g.jsonPath)
|
var nodes []*ajson.Node
|
||||||
if err != nil {
|
if g.jsonPath != "" {
|
||||||
return 0, err
|
nodes, err = root.JSONPath(g.jsonPath)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result, err := ajson.Eval(root, g.jsonEval)
|
||||||
|
nodes = append(nodes, result)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(nodes) == 0 {
|
if len(nodes) == 0 {
|
||||||
|
@ -26,6 +26,7 @@ func TestJSONPathMetricsGetter(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
jsonResponse []byte
|
jsonResponse []byte
|
||||||
jsonPath string
|
jsonPath string
|
||||||
|
jsonEval string
|
||||||
result float64
|
result float64
|
||||||
aggregator AggregatorFunc
|
aggregator AggregatorFunc
|
||||||
err error
|
err error
|
||||||
@ -58,6 +59,19 @@ func TestJSONPathMetricsGetter(t *testing.T) {
|
|||||||
result: 5,
|
result: 5,
|
||||||
aggregator: Average,
|
aggregator: Average,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "evaluated script",
|
||||||
|
jsonResponse: []byte(`{"active processes":1,"total processes":10}`),
|
||||||
|
jsonEval: "ceil($['active processes'] / $['total processes'] * 100)",
|
||||||
|
result: 10,
|
||||||
|
aggregator: Average,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid script should error",
|
||||||
|
jsonResponse: []byte(`{"active processes":1,"total processes":10}`),
|
||||||
|
jsonEval: "ceil($['active processes'] ) $['total processes'] * 100)",
|
||||||
|
err: errors.New("wrong request: formula has no left parentheses"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "json path not resulting in array or number should lead to error",
|
name: "json path not resulting in array or number should lead to error",
|
||||||
jsonResponse: []byte(`{"metric.value":5}`),
|
jsonResponse: []byte(`{"metric.value":5}`),
|
||||||
@ -74,7 +88,7 @@ func TestJSONPathMetricsGetter(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
server := makeTestHTTPServer(t, tc.jsonResponse)
|
server := makeTestHTTPServer(t, tc.jsonResponse)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
getter, err := NewJSONPathMetricsGetter(DefaultMetricsHTTPClient(), tc.aggregator, tc.jsonPath)
|
getter, err := NewJSONPathMetricsGetter(DefaultMetricsHTTPClient(), tc.aggregator, tc.jsonPath, tc.jsonEval)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
url, err := url.Parse(fmt.Sprintf("%s/metrics", server.URL))
|
url, err := url.Parse(fmt.Sprintf("%s/metrics", server.URL))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -33,6 +33,7 @@ func NewPodMetricsJSONPathGetter(config map[string]string) (*PodMetricsJSONPathG
|
|||||||
getter := PodMetricsJSONPathGetter{}
|
getter := PodMetricsJSONPathGetter{}
|
||||||
var (
|
var (
|
||||||
jsonPath string
|
jsonPath string
|
||||||
|
jsonEval string
|
||||||
aggregator AggregatorFunc
|
aggregator AggregatorFunc
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
@ -41,6 +42,16 @@ func NewPodMetricsJSONPathGetter(config map[string]string) (*PodMetricsJSONPathG
|
|||||||
jsonPath = v
|
jsonPath = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v, ok := config["json-eval"]; ok {
|
||||||
|
jsonEval = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonPath == "" && jsonEval == "" {
|
||||||
|
return nil, fmt.Errorf("config value json-key or json-eval must be set")
|
||||||
|
} else if jsonPath != "" && jsonEval != "" {
|
||||||
|
return nil, fmt.Errorf("config value json-key and json-eval are mutually exclusive")
|
||||||
|
}
|
||||||
|
|
||||||
if v, ok := config["scheme"]; ok {
|
if v, ok := config["scheme"]; ok {
|
||||||
getter.scheme = v
|
getter.scheme = v
|
||||||
}
|
}
|
||||||
@ -93,7 +104,7 @@ func NewPodMetricsJSONPathGetter(config map[string]string) (*PodMetricsJSONPathG
|
|||||||
connectTimeout = d
|
connectTimeout = d
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonPathGetter, err := NewJSONPathMetricsGetter(CustomMetricsHTTPClient(requestTimeout, connectTimeout), aggregator, jsonPath)
|
jsonPathGetter, err := NewJSONPathMetricsGetter(CustomMetricsHTTPClient(requestTimeout, connectTimeout), aggregator, jsonPath, jsonEval)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -86,6 +86,17 @@ func TestNewPodJSONPathMetricsGetter(t *testing.T) {
|
|||||||
port: 9090,
|
port: 9090,
|
||||||
rawQuery: "foo=bar&baz=bop",
|
rawQuery: "foo=bar&baz=bop",
|
||||||
}, getterWithRawQuery)
|
}, getterWithRawQuery)
|
||||||
|
|
||||||
|
configErrorMixedPathEval := map[string]string{
|
||||||
|
"json-key": "{}",
|
||||||
|
"json-eval": "avg($.values)",
|
||||||
|
"scheme": "http",
|
||||||
|
"path": "/metrics",
|
||||||
|
"port": "9090",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err6 := NewPodMetricsJSONPathGetter(configErrorMixedPathEval)
|
||||||
|
require.Error(t, err6)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildMetricsURL(t *testing.T) {
|
func TestBuildMetricsURL(t *testing.T) {
|
||||||
|
Reference in New Issue
Block a user