From: Michael Stapelberg Date: Tue, 24 Nov 2015 17:39:41 +0000 (+0100) Subject: http: implement FailIfRegexp and FailIfNotRegexp X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=c80d6fbc06f042a348006e65a36e0975812f4cf6;p=blackbox_exporter.git http: implement FailIfRegexp and FailIfNotRegexp fixes #12 --- diff --git a/AUTHORS.md b/AUTHORS.md index a50e8e7..0020a2f 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -9,4 +9,5 @@ The following individuals have contributed code to this repository (listed in alphabetical order): * Brian Brazil +* Michael Stapelberg diff --git a/README.md b/README.md index 7df08fb..4157bce 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,10 @@ modules: no_follow_redirects: false fail_if_ssl: false fail_if_not_ssl: false + fail_if_matches_regexp: + - "Could not connect to database" + fail_if_not_matches_regexp: + - "Download the latest version here" tcp_connect: prober: tcp timeout: 5s diff --git a/http.go b/http.go index 52ccfd4..a4ee69f 100644 --- a/http.go +++ b/http.go @@ -4,13 +4,45 @@ import ( "crypto/tls" "errors" "fmt" + "io" + "io/ioutil" "net/http" + "regexp" "strings" "time" "github.com/prometheus/log" ) +func matchRegularExpressions(reader io.Reader, config HTTPProbe) bool { + body, err := ioutil.ReadAll(reader) + if err != nil { + log.Errorf("Error reading HTTP body: %s", err) + return false + } + for _, expression := range config.FailIfMatchesRegexp { + re, err := regexp.Compile(expression) + if err != nil { + log.Errorf("Could not compile expression %q as regular expression: %s", expression, err) + return false + } + if re.Match(body) { + return false + } + } + for _, expression := range config.FailIfNotMatchesRegexp { + re, err := regexp.Compile(expression) + if err != nil { + log.Errorf("Could not compile expression %q as regular expression: %s", expression, err) + return false + } + if !re.Match(body) { + return false + } + } + return true +} + func getEarliestCertExpiry(state *tls.ConnectionState) time.Time { earliest := time.Time{} for _, cert := range state.PeerCertificates { @@ -67,6 +99,10 @@ func probeHTTP(target string, w http.ResponseWriter, module Module) (success boo } else if 200 <= resp.StatusCode && resp.StatusCode < 300 { success = true } + + if success && (len(config.FailIfMatchesRegexp) > 0 || len(config.FailIfNotMatchesRegexp) > 0) { + success = matchRegularExpressions(resp.Body, config) + } } if resp == nil { diff --git a/http_test.go b/http_test.go index 0302e01..ce1b21f 100644 --- a/http_test.go +++ b/http_test.go @@ -14,6 +14,7 @@ package main import ( + "fmt" "net/http" "net/http/httptest" "strings" @@ -125,3 +126,115 @@ func TestFailIfNotSSL(t *testing.T) { t.Fatalf("Expected HTTP without SSL, got %s", body) } } + +func TestFailIfMatchesRegexp(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Bad news: could not connect to database server") + })) + defer ts.Close() + + recorder := httptest.NewRecorder() + result := probeHTTP(ts.URL, recorder, + Module{Timeout: time.Second, HTTP: HTTPProbe{FailIfMatchesRegexp: []string{"could not connect to database"}}}) + body := recorder.Body.String() + if result { + t.Fatalf("Regexp test succeeded unexpectedly, got %s", body) + } + + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Download the latest version here") + })) + defer ts.Close() + + recorder = httptest.NewRecorder() + result = probeHTTP(ts.URL, recorder, + Module{Timeout: time.Second, HTTP: HTTPProbe{FailIfMatchesRegexp: []string{"could not connect to database"}}}) + body = recorder.Body.String() + if !result { + t.Fatalf("Regexp test failed unexpectedly, got %s", body) + } + + // With multiple regexps configured, verify that any matching regexp causes + // the probe to fail, but probes succeed when no regexp matches. + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "internal error") + })) + defer ts.Close() + + recorder = httptest.NewRecorder() + result = probeHTTP(ts.URL, recorder, + Module{Timeout: time.Second, HTTP: HTTPProbe{FailIfMatchesRegexp: []string{"could not connect to database", "internal error"}}}) + body = recorder.Body.String() + if result { + t.Fatalf("Regexp test succeeded unexpectedly, got %s", body) + } + + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "hello world") + })) + defer ts.Close() + + recorder = httptest.NewRecorder() + result = probeHTTP(ts.URL, recorder, + Module{Timeout: time.Second, HTTP: HTTPProbe{FailIfMatchesRegexp: []string{"could not connect to database", "internal error"}}}) + body = recorder.Body.String() + if !result { + t.Fatalf("Regexp test failed unexpectedly, got %s", body) + } +} + +func TestFailIfNotMatchesRegexp(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Bad news: could not connect to database server") + })) + defer ts.Close() + + recorder := httptest.NewRecorder() + result := probeHTTP(ts.URL, recorder, + Module{Timeout: time.Second, HTTP: HTTPProbe{FailIfNotMatchesRegexp: []string{"Download the latest version here"}}}) + body := recorder.Body.String() + if result { + t.Fatalf("Regexp test succeeded unexpectedly, got %s", body) + } + + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Download the latest version here") + })) + defer ts.Close() + + recorder = httptest.NewRecorder() + result = probeHTTP(ts.URL, recorder, + Module{Timeout: time.Second, HTTP: HTTPProbe{FailIfNotMatchesRegexp: []string{"Download the latest version here"}}}) + body = recorder.Body.String() + if !result { + t.Fatalf("Regexp test failed unexpectedly, got %s", body) + } + + // With multiple regexps configured, verify that any non-matching regexp + // causes the probe to fail, but probes succeed when all regexps match. + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Download the latest version here") + })) + defer ts.Close() + + recorder = httptest.NewRecorder() + result = probeHTTP(ts.URL, recorder, + Module{Timeout: time.Second, HTTP: HTTPProbe{FailIfNotMatchesRegexp: []string{"Download the latest version here", "Copyright 2015"}}}) + body = recorder.Body.String() + if result { + t.Fatalf("Regexp test succeeded unexpectedly, got %s", body) + } + + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Download the latest version here. Copyright 2015 Test Inc.") + })) + defer ts.Close() + + recorder = httptest.NewRecorder() + result = probeHTTP(ts.URL, recorder, + Module{Timeout: time.Second, HTTP: HTTPProbe{FailIfNotMatchesRegexp: []string{"Download the latest version here", "Copyright 2015"}}}) + body = recorder.Body.String() + if !result { + t.Fatalf("Regexp test failed unexpectedly, got %s", body) + } +} diff --git a/main.go b/main.go index d3b4e32..f31a539 100644 --- a/main.go +++ b/main.go @@ -3,11 +3,12 @@ package main import ( "flag" "fmt" - "gopkg.in/yaml.v2" "io/ioutil" "net/http" "time" + "gopkg.in/yaml.v2" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/log" ) @@ -29,11 +30,13 @@ type Module struct { type HTTPProbe struct { // Defaults to 2xx. - ValidStatusCodes []int `yaml:"valid_status_codes"` - NoFollowRedirects bool `yaml:"no_follow_redirects"` - FailIfSSL bool `yaml:"fail_if_ssl"` - FailIfNotSSL bool `yaml:"fail_if_not_ssl"` - Method string `yaml:"method"` + ValidStatusCodes []int `yaml:"valid_status_codes"` + NoFollowRedirects bool `yaml:"no_follow_redirects"` + FailIfSSL bool `yaml:"fail_if_ssl"` + FailIfNotSSL bool `yaml:"fail_if_not_ssl"` + Method string `yaml:"method"` + FailIfMatchesRegexp []string `yaml:"fail_if_matches_regexp"` + FailIfNotMatchesRegexp []string `yaml:"fail_if_not_matches_regexp"` } type TCPProbe struct {