type HTTPProbe struct {
// Defaults to 2xx.
- ValidStatusCodes []int `yaml:"valid_status_codes,omitempty"`
- ValidHTTPVersions []string `yaml:"valid_http_versions,omitempty"`
- IPProtocol string `yaml:"preferred_ip_protocol,omitempty"`
- IPProtocolFallback bool `yaml:"ip_protocol_fallback,omitempty"`
- NoFollowRedirects bool `yaml:"no_follow_redirects,omitempty"`
- FailIfSSL bool `yaml:"fail_if_ssl,omitempty"`
- FailIfNotSSL bool `yaml:"fail_if_not_ssl,omitempty"`
- Method string `yaml:"method,omitempty"`
- Headers map[string]string `yaml:"headers,omitempty"`
- FailIfMatchesRegexp []string `yaml:"fail_if_matches_regexp,omitempty"`
- FailIfNotMatchesRegexp []string `yaml:"fail_if_not_matches_regexp,omitempty"`
- Body string `yaml:"body,omitempty"`
- HTTPClientConfig config.HTTPClientConfig `yaml:"http_client_config,inline"`
+ ValidStatusCodes []int `yaml:"valid_status_codes,omitempty"`
+ ValidHTTPVersions []string `yaml:"valid_http_versions,omitempty"`
+ IPProtocol string `yaml:"preferred_ip_protocol,omitempty"`
+ IPProtocolFallback bool `yaml:"ip_protocol_fallback,omitempty"`
+ NoFollowRedirects bool `yaml:"no_follow_redirects,omitempty"`
+ FailIfSSL bool `yaml:"fail_if_ssl,omitempty"`
+ FailIfNotSSL bool `yaml:"fail_if_not_ssl,omitempty"`
+ Method string `yaml:"method,omitempty"`
+ Headers map[string]string `yaml:"headers,omitempty"`
+ FailIfBodyMatchesRegexp []string `yaml:"fail_if_body_matches_regexp,omitempty"`
+ FailIfBodyNotMatchesRegexp []string `yaml:"fail_if_body_not_matches_regexp,omitempty"`
+ FailIfHeaderMatchesRegexp []HeaderMatch `yaml:"fail_if_header_matches,omitempty"`
+ FailIfHeaderNotMatchesRegexp []HeaderMatch `yaml:"fail_if_header_not_matches,omitempty"`
+ Body string `yaml:"body,omitempty"`
+ HTTPClientConfig config.HTTPClientConfig `yaml:"http_client_config,inline"`
+}
+
+type HeaderMatch struct {
+ Header string `yaml:"header,omitempty"`
+ Regexp string `yaml:"regexp,omitempty"`
+ AllowMissing bool `yaml:"allow_missing,omitempty"`
}
type QueryResponse struct {
}
return nil
}
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface.
+func (s *HeaderMatch) UnmarshalYAML(unmarshal func(interface{}) error) error {
+ type plain HeaderMatch
+ if err := unmarshal((*plain)(s)); err != nil {
+ return err
+ }
+
+ if s.Header == "" {
+ return errors.New("header name must be set for HTTP header matchers")
+ }
+
+ if s.Regexp == "" {
+ return errors.New("regexp must be set for HTTP header matchers")
+ }
+
+ return nil
+}
"net/http"
"net/http/cookiejar"
"net/http/httptrace"
+ "net/textproto"
"net/url"
"regexp"
"strconv"
level.Error(logger).Log("msg", "Error reading HTTP body", "err", err)
return false
}
- for _, expression := range httpConfig.FailIfMatchesRegexp {
+ for _, expression := range httpConfig.FailIfBodyMatchesRegexp {
re, err := regexp.Compile(expression)
if err != nil {
level.Error(logger).Log("msg", "Could not compile regular expression", "regexp", expression, "err", err)
return false
}
}
- for _, expression := range httpConfig.FailIfNotMatchesRegexp {
+ for _, expression := range httpConfig.FailIfBodyNotMatchesRegexp {
re, err := regexp.Compile(expression)
if err != nil {
level.Error(logger).Log("msg", "Could not compile regular expression", "regexp", expression, "err", err)
return true
}
+func matchRegularExpressionsOnHeaders(header http.Header, httpConfig config.HTTPProbe, logger log.Logger) bool {
+ for _, headerMatchSpec := range httpConfig.FailIfHeaderMatchesRegexp {
+ values := header[textproto.CanonicalMIMEHeaderKey(headerMatchSpec.Header)]
+ if len(values) == 0 {
+ if !headerMatchSpec.AllowMissing {
+ level.Error(logger).Log("msg", "Missing required header", "header", headerMatchSpec.Header)
+ return false
+ } else {
+ continue // No need to match any regex on missing headers.
+ }
+ }
+
+ re, err := regexp.Compile(headerMatchSpec.Regexp)
+ if err != nil {
+ level.Error(logger).Log("msg", "Could not compile regular expression", "regexp", headerMatchSpec.Regexp, "err", err)
+ return false
+ }
+
+ for _, val := range values {
+ if re.MatchString(val) {
+ level.Error(logger).Log("msg", "Header matched regular expression", "header", headerMatchSpec.Header,
+ "regexp", headerMatchSpec.Regexp, "value_count", len(values))
+ return false
+ }
+ }
+ }
+ for _, headerMatchSpec := range httpConfig.FailIfHeaderNotMatchesRegexp {
+ values := header[textproto.CanonicalMIMEHeaderKey(headerMatchSpec.Header)]
+ if len(values) == 0 {
+ if !headerMatchSpec.AllowMissing {
+ level.Error(logger).Log("msg", "Missing required header", "header", headerMatchSpec.Header)
+ return false
+ } else {
+ continue // No need to match any regex on missing headers.
+ }
+ }
+
+ re, err := regexp.Compile(headerMatchSpec.Regexp)
+ if err != nil {
+ level.Error(logger).Log("msg", "Could not compile regular expression", "regexp", headerMatchSpec.Regexp, "err", err)
+ return false
+ }
+
+ anyHeaderValueMatched := false
+
+ for _, val := range values {
+ if re.MatchString(val) {
+ anyHeaderValueMatched = true
+ break
+ }
+ }
+
+ if !anyHeaderValueMatched {
+ level.Error(logger).Log("msg", "Header did not match regular expression", "header", headerMatchSpec.Header,
+ "regexp", headerMatchSpec.Regexp, "value_count", len(values))
+ return false
+ }
+ }
+
+ return true
+}
+
// roundTripTrace holds timings for a single HTTP roundtrip.
type roundTripTrace struct {
tls bool
level.Info(logger).Log("msg", "Invalid HTTP response status code, wanted 2xx", "status_code", resp.StatusCode)
}
- if success && (len(httpConfig.FailIfMatchesRegexp) > 0 || len(httpConfig.FailIfNotMatchesRegexp) > 0) {
+ if success && (len(httpConfig.FailIfHeaderMatchesRegexp) > 0 || len(httpConfig.FailIfHeaderNotMatchesRegexp) > 0) {
+ success = matchRegularExpressionsOnHeaders(resp.Header, httpConfig, logger)
+ if success {
+ probeFailedDueToRegex.Set(0)
+ } else {
+ probeFailedDueToRegex.Set(1)
+ }
+ }
+
+ if success && (len(httpConfig.FailIfBodyMatchesRegexp) > 0 || len(httpConfig.FailIfBodyNotMatchesRegexp) > 0) {
success = matchRegularExpressions(resp.Body, httpConfig, logger)
if success {
probeFailedDueToRegex.Set(0)
checkRegistryResults(expectedResults, mfs, t)
}
-func TestFailIfMatchesRegexp(t *testing.T) {
+func TestFailIfBodyMatchesRegexp(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")
}))
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, FailIfMatchesRegexp: []string{"could not connect to database"}}}, registry, log.NewNopLogger())
+ config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyMatchesRegexp: []string{"could not connect to database"}}}, registry, log.NewNopLogger())
body := recorder.Body.String()
if result {
t.Fatalf("Regexp test succeeded unexpectedly, got %s", body)
recorder = httptest.NewRecorder()
registry = prometheus.NewRegistry()
result = ProbeHTTP(testCTX, ts.URL,
- config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfMatchesRegexp: []string{"could not connect to database"}}}, registry, log.NewNopLogger())
+ config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyMatchesRegexp: []string{"could not connect to database"}}}, registry, log.NewNopLogger())
body = recorder.Body.String()
if !result {
t.Fatalf("Regexp test failed unexpectedly, got %s", body)
recorder = httptest.NewRecorder()
registry = prometheus.NewRegistry()
result = ProbeHTTP(testCTX, ts.URL,
- config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfMatchesRegexp: []string{"could not connect to database", "internal error"}}}, registry, log.NewNopLogger())
+ config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyMatchesRegexp: []string{"could not connect to database", "internal error"}}}, registry, log.NewNopLogger())
body = recorder.Body.String()
if result {
t.Fatalf("Regexp test succeeded unexpectedly, got %s", body)
recorder = httptest.NewRecorder()
registry = prometheus.NewRegistry()
result = ProbeHTTP(testCTX, ts.URL,
- config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfMatchesRegexp: []string{"could not connect to database", "internal error"}}}, registry, log.NewNopLogger())
+ config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyMatchesRegexp: []string{"could not connect to database", "internal error"}}}, registry, log.NewNopLogger())
body = recorder.Body.String()
if !result {
t.Fatalf("Regexp test failed unexpectedly, got %s", body)
}
}
-func TestFailIfNotMatchesRegexp(t *testing.T) {
+func TestFailIfBodyNotMatchesRegexp(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")
}))
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, FailIfNotMatchesRegexp: []string{"Download the latest version here"}}}, registry, log.NewNopLogger())
+ config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyNotMatchesRegexp: []string{"Download the latest version here"}}}, registry, log.NewNopLogger())
body := recorder.Body.String()
if result {
t.Fatalf("Regexp test succeeded unexpectedly, got %s", body)
recorder = httptest.NewRecorder()
registry = prometheus.NewRegistry()
result = ProbeHTTP(testCTX, ts.URL,
- config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfNotMatchesRegexp: []string{"Download the latest version here"}}}, registry, log.NewNopLogger())
+ config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyNotMatchesRegexp: []string{"Download the latest version here"}}}, registry, log.NewNopLogger())
body = recorder.Body.String()
if !result {
t.Fatalf("Regexp test failed unexpectedly, got %s", body)
recorder = httptest.NewRecorder()
registry = prometheus.NewRegistry()
result = ProbeHTTP(testCTX, ts.URL,
- config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfNotMatchesRegexp: []string{"Download the latest version here", "Copyright 2015"}}}, registry, log.NewNopLogger())
+ config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyNotMatchesRegexp: []string{"Download the latest version here", "Copyright 2015"}}}, registry, log.NewNopLogger())
body = recorder.Body.String()
if result {
t.Fatalf("Regexp test succeeded unexpectedly, got %s", body)
recorder = httptest.NewRecorder()
registry = prometheus.NewRegistry()
result = ProbeHTTP(testCTX, ts.URL,
- config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfNotMatchesRegexp: []string{"Download the latest version here", "Copyright 2015"}}}, registry, log.NewNopLogger())
+ config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{IPProtocolFallback: true, FailIfBodyNotMatchesRegexp: []string{"Download the latest version here", "Copyright 2015"}}}, registry, log.NewNopLogger())
body = recorder.Body.String()
if !result {
t.Fatalf("Regexp test failed unexpectedly, got %s", body)
}
}
+func TestFailIfHeaderMatchesRegexp(t *testing.T) {
+ tests := []struct {
+ Rule config.HeaderMatch
+ Values []string
+ ShouldSucceed bool
+ }{
+ {config.HeaderMatch{"Content-Type", "text/javascript", false}, []string{"text/javascript"}, false},
+ {config.HeaderMatch{"Content-Type", "text/javascript", false}, []string{"application/octet-stream"}, true},
+ {config.HeaderMatch{"content-type", "text/javascript", false}, []string{"application/octet-stream"}, true},
+ {config.HeaderMatch{"Content-Type", ".*", false}, []string{""}, false},
+ {config.HeaderMatch{"Content-Type", ".*", false}, []string{}, false},
+ {config.HeaderMatch{"Content-Type", ".*", true}, []string{""}, false},
+ {config.HeaderMatch{"Content-Type", ".*", true}, []string{}, true},
+ {config.HeaderMatch{"Set-Cookie", ".*Domain=\\.example\\.com.*", false}, []string{"gid=1; Expires=Tue, 19-Mar-2019 20:08:29 GMT; Domain=.example.com; Path=/"}, false},
+ {config.HeaderMatch{"Set-Cookie", ".*Domain=\\.example\\.com.*", false}, []string{"zz=4; expires=Mon, 01-Jan-1990 00:00:00 GMT; Domain=www.example.com; Path=/", "gid=1; Expires=Tue, 19-Mar-2019 20:08:29 GMT; Domain=.example.com; Path=/"}, false},
+ }
+
+ for i, test := range tests {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ for _, val := range test.Values {
+ w.Header().Add(test.Rule.Header, val)
+ }
+ }))
+ defer ts.Close()
+ 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, FailIfHeaderMatchesRegexp: []config.HeaderMatch{test.Rule}}}, registry, log.NewNopLogger())
+ if result != test.ShouldSucceed {
+ t.Fatalf("Test %d had unexpected result: succeeded: %t, expected: %+v", i, result, test)
+ }
+
+ mfs, err := registry.Gather()
+ if err != nil {
+ t.Fatal(err)
+ }
+ expectedResults := map[string]float64{
+ "probe_failed_due_to_regex": 1,
+ }
+
+ if test.ShouldSucceed {
+ expectedResults["probe_failed_due_to_regex"] = 0
+ }
+
+ checkRegistryResults(expectedResults, mfs, t)
+ }
+}
+
+func TestFailIfHeaderNotMatchesRegexp(t *testing.T) {
+ tests := []struct {
+ Rule config.HeaderMatch
+ Values []string
+ ShouldSucceed bool
+ }{
+ {config.HeaderMatch{"Content-Type", "text/javascript", false}, []string{"text/javascript"}, true},
+ {config.HeaderMatch{"content-type", "text/javascript", false}, []string{"text/javascript"}, true},
+ {config.HeaderMatch{"Content-Type", "text/javascript", false}, []string{"application/octet-stream"}, false},
+ {config.HeaderMatch{"Content-Type", ".*", false}, []string{""}, true},
+ {config.HeaderMatch{"Content-Type", ".*", false}, []string{}, false},
+ {config.HeaderMatch{"Content-Type", ".*", true}, []string{}, true},
+ {config.HeaderMatch{"Set-Cookie", ".*Domain=\\.example\\.com.*", false}, []string{"zz=4; expires=Mon, 01-Jan-1990 00:00:00 GMT; Domain=www.example.com; Path=/"}, false},
+ {config.HeaderMatch{"Set-Cookie", ".*Domain=\\.example\\.com.*", false}, []string{"zz=4; expires=Mon, 01-Jan-1990 00:00:00 GMT; Domain=www.example.com; Path=/", "gid=1; Expires=Tue, 19-Mar-2019 20:08:29 GMT; Domain=.example.com; Path=/"}, true},
+ }
+
+ for i, test := range tests {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ for _, val := range test.Values {
+ w.Header().Add(test.Rule.Header, val)
+ }
+ }))
+ defer ts.Close()
+ 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, FailIfHeaderNotMatchesRegexp: []config.HeaderMatch{test.Rule}}}, registry, log.NewNopLogger())
+ if result != test.ShouldSucceed {
+ t.Fatalf("Test %d had unexpected result: succeeded: %t, expected: %+v", i, result, test)
+ }
+
+ mfs, err := registry.Gather()
+ if err != nil {
+ t.Fatal(err)
+ }
+ expectedResults := map[string]float64{
+ "probe_failed_due_to_regex": 1,
+ }
+
+ if test.ShouldSucceed {
+ expectedResults["probe_failed_due_to_regex"] = 0
+ }
+
+ checkRegistryResults(expectedResults, mfs, t)
+ }
+}
+
func TestHTTPHeaders(t *testing.T) {
headers := map[string]string{
"Host": "my-secret-vhost.com",