http: implement FailIfRegexp and FailIfNotRegexp
authorMichael Stapelberg <stapelberg@google.com>
Tue, 24 Nov 2015 17:39:41 +0000 (18:39 +0100)
committerMichael Stapelberg <stapelberg@google.com>
Tue, 1 Dec 2015 07:48:48 +0000 (08:48 +0100)
fixes #12

AUTHORS.md
README.md
http.go
http_test.go
main.go

index a50e8e7db98893686a4ba358a0d4374032d3852f..0020a2fe7d4337be6450e4fbcbdb2016b96f94f0 100644 (file)
@@ -9,4 +9,5 @@ The following individuals have contributed code to this repository
 (listed in alphabetical order):
 
 * Brian Brazil <brian.brazil@robustperception.io>
+* Michael Stapelberg <stapelberg@google.com>
 
index 7df08fb8169fdd403fe596765dca656b183700c7..4157bce1ec2c2ae9bbbe8f08ede5cd0641235f9c 100644 (file)
--- 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 52ccfd4ef07dffbb0a27d59687989e8f3d1f9e7e..a4ee69ff90e3b4e81c70b7c15c98f3aa6c356a1a 100644 (file)
--- 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 {
index 0302e01f82a275a562009a8bc2976ac5887ce2c1..ce1b21f685ce2ba7ad9b2e8b205ce6efd3e9f5c9 100644 (file)
@@ -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 d3b4e32e48a6029e4319878a8c729b42c484bb04..f31a539e8dec958737f8e50bf29394b7838903ef 100644 (file)
--- 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 {