Add support for ajson scripting engine in Pod collector through json-eval config key

Signed-off-by: Angel Alonso <angel.alonso@automattic.com>
This commit is contained in:
Angel Alonso
2025-01-28 20:17:28 +00:00
parent 4204daa44f
commit 5f6a683d64
5 changed files with 61 additions and 11 deletions

View File

@ -62,7 +62,7 @@ func (p *HTTPCollectorPlugin) NewCollector(_ context.Context, hpa *autoscalingv2
return nil, err
}
}
jsonPathGetter, err := httpmetrics.NewJSONPathMetricsGetter(httpmetrics.DefaultMetricsHTTPClient(), aggFunc, jsonPath)
jsonPathGetter, err := httpmetrics.NewJSONPathMetricsGetter(httpmetrics.DefaultMetricsHTTPClient(), aggFunc, jsonPath, "")
if err != nil {
return nil, err
}

View File

@ -16,18 +16,22 @@ import (
// the json path query.
type JSONPathMetricsGetter struct {
jsonPath string
jsonEval string
aggregator AggregatorFunc
client *http.Client
}
// 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
_, err := ajson.ParseJSONPath(jsonPath)
if err != nil {
return nil, err
if jsonPath != "" {
_, err := ajson.ParseJSONPath(jsonPath)
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
@ -67,9 +71,19 @@ func (g *JSONPathMetricsGetter) GetMetric(metricsURL url.URL) (float64, error) {
return 0, err
}
nodes, err := root.JSONPath(g.jsonPath)
if err != nil {
return 0, err
var nodes []*ajson.Node
if g.jsonPath != "" {
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 {

View File

@ -26,6 +26,7 @@ func TestJSONPathMetricsGetter(t *testing.T) {
name string
jsonResponse []byte
jsonPath string
jsonEval string
result float64
aggregator AggregatorFunc
err error
@ -58,6 +59,19 @@ func TestJSONPathMetricsGetter(t *testing.T) {
result: 5,
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",
jsonResponse: []byte(`{"metric.value":5}`),
@ -74,7 +88,7 @@ func TestJSONPathMetricsGetter(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
server := makeTestHTTPServer(t, tc.jsonResponse)
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)
url, err := url.Parse(fmt.Sprintf("%s/metrics", server.URL))
require.NoError(t, err)

View File

@ -33,6 +33,7 @@ func NewPodMetricsJSONPathGetter(config map[string]string) (*PodMetricsJSONPathG
getter := PodMetricsJSONPathGetter{}
var (
jsonPath string
jsonEval string
aggregator AggregatorFunc
err error
)
@ -41,6 +42,16 @@ func NewPodMetricsJSONPathGetter(config map[string]string) (*PodMetricsJSONPathG
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 {
getter.scheme = v
}
@ -93,7 +104,7 @@ func NewPodMetricsJSONPathGetter(config map[string]string) (*PodMetricsJSONPathG
connectTimeout = d
}
jsonPathGetter, err := NewJSONPathMetricsGetter(CustomMetricsHTTPClient(requestTimeout, connectTimeout), aggregator, jsonPath)
jsonPathGetter, err := NewJSONPathMetricsGetter(CustomMetricsHTTPClient(requestTimeout, connectTimeout), aggregator, jsonPath, jsonEval)
if err != nil {
return nil, err
}

View File

@ -86,6 +86,17 @@ func TestNewPodJSONPathMetricsGetter(t *testing.T) {
port: 9090,
rawQuery: "foo=bar&baz=bop",
}, 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) {