From 203443773f5536c06c55c95f30b089237d0edb19 Mon Sep 17 00:00:00 2001 From: Juho Majasaari Date: Wed, 5 Jun 2024 14:25:12 +0300 Subject: [PATCH 1/3] Support matching JSON body with CEL expressions Signed-off-by: Juho Majasaari --- CONFIGURATION.md | 6 ++ config/config.go | 72 +++++++++++++++++++++++ go.mod | 5 ++ go.sum | 11 ++++ prober/http.go | 69 ++++++++++++++++++++++ prober/http_test.go | 138 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 301 insertions(+) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 05982c547..c720dc4ef 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -89,6 +89,12 @@ modules: # Probe fails if SSL is not present. [ fail_if_not_ssl: | default = false ] + # Probe fails if response body JSON matches CEL: + fail_if_body_json_matches_cel: + + # Probe fails if response body JSON does not match CEL: + fail_if_body_json_not_matches_cel: + # Probe fails if response body matches regex. fail_if_body_matches_regexp: [ - , ... ] diff --git a/config/config.go b/config/config.go index 143095d5e..e94cec689 100644 --- a/config/config.go +++ b/config/config.go @@ -28,6 +28,8 @@ import ( "sync" "time" + "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker/decls" yaml "gopkg.in/yaml.v3" "github.com/alecthomas/units" @@ -144,6 +146,74 @@ func (sc *SafeConfig) ReloadConfig(confFile string, logger *slog.Logger) (err er return nil } +// CelProgram encapsulates a cel.Program and makes it YAML marshalable. +type CelProgram struct { + cel.Program + expression string +} + +// NewCelProgram creates a new CEL Program and returns an error if the +// passed-in CEL expression does not compile. +func NewCelProgram(s string) (CelProgram, error) { + program := CelProgram{ + expression: s, + } + + env, err := cel.NewEnv( + cel.Declarations( + decls.NewVar("body", decls.NewMapType(decls.String, decls.Dyn)), + ), + ) + if err != nil { + return program, fmt.Errorf("error creating CEL environment: %s", err) + } + + ast, issues := env.Compile(s) + if issues != nil && issues.Err() != nil { + return program, fmt.Errorf("error compiling CEL program: %s", issues.Err()) + } + + celProg, err := env.Program(ast) + if err != nil { + return program, fmt.Errorf("error creating CEL program: %s", err) + } + + program.Program = celProg + + return program, nil +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *CelProgram) UnmarshalYAML(unmarshal func(interface{}) error) error { + var expr string + if err := unmarshal(&expr); err != nil { + return err + } + celProg, err := NewCelProgram(expr) + if err != nil { + return fmt.Errorf("\"Could not compile CEL program\" expression=\"%s\"", expr) + } + *c = celProg + return nil +} + +// MarshalYAML implements the yaml.Marshaler interface. +func (c CelProgram) MarshalYAML() (interface{}, error) { + if c.expression != "" { + return c.expression, nil + } + return nil, nil +} + +// MustNewCelProgram works like NewCelProgram, but panics if the CEL expression does not compile. +func MustNewCelProgram(s string) CelProgram { + c, err := NewCelProgram(s) + if err != nil { + panic(err) + } + return c +} + // Regexp encapsulates a regexp.Regexp and makes it YAML marshalable. type Regexp struct { *regexp.Regexp @@ -215,6 +285,8 @@ type HTTPProbe struct { Headers map[string]string `yaml:"headers,omitempty"` FailIfBodyMatchesRegexp []Regexp `yaml:"fail_if_body_matches_regexp,omitempty"` FailIfBodyNotMatchesRegexp []Regexp `yaml:"fail_if_body_not_matches_regexp,omitempty"` + FailIfBodyJSONMatchesCel *CelProgram `yaml:"fail_if_body_json_matches_cel,omitempty"` + FailIfBodyJSONNotMatchesCel *CelProgram `yaml:"fail_if_body_json_not_matches_cel,omitempty"` FailIfHeaderMatchesRegexp []HeaderMatch `yaml:"fail_if_header_matches,omitempty"` FailIfHeaderNotMatchesRegexp []HeaderMatch `yaml:"fail_if_header_not_matches,omitempty"` Body string `yaml:"body,omitempty"` diff --git a/go.mod b/go.mod index a5dd2e6c1..b7db6cd0e 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/alecthomas/kingpin/v2 v2.4.0 github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 github.com/andybalholm/brotli v1.1.1 + github.com/google/cel-go v0.20.1 github.com/miekg/dns v1.1.62 github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_model v0.6.1 @@ -18,6 +19,7 @@ require ( ) require ( + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect @@ -28,14 +30,17 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect golang.org/x/crypto v0.31.0 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/mod v0.18.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.22.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect google.golang.org/protobuf v1.35.2 // indirect ) diff --git a/go.sum b/go.sum index 9ea3f2cac..d4496d77c 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4 github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -20,6 +22,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= +github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -58,8 +62,11 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= @@ -78,6 +85,8 @@ go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HY go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= @@ -92,6 +101,8 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= diff --git a/prober/http.go b/prober/http.go index 864bf93bf..138a70ab8 100644 --- a/prober/http.go +++ b/prober/http.go @@ -18,6 +18,7 @@ import ( "compress/gzip" "context" "crypto/tls" + "encoding/json" "errors" "fmt" "io" @@ -35,6 +36,7 @@ import ( "time" "github.com/andybalholm/brotli" + "github.com/google/cel-go/cel" "github.com/prometheus/client_golang/prometheus" pconfig "github.com/prometheus/common/config" "github.com/prometheus/common/version" @@ -64,6 +66,58 @@ func matchRegularExpressions(reader io.Reader, httpConfig config.HTTPProbe, logg return true } +func matchCelExpressions(reader io.Reader, httpConfig config.HTTPProbe, logger *slog.Logger) bool { + body, err := io.ReadAll(reader) + if err != nil { + logger.Error("msg", "Error reading HTTP body", "err", err) + return false + } + + bodyJSON := make(map[string]interface{}) + if err := json.Unmarshal(body, &bodyJSON); err != nil { + logger.Error("msg", "Error unmarshalling HTTP body", "err", err) + return false + } + + evalPayload := map[string]interface{}{ + "body": bodyJSON, + } + + if httpConfig.FailIfBodyJSONMatchesCel != nil { + result, details, err := httpConfig.FailIfBodyJSONMatchesCel.Eval(evalPayload) + if err != nil { + logger.Error("msg", "Error evaluating CEL expression", "err", err) + return false + } + if result.Type() != cel.BoolType { + logger.Error("msg", "CEL evaluation result is not a boolean", "details", details) + return false + } + if result.Type() == cel.BoolType && result.Value().(bool) { + logger.Error("msg", "Body matched CEL expression", "expression", httpConfig.FailIfBodyJSONMatchesCel) + return false + } + } + + if httpConfig.FailIfBodyJSONNotMatchesCel != nil { + result, details, err := httpConfig.FailIfBodyJSONNotMatchesCel.Eval(evalPayload) + if err != nil { + logger.Error("msg", "Error evaluating CEL expression", "err", err) + return false + } + if result.Type() != cel.BoolType { + logger.Error("msg", "CEL evaluation result is not a boolean", "details", details) + return false + } + if result.Type() == cel.BoolType && !result.Value().(bool) { + logger.Error("msg", "Body did not match CEL expression", "expression", httpConfig.FailIfBodyJSONNotMatchesCel) + return false + } + } + + return true +} + func matchRegularExpressionsOnHeaders(header http.Header, httpConfig config.HTTPProbe, logger *slog.Logger) bool { for _, headerMatchSpec := range httpConfig.FailIfHeaderMatchesRegexp { values := header[textproto.CanonicalMIMEHeaderKey(headerMatchSpec.Header)] @@ -296,6 +350,11 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr Help: "Indicates if probe failed due to regex", }) + probeFailedDueToCel = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "probe_failed_due_to_cel", + Help: "Indicates if probe failed due to CEL", + }) + probeHTTPLastModified = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_http_last_modified_timestamp_seconds", Help: "Returns the Last-Modified HTTP response header in unixtime", @@ -310,6 +369,7 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr registry.MustRegister(statusCodeGauge) registry.MustRegister(probeHTTPVersionGauge) registry.MustRegister(probeFailedDueToRegex) + registry.MustRegister(probeFailedDueToCel) httpConfig := module.HTTP @@ -547,6 +607,15 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr } } + if success && (httpConfig.FailIfBodyJSONMatchesCel != nil || httpConfig.FailIfBodyJSONNotMatchesCel != nil) { + success = matchCelExpressions(byteCounter, httpConfig, logger) + if success { + probeFailedDueToCel.Set(0) + } else { + probeFailedDueToCel.Set(1) + } + } + if !requestErrored { _, err = io.Copy(io.Discard, byteCounter) if err != nil { diff --git a/prober/http_test.go b/prober/http_test.go index 0f14816bc..562043c6f 100644 --- a/prober/http_test.go +++ b/prober/http_test.go @@ -936,6 +936,144 @@ func TestFailIfNotSSLLogMsg(t *testing.T) { } } +func TestFailIfBodyMatchesCel(t *testing.T) { + testcases := map[string]struct { + respBody string + cel config.CelProgram + expectedResult bool + }{ + "cel matches": { + respBody: `{"foo": {"bar": "baz"}}`, + cel: config.MustNewCelProgram("body.foo.bar == 'baz'"), + expectedResult: false, + }, + "cel does not match": { + respBody: `{"foo": {"bar": "baz"}}`, + cel: config.MustNewCelProgram("body.foo.bar == 'qux'"), + expectedResult: true, + }, + "cel does not match with empty body": { + respBody: `{}`, + cel: config.MustNewCelProgram("body.foo.bar == 'qux'"), + expectedResult: false, + }, + "cel result not boolean": { + respBody: `{"foo": {"bar": "baz"}}`, + cel: config.MustNewCelProgram("body.foo.bar"), + expectedResult: false, + }, + "body is not json": { + respBody: "hello world", + cel: config.MustNewCelProgram("body.foo.bar == 'baz'"), + expectedResult: false, + }, + } + + for name, testcase := range testcases { + t.Run(name, func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, testcase.respBody) + })) + defer ts.Close() + + recorder := httptest.NewRecorder() + registry := prometheus.NewRegistry() + testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyJSONMatchesCel: &testcase.cel}}, registry, log.NewNopLogger()) + if testcase.expectedResult && !result { + t.Fatalf("CEL test failed unexpectedly, got %s", recorder.Body.String()) + } else if !testcase.expectedResult && result { + t.Fatalf("CEL test succeeded unexpectedly, got %s", recorder.Body.String()) + } + mfs, err := registry.Gather() + if err != nil { + t.Fatal(err) + } + boolToFloat := func(v bool) float64 { + if v { + return 1 + } + return 0 + } + expectedResults := map[string]float64{ + "probe_failed_due_to_cel": boolToFloat(!testcase.expectedResult), + "probe_http_content_length": float64(len(testcase.respBody)), // Issue #673: check that this is correctly populated when using regex validations. + "probe_http_uncompressed_body_length": float64(len(testcase.respBody)), // Issue #673, see above. + } + checkRegistryResults(expectedResults, mfs, t) + }) + } +} + +func TestFailIfBodyNotMatchesCel(t *testing.T) { + testcases := map[string]struct { + respBody string + cel config.CelProgram + expectedResult bool + }{ + "cel matches": { + respBody: `{"foo": {"bar": "baz"}}`, + cel: config.MustNewCelProgram("body.foo.bar == 'baz'"), + expectedResult: true, + }, + "cel does not match": { + respBody: `{"foo": {"bar": "baz"}}`, + cel: config.MustNewCelProgram("body.foo.bar == 'qux'"), + expectedResult: false, + }, + "cel does not match with empty body": { + respBody: `{}`, + cel: config.MustNewCelProgram("body.foo.bar == 'qux'"), + expectedResult: false, + }, + "cel result not boolean": { + respBody: `{"foo": {"bar": "baz"}}`, + cel: config.MustNewCelProgram("body.foo.bar"), + expectedResult: false, + }, + "body is not json": { + respBody: "hello world", + cel: config.MustNewCelProgram("body.foo.bar == 'baz'"), + expectedResult: false, + }, + } + + for name, testcase := range testcases { + t.Run(name, func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, testcase.respBody) + })) + defer ts.Close() + + recorder := httptest.NewRecorder() + registry := prometheus.NewRegistry() + testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyJSONNotMatchesCel: &testcase.cel}}, registry, log.NewNopLogger()) + if testcase.expectedResult && !result { + t.Fatalf("CEL test failed unexpectedly, got %s", recorder.Body.String()) + } else if !testcase.expectedResult && result { + t.Fatalf("CEL test succeeded unexpectedly, got %s", recorder.Body.String()) + } + mfs, err := registry.Gather() + if err != nil { + t.Fatal(err) + } + boolToFloat := func(v bool) float64 { + if v { + return 1 + } + return 0 + } + expectedResults := map[string]float64{ + "probe_failed_due_to_cel": boolToFloat(!testcase.expectedResult), + } + checkRegistryResults(expectedResults, mfs, t) + }) + } +} + func TestFailIfBodyMatchesRegexp(t *testing.T) { testcases := map[string]struct { respBody string From 7be6b2f1db9942f7e00c2a4ce4b573c8e5de8a47 Mon Sep 17 00:00:00 2001 From: Juho Majasaari Date: Thu, 1 Aug 2024 15:48:06 +0300 Subject: [PATCH 2/3] Use ContextEval with Cel to ensure timeouts are respected Signed-off-by: Juho Majasaari --- config/config.go | 2 +- prober/http.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index e94cec689..89a98c9a3 100644 --- a/config/config.go +++ b/config/config.go @@ -173,7 +173,7 @@ func NewCelProgram(s string) (CelProgram, error) { return program, fmt.Errorf("error compiling CEL program: %s", issues.Err()) } - celProg, err := env.Program(ast) + celProg, err := env.Program(ast, cel.InterruptCheckFrequency(100)) if err != nil { return program, fmt.Errorf("error creating CEL program: %s", err) } diff --git a/prober/http.go b/prober/http.go index 138a70ab8..c18f008ec 100644 --- a/prober/http.go +++ b/prober/http.go @@ -84,7 +84,7 @@ func matchCelExpressions(reader io.Reader, httpConfig config.HTTPProbe, logger * } if httpConfig.FailIfBodyJSONMatchesCel != nil { - result, details, err := httpConfig.FailIfBodyJSONMatchesCel.Eval(evalPayload) + result, details, err := httpConfig.FailIfBodyJSONMatchesCel.ContextEval(ctx, evalPayload) if err != nil { logger.Error("msg", "Error evaluating CEL expression", "err", err) return false @@ -100,7 +100,7 @@ func matchCelExpressions(reader io.Reader, httpConfig config.HTTPProbe, logger * } if httpConfig.FailIfBodyJSONNotMatchesCel != nil { - result, details, err := httpConfig.FailIfBodyJSONNotMatchesCel.Eval(evalPayload) + result, details, err := httpConfig.FailIfBodyJSONNotMatchesCel.ContextEval(ctx, evalPayload) if err != nil { logger.Error("msg", "Error evaluating CEL expression", "err", err) return false @@ -608,7 +608,7 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr } if success && (httpConfig.FailIfBodyJSONMatchesCel != nil || httpConfig.FailIfBodyJSONNotMatchesCel != nil) { - success = matchCelExpressions(byteCounter, httpConfig, logger) + success = matchCelExpressions(ctx, byteCounter, httpConfig, logger) if success { probeFailedDueToCel.Set(0) } else { From d6df03c7bafc4836722c94bc3ebe8cf907dec2bb Mon Sep 17 00:00:00 2001 From: Juho Majasaari Date: Thu, 29 Aug 2024 11:30:38 +0300 Subject: [PATCH 3/3] Log Cel expressions on failures Signed-off-by: Juho Majasaari --- config/config.go | 8 ++++---- prober/http.go | 18 +++++++++--------- prober/http_test.go | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/config/config.go b/config/config.go index 89a98c9a3..9e76c13d3 100644 --- a/config/config.go +++ b/config/config.go @@ -149,14 +149,14 @@ func (sc *SafeConfig) ReloadConfig(confFile string, logger *slog.Logger) (err er // CelProgram encapsulates a cel.Program and makes it YAML marshalable. type CelProgram struct { cel.Program - expression string + Expression string } // NewCelProgram creates a new CEL Program and returns an error if the // passed-in CEL expression does not compile. func NewCelProgram(s string) (CelProgram, error) { program := CelProgram{ - expression: s, + Expression: s, } env, err := cel.NewEnv( @@ -199,8 +199,8 @@ func (c *CelProgram) UnmarshalYAML(unmarshal func(interface{}) error) error { // MarshalYAML implements the yaml.Marshaler interface. func (c CelProgram) MarshalYAML() (interface{}, error) { - if c.expression != "" { - return c.expression, nil + if c.Expression != "" { + return c.Expression, nil } return nil, nil } diff --git a/prober/http.go b/prober/http.go index c18f008ec..bbfdfb02c 100644 --- a/prober/http.go +++ b/prober/http.go @@ -66,16 +66,16 @@ func matchRegularExpressions(reader io.Reader, httpConfig config.HTTPProbe, logg return true } -func matchCelExpressions(reader io.Reader, httpConfig config.HTTPProbe, logger *slog.Logger) bool { +func matchCelExpressions(ctx context.Context, reader io.Reader, httpConfig config.HTTPProbe, logger *slog.Logger) bool { body, err := io.ReadAll(reader) if err != nil { - logger.Error("msg", "Error reading HTTP body", "err", err) + logger.Error("Error reading HTTP body", "err", err) return false } bodyJSON := make(map[string]interface{}) if err := json.Unmarshal(body, &bodyJSON); err != nil { - logger.Error("msg", "Error unmarshalling HTTP body", "err", err) + logger.Error("Error unmarshalling HTTP body", "err", err) return false } @@ -86,15 +86,15 @@ func matchCelExpressions(reader io.Reader, httpConfig config.HTTPProbe, logger * if httpConfig.FailIfBodyJSONMatchesCel != nil { result, details, err := httpConfig.FailIfBodyJSONMatchesCel.ContextEval(ctx, evalPayload) if err != nil { - logger.Error("msg", "Error evaluating CEL expression", "err", err) + logger.Error("Error evaluating CEL expression", "err", err) return false } if result.Type() != cel.BoolType { - logger.Error("msg", "CEL evaluation result is not a boolean", "details", details) + logger.Error("CEL evaluation result is not a boolean", "details", details) return false } if result.Type() == cel.BoolType && result.Value().(bool) { - logger.Error("msg", "Body matched CEL expression", "expression", httpConfig.FailIfBodyJSONMatchesCel) + logger.Error("Body matched CEL expression", "expression", httpConfig.FailIfBodyJSONMatchesCel.Expression) return false } } @@ -102,15 +102,15 @@ func matchCelExpressions(reader io.Reader, httpConfig config.HTTPProbe, logger * if httpConfig.FailIfBodyJSONNotMatchesCel != nil { result, details, err := httpConfig.FailIfBodyJSONNotMatchesCel.ContextEval(ctx, evalPayload) if err != nil { - logger.Error("msg", "Error evaluating CEL expression", "err", err) + logger.Error("Error evaluating CEL expression", "err", err) return false } if result.Type() != cel.BoolType { - logger.Error("msg", "CEL evaluation result is not a boolean", "details", details) + logger.Error("CEL evaluation result is not a boolean", "details", details) return false } if result.Type() == cel.BoolType && !result.Value().(bool) { - logger.Error("msg", "Body did not match CEL expression", "expression", httpConfig.FailIfBodyJSONNotMatchesCel) + logger.Error("Body did not match CEL expression", "expression", httpConfig.FailIfBodyJSONNotMatchesCel.Expression) return false } } diff --git a/prober/http_test.go b/prober/http_test.go index 562043c6f..3e84edb82 100644 --- a/prober/http_test.go +++ b/prober/http_test.go @@ -980,7 +980,7 @@ func TestFailIfBodyMatchesCel(t *testing.T) { registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyJSONMatchesCel: &testcase.cel}}, registry, log.NewNopLogger()) + result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyJSONMatchesCel: &testcase.cel}}, registry, promslog.NewNopLogger()) if testcase.expectedResult && !result { t.Fatalf("CEL test failed unexpectedly, got %s", recorder.Body.String()) } else if !testcase.expectedResult && result { @@ -1050,7 +1050,7 @@ func TestFailIfBodyNotMatchesCel(t *testing.T) { registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyJSONNotMatchesCel: &testcase.cel}}, registry, log.NewNopLogger()) + result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyJSONNotMatchesCel: &testcase.cel}}, registry, promslog.NewNopLogger()) if testcase.expectedResult && !result { t.Fatalf("CEL test failed unexpectedly, got %s", recorder.Body.String()) } else if !testcase.expectedResult && result {