From 9003efc3fa756cb3550e76b9ec494cae21c00a15 Mon Sep 17 00:00:00 2001 From: Len Smith Date: Wed, 1 Feb 2017 09:21:08 -0500 Subject: [PATCH] http_response : Add in support for looking for substring in response (#2204) * Add in support for looking for substring in response * Add note to CHANGELOG.md * Switch from substring match to regex match * Requested code changes * Make requested changes and refactor to avoid nested if-else. * Convert tabs to space and compile regex once --- CHANGELOG.md | 1 + plugins/inputs/http_response/README.md | 5 ++ plugins/inputs/http_response/http_response.go | 51 +++++++++-- .../http_response/http_response_test.go | 84 +++++++++++++++++++ 4 files changed, 135 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5131f50eb..e123e33a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ plugins, not just statsd. ### Features +- [#2204](https://github.com/influxdata/telegraf/pull/2204): Extend http_response to support searching for a substring in response. Return 1 if found, else 0. - [#2123](https://github.com/influxdata/telegraf/pull/2123): Fix improper calculation of CPU percentages - [#1564](https://github.com/influxdata/telegraf/issues/1564): Use RFC3339 timestamps in log output. - [#1997](https://github.com/influxdata/telegraf/issues/1997): Non-default HTTP timeouts for RabbitMQ plugin. diff --git a/plugins/inputs/http_response/README.md b/plugins/inputs/http_response/README.md index ec873ad2b..01dd09536 100644 --- a/plugins/inputs/http_response/README.md +++ b/plugins/inputs/http_response/README.md @@ -23,6 +23,11 @@ This input plugin will test HTTP/HTTPS connections. # {'fake':'data'} # ''' + ## Optional substring or regex match in body of the response + ## response_string_match = "\"service_status\": \"up\"" + ## response_string_match = "ok" + ## response_string_match = "\".*_status\".?:.?\"up\"" + ## Optional SSL Config # ssl_ca = "/etc/telegraf/ca.pem" # ssl_cert = "/etc/telegraf/cert.pem" diff --git a/plugins/inputs/http_response/http_response.go b/plugins/inputs/http_response/http_response.go index 34eadaa4f..111e35518 100644 --- a/plugins/inputs/http_response/http_response.go +++ b/plugins/inputs/http_response/http_response.go @@ -3,8 +3,11 @@ package http_response import ( "errors" "io" + "io/ioutil" + "log" "net/http" "net/url" + "regexp" "strings" "time" @@ -15,12 +18,14 @@ import ( // HTTPResponse struct type HTTPResponse struct { - Address string - Body string - Method string - ResponseTimeout internal.Duration - Headers map[string]string - FollowRedirects bool + Address string + Body string + Method string + ResponseTimeout internal.Duration + Headers map[string]string + FollowRedirects bool + ResponseStringMatch string + compiledStringMatch *regexp.Regexp // Path to CA file SSLCA string `toml:"ssl_ca"` @@ -54,6 +59,11 @@ var sampleConfig = ` # {'fake':'data'} # ''' + ## Optional substring or regex match in body of the response + ## response_string_match = "\"service_status\": \"up\"" + ## response_string_match = "ok" + ## response_string_match = "\".*_status\".?:.?\"up\"" + ## Optional SSL Config # ssl_ca = "/etc/telegraf/ca.pem" # ssl_cert = "/etc/telegraf/cert.pem" @@ -137,6 +147,35 @@ func (h *HTTPResponse) HTTPGather() (map[string]interface{}, error) { } fields["response_time"] = time.Since(start).Seconds() fields["http_response_code"] = resp.StatusCode + + // Check the response for a regex match. + if h.ResponseStringMatch != "" { + + // Compile once and reuse + if h.compiledStringMatch == nil { + h.compiledStringMatch = regexp.MustCompile(h.ResponseStringMatch) + if err != nil { + log.Printf("E! Failed to compile regular expression %s : %s", h.ResponseStringMatch, err) + fields["response_string_match"] = 0 + return fields, nil + } + } + + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Printf("E! Failed to read body of HTTP Response : %s", err) + fields["response_string_match"] = 0 + return fields, nil + } + + if h.compiledStringMatch.Match(bodyBytes) { + fields["response_string_match"] = 1 + } else { + fields["response_string_match"] = 0 + } + + } + return fields, nil } diff --git a/plugins/inputs/http_response/http_response_test.go b/plugins/inputs/http_response/http_response_test.go index f0d0040d6..236e5d88b 100644 --- a/plugins/inputs/http_response/http_response_test.go +++ b/plugins/inputs/http_response/http_response_test.go @@ -22,6 +22,9 @@ func setUpTestMux() http.Handler { mux.HandleFunc("/good", func(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "hit the good page!") }) + mux.HandleFunc("/jsonresponse", func(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "\"service_status\": \"up\", \"healthy\" : \"true\"") + }) mux.HandleFunc("/badredirect", func(w http.ResponseWriter, req *http.Request) { http.Redirect(w, req, "/badredirect", http.StatusMovedPermanently) }) @@ -236,6 +239,87 @@ func TestBody(t *testing.T) { } } +func TestStringMatch(t *testing.T) { + mux := setUpTestMux() + ts := httptest.NewServer(mux) + defer ts.Close() + + h := &HTTPResponse{ + Address: ts.URL + "/good", + Body: "{ 'test': 'data'}", + Method: "GET", + ResponseStringMatch: "hit the good page", + ResponseTimeout: internal.Duration{Duration: time.Second * 20}, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + FollowRedirects: true, + } + fields, err := h.HTTPGather() + require.NoError(t, err) + assert.NotEmpty(t, fields) + if assert.NotNil(t, fields["http_response_code"]) { + assert.Equal(t, http.StatusOK, fields["http_response_code"]) + } + assert.Equal(t, 1, fields["response_string_match"]) + assert.NotNil(t, fields["response_time"]) + +} + +func TestStringMatchJson(t *testing.T) { + mux := setUpTestMux() + ts := httptest.NewServer(mux) + defer ts.Close() + + h := &HTTPResponse{ + Address: ts.URL + "/jsonresponse", + Body: "{ 'test': 'data'}", + Method: "GET", + ResponseStringMatch: "\"service_status\": \"up\"", + ResponseTimeout: internal.Duration{Duration: time.Second * 20}, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + FollowRedirects: true, + } + fields, err := h.HTTPGather() + require.NoError(t, err) + assert.NotEmpty(t, fields) + if assert.NotNil(t, fields["http_response_code"]) { + assert.Equal(t, http.StatusOK, fields["http_response_code"]) + } + assert.Equal(t, 1, fields["response_string_match"]) + assert.NotNil(t, fields["response_time"]) + +} + +func TestStringMatchFail(t *testing.T) { + mux := setUpTestMux() + ts := httptest.NewServer(mux) + defer ts.Close() + + h := &HTTPResponse{ + Address: ts.URL + "/good", + Body: "{ 'test': 'data'}", + Method: "GET", + ResponseStringMatch: "hit the bad page", + ResponseTimeout: internal.Duration{Duration: time.Second * 20}, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + FollowRedirects: true, + } + fields, err := h.HTTPGather() + require.NoError(t, err) + assert.NotEmpty(t, fields) + if assert.NotNil(t, fields["http_response_code"]) { + assert.Equal(t, http.StatusOK, fields["http_response_code"]) + } + assert.Equal(t, 0, fields["response_string_match"]) + assert.NotNil(t, fields["response_time"]) + +} + func TestTimeout(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux)