Skip to content

Commit 9c8837c

Browse files
authored
Merge pull request #783 from Adirael/feature/support-json-eval
Add support for ajson scripting engine
2 parents ec55cda + e0a1ca5 commit 9c8837c

File tree

6 files changed

+86
-14
lines changed

6 files changed

+86
-14
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ metadata:
116116
annotations:
117117
# metric-config.<metricType>.<metricName>.<collectorType>/<configKey>
118118
metric-config.pods.requests-per-second.json-path/json-key: "$.http_server.rps"
119+
metric-config.pods.requests-per-second.json-path/json-eval: "ceil($['active processes'] / $['total processes'] * 100)" # cannot use both json-eval and json-key
119120
metric-config.pods.requests-per-second.json-path/path: /metrics
120121
metric-config.pods.requests-per-second.json-path/port: "9090"
121122
metric-config.pods.requests-per-second.json-path/scheme: "https"
@@ -158,6 +159,10 @@ The json-path query support depends on the
158159
See the README for possible queries. It's expected that the metric you query
159160
returns something that can be turned into a `float64`.
160161

162+
The `json-eval` configuration option allows for more complex calculations to be
163+
performed on the extracted metric. The `json-eval` expression is evaluated using
164+
[ajson's script engine](https://github.com/spyzhov/ajson?tab=readme-ov-file#script-engine).
165+
161166
The other configuration options `path`, `port` and `scheme` specify where the metrics
162167
endpoint is exposed on the pod. The `path` and `port` options do not have default values
163168
so they must be defined. The `scheme` is optional and defaults to `http`.
@@ -825,6 +830,7 @@ metadata:
825830
annotations:
826831
# metric-config.<metricType>.<metricName>.<collectorType>/<configKey>
827832
metric-config.external.unique-metric-name.json-path/json-key: "$.some-metric.value"
833+
metric-config.external.unique-metric-name.json-path/json-eval: ceil($['active processes'] / $['total processes'] * 100) # cannot use both json-eval and json-key
828834
metric-config.external.unique-metric-name.json-path/endpoint: "http://metric-source.app-namespace:8080/metrics"
829835
metric-config.external.unique-metric-name.json-path/aggregator: "max"
830836
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
852858
configuration values are supported:
853859

854860
- `json-key` to specify the JSON path of the metric to be queried
861+
- `json-eval` to specify an evaluate string to [evaluate on the script engine](https://github.com/spyzhov/ajson?tab=readme-ov-file#script-engine),
862+
cannot be used in conjunction with `json-key`
855863
- `endpoint` the fully formed path to query for the metric. In the above example a Kubernetes _Service_
856864
in the namespace `app-namespace` is called.
857865
- `aggregator` is only required if the metric is an array of values and specifies how the values

pkg/collector/http_collector.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const (
1919
HTTPMetricNameLegacy = "http"
2020
HTTPEndpointAnnotationKey = "endpoint"
2121
HTTPJsonPathAnnotationKey = "json-key"
22+
HTTPJsonEvalAnnotationKey = "json-eval"
2223
)
2324

2425
type HTTPCollectorPlugin struct{}
@@ -31,14 +32,27 @@ func (p *HTTPCollectorPlugin) NewCollector(_ context.Context, hpa *autoscalingv2
3132
collector := &HTTPCollector{
3233
namespace: hpa.Namespace,
3334
}
35+
3436
var (
3537
value string
3638
ok bool
39+
40+
jsonPath string
41+
jsonEval string
3742
)
38-
if value, ok = config.Config[HTTPJsonPathAnnotationKey]; !ok {
39-
return nil, fmt.Errorf("config value %s not found", HTTPJsonPathAnnotationKey)
43+
44+
if value, ok = config.Config[HTTPJsonPathAnnotationKey]; ok {
45+
jsonPath = value
46+
}
47+
if value, ok = config.Config[HTTPJsonEvalAnnotationKey]; ok {
48+
jsonEval = value
49+
}
50+
if jsonPath == "" && jsonEval == "" {
51+
return nil, fmt.Errorf("config value %s or %s not found", HTTPJsonPathAnnotationKey, HTTPJsonEvalAnnotationKey)
52+
}
53+
if jsonPath != "" && jsonEval != "" {
54+
return nil, fmt.Errorf("config value %s and %s cannot be used together", HTTPJsonPathAnnotationKey, HTTPJsonEvalAnnotationKey)
4055
}
41-
jsonPath := value
4256

4357
if value, ok = config.Config[HTTPEndpointAnnotationKey]; !ok {
4458
return nil, fmt.Errorf("config value %s not found", HTTPEndpointAnnotationKey)
@@ -62,7 +76,7 @@ func (p *HTTPCollectorPlugin) NewCollector(_ context.Context, hpa *autoscalingv2
6276
return nil, err
6377
}
6478
}
65-
jsonPathGetter, err := httpmetrics.NewJSONPathMetricsGetter(httpmetrics.DefaultMetricsHTTPClient(), aggFunc, jsonPath)
79+
jsonPathGetter, err := httpmetrics.NewJSONPathMetricsGetter(httpmetrics.DefaultMetricsHTTPClient(), aggFunc, jsonPath, jsonEval)
6680
if err != nil {
6781
return nil, err
6882
}

pkg/collector/httpmetrics/json_path.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,22 @@ import (
1616
// the json path query.
1717
type JSONPathMetricsGetter struct {
1818
jsonPath string
19+
jsonEval string
1920
aggregator AggregatorFunc
2021
client *http.Client
2122
}
2223

2324
// NewJSONPathMetricsGetter initializes a new JSONPathMetricsGetter.
24-
func NewJSONPathMetricsGetter(httpClient *http.Client, aggregatorFunc AggregatorFunc, jsonPath string) (*JSONPathMetricsGetter, error) {
25+
func NewJSONPathMetricsGetter(httpClient *http.Client, aggregatorFunc AggregatorFunc, jsonPath string, jsonEval string) (*JSONPathMetricsGetter, error) {
2526
// check that jsonPath parses
26-
_, err := ajson.ParseJSONPath(jsonPath)
27-
if err != nil {
28-
return nil, err
27+
if jsonPath != "" {
28+
_, err := ajson.ParseJSONPath(jsonPath)
29+
if err != nil {
30+
return nil, err
31+
}
2932
}
30-
return &JSONPathMetricsGetter{client: httpClient, aggregator: aggregatorFunc, jsonPath: jsonPath}, nil
33+
34+
return &JSONPathMetricsGetter{client: httpClient, aggregator: aggregatorFunc, jsonPath: jsonPath, jsonEval: jsonEval}, nil
3135
}
3236

3337
var DefaultRequestTimeout = 15 * time.Second
@@ -67,9 +71,19 @@ func (g *JSONPathMetricsGetter) GetMetric(metricsURL url.URL) (float64, error) {
6771
return 0, err
6872
}
6973

70-
nodes, err := root.JSONPath(g.jsonPath)
71-
if err != nil {
72-
return 0, err
74+
var nodes []*ajson.Node
75+
if g.jsonPath != "" {
76+
nodes, err = root.JSONPath(g.jsonPath)
77+
if err != nil {
78+
return 0, err
79+
}
80+
} else {
81+
result, err := ajson.Eval(root, g.jsonEval)
82+
nodes = append(nodes, result)
83+
84+
if err != nil {
85+
return 0, err
86+
}
7387
}
7488

7589
if len(nodes) == 0 {

pkg/collector/httpmetrics/json_path_test.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func TestJSONPathMetricsGetter(t *testing.T) {
2626
name string
2727
jsonResponse []byte
2828
jsonPath string
29+
jsonEval string
2930
result float64
3031
aggregator AggregatorFunc
3132
err error
@@ -58,6 +59,19 @@ func TestJSONPathMetricsGetter(t *testing.T) {
5859
result: 5,
5960
aggregator: Average,
6061
},
62+
{
63+
name: "evaluated script",
64+
jsonResponse: []byte(`{"active processes":1,"total processes":10}`),
65+
jsonEval: "ceil($['active processes'] / $['total processes'] * 100)",
66+
result: 10,
67+
aggregator: Average,
68+
},
69+
{
70+
name: "invalid script should error",
71+
jsonResponse: []byte(`{"active processes":1,"total processes":10}`),
72+
jsonEval: "ceil($['active processes'] ) $['total processes'] * 100)",
73+
err: errors.New("wrong request: formula has no left parentheses"),
74+
},
6175
{
6276
name: "json path not resulting in array or number should lead to error",
6377
jsonResponse: []byte(`{"metric.value":5}`),
@@ -74,7 +88,7 @@ func TestJSONPathMetricsGetter(t *testing.T) {
7488
t.Run(tc.name, func(t *testing.T) {
7589
server := makeTestHTTPServer(t, tc.jsonResponse)
7690
defer server.Close()
77-
getter, err := NewJSONPathMetricsGetter(DefaultMetricsHTTPClient(), tc.aggregator, tc.jsonPath)
91+
getter, err := NewJSONPathMetricsGetter(DefaultMetricsHTTPClient(), tc.aggregator, tc.jsonPath, tc.jsonEval)
7892
require.NoError(t, err)
7993
url, err := url.Parse(fmt.Sprintf("%s/metrics", server.URL))
8094
require.NoError(t, err)

pkg/collector/httpmetrics/pod_metrics.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func NewPodMetricsJSONPathGetter(config map[string]string) (*PodMetricsJSONPathG
3333
getter := PodMetricsJSONPathGetter{}
3434
var (
3535
jsonPath string
36+
jsonEval string
3637
aggregator AggregatorFunc
3738
err error
3839
)
@@ -41,6 +42,16 @@ func NewPodMetricsJSONPathGetter(config map[string]string) (*PodMetricsJSONPathG
4142
jsonPath = v
4243
}
4344

45+
if v, ok := config["json-eval"]; ok {
46+
jsonEval = v
47+
}
48+
49+
if jsonPath == "" && jsonEval == "" {
50+
return nil, fmt.Errorf("config value json-key or json-eval must be set")
51+
} else if jsonPath != "" && jsonEval != "" {
52+
return nil, fmt.Errorf("config value json-key and json-eval are mutually exclusive")
53+
}
54+
4455
if v, ok := config["scheme"]; ok {
4556
getter.scheme = v
4657
}
@@ -93,7 +104,7 @@ func NewPodMetricsJSONPathGetter(config map[string]string) (*PodMetricsJSONPathG
93104
connectTimeout = d
94105
}
95106

96-
jsonPathGetter, err := NewJSONPathMetricsGetter(CustomMetricsHTTPClient(requestTimeout, connectTimeout), aggregator, jsonPath)
107+
jsonPathGetter, err := NewJSONPathMetricsGetter(CustomMetricsHTTPClient(requestTimeout, connectTimeout), aggregator, jsonPath, jsonEval)
97108
if err != nil {
98109
return nil, err
99110
}

pkg/collector/httpmetrics/pod_metrics_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,17 @@ func TestNewPodJSONPathMetricsGetter(t *testing.T) {
8686
port: 9090,
8787
rawQuery: "foo=bar&baz=bop",
8888
}, getterWithRawQuery)
89+
90+
configErrorMixedPathEval := map[string]string{
91+
"json-key": "{}",
92+
"json-eval": "avg($.values)",
93+
"scheme": "http",
94+
"path": "/metrics",
95+
"port": "9090",
96+
}
97+
98+
_, err6 := NewPodMetricsJSONPathGetter(configErrorMixedPathEval)
99+
require.Error(t, err6)
89100
}
90101

91102
func TestBuildMetricsURL(t *testing.T) {

0 commit comments

Comments
 (0)