Skip to content

Commit 72ab3d8

Browse files
committed
WIP: Initial support for CEL
This adds initial support for queries using the common expression language (CEL). Signed-off-by: Manuel Rüger <[email protected]>
1 parent 15d4535 commit 72ab3d8

File tree

6 files changed

+117
-12
lines changed

6 files changed

+117
-12
lines changed

config/config.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
// Metric contains values that define a metric
2424
type Metric struct {
2525
Name string
26+
Engine EngineType
2627
Path string
2728
Labels map[string]string
2829
Type ScrapeType
@@ -44,7 +45,14 @@ type ValueType string
4445
const (
4546
ValueTypeGauge ValueType = "gauge"
4647
ValueTypeCounter ValueType = "counter"
47-
ValueTypeUntyped ValueType = "untyped"
48+
ValueTypeUntyped ValueType = "untyped" // default
49+
)
50+
51+
type EngineType string
52+
53+
const (
54+
EngineTypeJSONPath EngineType = "jsonpath" // default
55+
EngineTypeCEL EngineType = "cel"
4856
)
4957

5058
// Config contains multiple modules.
@@ -89,6 +97,9 @@ func LoadConfig(configPath string) (Config, error) {
8997
if module.Metrics[i].ValueType == "" {
9098
module.Metrics[i].ValueType = ValueTypeUntyped
9199
}
100+
if module.Metrics[i].Engine == "" {
101+
module.Metrics[i].Engine = EngineTypeJSONPath
102+
}
92103
}
93104
}
94105

examples/config.yml

+9-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ modules:
99
help: Example of a top-level global value scrape in the json
1010
labels:
1111
environment: beta # static label
12-
location: 'planet-{.location}' # dynamic label
12+
location: 'planet-{ .location }' # dynamic label
13+
- name: example_cel_global_value
14+
engine: cel
15+
path: '.counter'
16+
help: Example of a top-level global value scrape in the json using cel
17+
valuetype: 'gauge'
18+
labels:
19+
environment: "\"beta\"" # static label. Quotes need to be escaped for CEL
20+
location: "\"planet-\"+.location" # dynamic label. Quotes need to be escaped for CEL
1321
- name: example_timestamped_value
1422
type: object
1523
path: '{ .values[?(@.state == "INACTIVE")] }'

exporter/collector.go

+75-9
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ package exporter
1616
import (
1717
"bytes"
1818
"encoding/json"
19+
"fmt"
1920
"time"
2021

2122
"github.com/go-kit/log"
2223
"github.com/go-kit/log/level"
24+
"github.com/google/cel-go/cel"
2325
"github.com/prometheus-community/json_exporter/config"
2426
"github.com/prometheus/client_golang/prometheus"
2527
"k8s.io/client-go/util/jsonpath"
@@ -34,6 +36,7 @@ type JSONMetricCollector struct {
3436
type JSONMetric struct {
3537
Desc *prometheus.Desc
3638
Type config.ScrapeType
39+
EngineType config.EngineType
3740
KeyJSONPath string
3841
ValueJSONPath string
3942
LabelsJSONPaths []string
@@ -51,7 +54,8 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) {
5154
for _, m := range mc.JSONMetrics {
5255
switch m.Type {
5356
case config.ValueScrape:
54-
value, err := extractValue(mc.Logger, mc.Data, m.KeyJSONPath, false)
57+
level.Debug(mc.Logger).Log("msg", "Extracting value for metric", "path", m.KeyJSONPath, "metric", m.Desc)
58+
value, err := extractValue(mc.Logger, m.EngineType, mc.Data, m.KeyJSONPath, false)
5559
if err != nil {
5660
level.Error(mc.Logger).Log("msg", "Failed to extract value for metric", "path", m.KeyJSONPath, "err", err, "metric", m.Desc)
5761
continue
@@ -62,7 +66,7 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) {
6266
m.Desc,
6367
m.ValueType,
6468
floatValue,
65-
extractLabels(mc.Logger, mc.Data, m.LabelsJSONPaths)...,
69+
extractLabels(mc.Logger, m.EngineType, mc.Data, m.LabelsJSONPaths)...,
6670
)
6771
ch <- timestampMetric(mc.Logger, m, mc.Data, metric)
6872
} else {
@@ -71,7 +75,8 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) {
7175
}
7276

7377
case config.ObjectScrape:
74-
values, err := extractValue(mc.Logger, mc.Data, m.KeyJSONPath, true)
78+
level.Debug(mc.Logger).Log("msg", "Extracting object for metric", "path", m.KeyJSONPath, "metric", m.Desc)
79+
values, err := extractValue(mc.Logger, m.EngineType, mc.Data, m.KeyJSONPath, true)
7580
if err != nil {
7681
level.Error(mc.Logger).Log("msg", "Failed to extract json objects for metric", "err", err, "metric", m.Desc)
7782
continue
@@ -85,7 +90,7 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) {
8590
level.Error(mc.Logger).Log("msg", "Failed to marshal data to json", "path", m.ValueJSONPath, "err", err, "metric", m.Desc, "data", data)
8691
continue
8792
}
88-
value, err := extractValue(mc.Logger, jdata, m.ValueJSONPath, false)
93+
value, err := extractValue(mc.Logger, m.EngineType, jdata, m.ValueJSONPath, false)
8994
if err != nil {
9095
level.Error(mc.Logger).Log("msg", "Failed to extract value for metric", "path", m.ValueJSONPath, "err", err, "metric", m.Desc)
9196
continue
@@ -96,7 +101,7 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) {
96101
m.Desc,
97102
m.ValueType,
98103
floatValue,
99-
extractLabels(mc.Logger, jdata, m.LabelsJSONPaths)...,
104+
extractLabels(mc.Logger, m.EngineType, jdata, m.LabelsJSONPaths)...,
100105
)
101106
ch <- timestampMetric(mc.Logger, m, jdata, metric)
102107
} else {
@@ -115,8 +120,19 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) {
115120
}
116121
}
117122

123+
func extractValue(logger log.Logger, engine config.EngineType, data []byte, path string, enableJSONOutput bool) (string, error) {
124+
switch engine {
125+
case config.EngineTypeJSONPath:
126+
return extractValueJSONPath(logger, data, path, enableJSONOutput)
127+
case config.EngineTypeCEL:
128+
return extractValueCEL(logger, data, path)
129+
default:
130+
return "", fmt.Errorf("Unknown engine type: %s", engine)
131+
}
132+
}
133+
118134
// Returns the last matching value at the given json path
119-
func extractValue(logger log.Logger, data []byte, path string, enableJSONOutput bool) (string, error) {
135+
func extractValueJSONPath(logger log.Logger, data []byte, path string, enableJSONOutput bool) (string, error) {
120136
var jsonData interface{}
121137
buf := new(bytes.Buffer)
122138

@@ -148,11 +164,61 @@ func extractValue(logger log.Logger, data []byte, path string, enableJSONOutput
148164
return buf.String(), nil
149165
}
150166

167+
// Returns the last matching value at the given json path
168+
func extractValueCEL(logger log.Logger, data []byte, expression string) (string, error) {
169+
170+
var jsonData map[string]any
171+
172+
err := json.Unmarshal(data, &jsonData)
173+
if err != nil {
174+
level.Error(logger).Log("msg", "Failed to unmarshal data to json", "err", err, "data", data)
175+
return "", err
176+
}
177+
178+
inputVars := make([]cel.EnvOption, 0, len(jsonData))
179+
for k := range jsonData {
180+
inputVars = append(inputVars, cel.Variable(k, cel.DynType))
181+
}
182+
183+
env, err := cel.NewEnv(inputVars...)
184+
185+
if err != nil {
186+
level.Error(logger).Log("msg", "Failed to set up CEL environment", "err", err, "data", data)
187+
return "", err
188+
}
189+
190+
ast, issues := env.Compile(expression)
191+
if issues != nil && issues.Err() != nil {
192+
level.Error(logger).Log("CEL type-check error", issues.Err(), "expression", expression)
193+
return "", err
194+
}
195+
prg, err := env.Program(ast)
196+
if err != nil {
197+
level.Error(logger).Log("CEL program construction error", err)
198+
return "", err
199+
}
200+
201+
out, _, err := prg.Eval(jsonData)
202+
if err != nil {
203+
level.Error(logger).Log("msg", "Failed to evaluate cel query", "err", err, "expression", expression, "data", jsonData)
204+
return "", err
205+
}
206+
207+
result := out.ConvertToType(cel.StringType)
208+
209+
// Since we are finally going to extract only float64, unquote if necessary
210+
if res, err := jsonpath.UnquoteExtend(result.Value().(string)); err == nil {
211+
return res, nil
212+
}
213+
214+
return result.Value().(string), nil
215+
}
216+
151217
// Returns the list of labels created from the list of provided json paths
152-
func extractLabels(logger log.Logger, data []byte, paths []string) []string {
218+
func extractLabels(logger log.Logger, engine config.EngineType, data []byte, paths []string) []string {
153219
labels := make([]string, len(paths))
154220
for i, path := range paths {
155-
if result, err := extractValue(logger, data, path, false); err == nil {
221+
if result, err := extractValue(logger, engine, data, path, false); err == nil {
156222
labels[i] = result
157223
} else {
158224
level.Error(logger).Log("msg", "Failed to extract label value", "err", err, "path", path, "data", data)
@@ -165,7 +231,7 @@ func timestampMetric(logger log.Logger, m JSONMetric, data []byte, pm prometheus
165231
if m.EpochTimestampJSONPath == "" {
166232
return pm
167233
}
168-
ts, err := extractValue(logger, data, m.EpochTimestampJSONPath, false)
234+
ts, err := extractValue(logger, m.EngineType, data, m.EpochTimestampJSONPath, false)
169235
if err != nil {
170236
level.Error(logger).Log("msg", "Failed to extract timestamp for metric", "path", m.KeyJSONPath, "err", err, "metric", m.Desc)
171237
return pm

exporter/util.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ func CreateMetricsList(c config.Module) ([]JSONMetric, error) {
103103
variableLabels,
104104
nil,
105105
),
106+
EngineType: metric.Engine,
106107
KeyJSONPath: metric.Path,
107108
LabelsJSONPaths: variableLabelsValues,
108109
ValueType: valueType,
@@ -125,6 +126,7 @@ func CreateMetricsList(c config.Module) ([]JSONMetric, error) {
125126
variableLabels,
126127
nil,
127128
),
129+
EngineType: metric.Engine,
128130
KeyJSONPath: metric.Path,
129131
ValueJSONPath: valuePath,
130132
LabelsJSONPaths: variableLabelsValues,
@@ -134,7 +136,7 @@ func CreateMetricsList(c config.Module) ([]JSONMetric, error) {
134136
metrics = append(metrics, jsonMetric)
135137
}
136138
default:
137-
return nil, fmt.Errorf("Unknown metric type: '%s', for metric: '%s'", metric.Type, metric.Name)
139+
return nil, fmt.Errorf("unknown metric type: '%s', for metric: '%s'", metric.Type, metric.Name)
138140
}
139141
}
140142
return metrics, nil

go.mod

+6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/Masterminds/sprig/v3 v3.2.3
77
github.com/alecthomas/kingpin/v2 v2.3.2
88
github.com/go-kit/log v0.2.1
9+
github.com/google/cel-go v0.18.2
910
github.com/prometheus/client_golang v1.17.0
1011
github.com/prometheus/common v0.45.0
1112
github.com/prometheus/exporter-toolkit v0.10.0
@@ -17,6 +18,7 @@ require (
1718
github.com/Masterminds/goutils v1.1.1 // indirect
1819
github.com/Masterminds/semver/v3 v3.2.0 // indirect
1920
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
21+
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
2022
github.com/beorn7/perks v1.0.1 // indirect
2123
github.com/cespare/xxhash/v2 v2.2.0 // indirect
2224
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
@@ -35,13 +37,17 @@ require (
3537
github.com/prometheus/procfs v0.11.1 // indirect
3638
github.com/shopspring/decimal v1.2.0 // indirect
3739
github.com/spf13/cast v1.3.1 // indirect
40+
github.com/stoewer/go-strcase v1.2.0 // indirect
3841
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
3942
golang.org/x/crypto v0.14.0 // indirect
43+
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
4044
golang.org/x/net v0.17.0 // indirect
4145
golang.org/x/oauth2 v0.12.0 // indirect
4246
golang.org/x/sync v0.3.0 // indirect
4347
golang.org/x/sys v0.13.0 // indirect
4448
golang.org/x/text v0.13.0 // indirect
4549
google.golang.org/appengine v1.6.7 // indirect
50+
google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 // indirect
51+
google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 // indirect
4652
google.golang.org/protobuf v1.31.0 // indirect
4753
)

go.sum

+12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWr
88
github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
99
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
1010
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
11+
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
12+
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
1113
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
1214
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
1315
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
@@ -27,6 +29,8 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
2729
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
2830
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
2931
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
32+
github.com/google/cel-go v0.18.2 h1:L0B6sNBSVmt0OyECi8v6VOS74KOc9W/tLiWKfZABvf4=
33+
github.com/google/cel-go v0.18.2/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
3034
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
3135
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
3236
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -66,6 +70,8 @@ github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXY
6670
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
6771
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
6872
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
73+
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
74+
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
6975
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
7076
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
7177
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -79,6 +85,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
7985
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
8086
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
8187
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
88+
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
89+
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
8290
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
8391
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
8492
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -118,6 +126,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
118126
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
119127
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
120128
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
129+
google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 h1:nIgk/EEq3/YlnmVVXVnm14rC2oxgs1o0ong4sD/rd44=
130+
google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q=
131+
google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 h1:eSaPbMR4T7WfH9FvABk36NBMacoTUKdWCvV0dx+KfOg=
132+
google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5/go.mod h1:zBEcrKX2ZOcEkHWxBPAIvYUWOKKMIhYcmNiUIu2ji3I=
121133
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
122134
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
123135
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=

0 commit comments

Comments
 (0)