Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http_response: status_code match #8032

Merged
merged 3 commits into from
Oct 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions plugins/inputs/http_response/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ This input plugin checks HTTP/HTTPS connections.
# response_string_match = "ok"
# response_string_match = "\".*_status\".?:.?\"up\""

## Expected response status code.
## The status code of the response is compared to this value. If they match, the field
## "response_status_code_match" will be 1, otherwise it will be 0. If the
## expected status code is 0, the check is disabled and the field won't be added.
# response_status_code = 0

## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
Expand Down Expand Up @@ -77,6 +83,7 @@ This input plugin checks HTTP/HTTPS connections.
- response_time (float, seconds)
- content_length (int, response body length)
- response_string_match (int, 0 = mismatch / body read error, 1 = match)
- response_status_code_match (int, 0 = mismatch, 1 = match)
- http_response_code (int, response status code)
- result_type (string, deprecated in 1.6: use `result` tag and `result_code` field)
- result_code (int, [see below](#result--result_code))
Expand All @@ -87,14 +94,15 @@ Upon finishing polling the target server, the plugin registers the result of the

This tag is used to expose network and plugin errors. HTTP errors are considered a successful connection.

|Tag value |Corresponding field value|Description|
--------------------------|-------------------------|-----------|
|success | 0 |The HTTP request completed, even if the HTTP code represents an error|
|response_string_mismatch | 1 |The option `response_string_match` was used, and the body of the response didn't match the regex. HTTP errors with content in their body (like 4xx, 5xx) will trigger this error|
|body_read_error | 2 |The option `response_string_match` was used, but the plugin wasn't able to read the body of the response. Responses with empty bodies (like 3xx, HEAD, etc) will trigger this error. Or the option `response_body_field` was used and the content of the response body was not a valid utf-8. Or the size of the body of the response exceeded the `response_body_max_size` |
|connection_failed | 3 |Catch all for any network error not specifically handled by the plugin|
|timeout | 4 |The plugin timed out while awaiting the HTTP connection to complete|
|dns_error | 5 |There was a DNS error while attempting to connect to the host|
|Tag value |Corresponding field value|Description|
-------------------------------|-------------------------|-----------|
|success | 0 |The HTTP request completed, even if the HTTP code represents an error|
|response_string_mismatch | 1 |The option `response_string_match` was used, and the body of the response didn't match the regex. HTTP errors with content in their body (like 4xx, 5xx) will trigger this error|
|body_read_error | 2 |The option `response_string_match` was used, but the plugin wasn't able to read the body of the response. Responses with empty bodies (like 3xx, HEAD, etc) will trigger this error. Or the option `response_body_field` was used and the content of the response body was not a valid utf-8. Or the size of the body of the response exceeded the `response_body_max_size` |
|connection_failed | 3 |Catch all for any network error not specifically handled by the plugin|
|timeout | 4 |The plugin timed out while awaiting the HTTP connection to complete|
|dns_error | 5 |There was a DNS error while attempting to connect to the host|
|response_status_code_mismatch | 6 |The option `response_status_code_match` was used, and the status code of the response didn't match the value.|


### Example Output:
Expand Down
41 changes: 32 additions & 9 deletions plugins/inputs/http_response/http_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type HTTPResponse struct {
ResponseBodyField string `toml:"response_body_field"`
ResponseBodyMaxSize internal.Size `toml:"response_body_max_size"`
ResponseStringMatch string
ResponseStatusCode int
Interface string
// HTTP Basic Auth Credentials
Username string `toml:"username"`
Expand Down Expand Up @@ -106,6 +107,12 @@ var sampleConfig = `
# response_string_match = "ok"
# response_string_match = "\".*_status\".?:.?\"up\""

## Expected response status code.
## The status code of the response is compared to this value. If they match, the field
## "response_status_code_match" will be 1, otherwise it will be 0. If the
## expected status code is 0, the check is disabled and the field won't be added.
# response_status_code = 0

## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
Expand Down Expand Up @@ -208,12 +215,13 @@ func localAddress(interfaceName string) (net.Addr, error) {

func setResult(result_string string, fields map[string]interface{}, tags map[string]string) {
result_codes := map[string]int{
"success": 0,
"response_string_mismatch": 1,
"body_read_error": 2,
"connection_failed": 3,
"timeout": 4,
"dns_error": 5,
"success": 0,
"response_string_mismatch": 1,
"body_read_error": 2,
"connection_failed": 3,
"timeout": 4,
"dns_error": 5,
"response_status_code_mismatch": 6,
}

tags["result"] = result_string
Expand Down Expand Up @@ -352,16 +360,31 @@ func (h *HTTPResponse) httpGather(u string) (map[string]interface{}, map[string]
}
fields["content_length"] = len(bodyBytes)

// Check the response for a regex match.
var success = true

// Check the response for a regex
if h.ResponseStringMatch != "" {
if h.compiledStringMatch.Match(bodyBytes) {
setResult("success", fields, tags)
fields["response_string_match"] = 1
} else {
success = false
setResult("response_string_mismatch", fields, tags)
fields["response_string_match"] = 0
}
} else {
}

// Check the response status code
if h.ResponseStatusCode > 0 {
if resp.StatusCode == h.ResponseStatusCode {
fields["response_status_code_match"] = 1
} else {
success = false
setResult("response_status_code_mismatch", fields, tags)
fields["response_status_code_match"] = 0
}
}

if success {
setResult("success", fields, tags)
}

Expand Down
139 changes: 139 additions & 0 deletions plugins/inputs/http_response/http_response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ func setUpTestMux() http.Handler {
time.Sleep(time.Second * 2)
return
})
mux.HandleFunc("/nocontent", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
return mux
}

Expand Down Expand Up @@ -1110,3 +1113,139 @@ func TestBasicAuth(t *testing.T) {
absentFields := []string{"response_string_match"}
checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil)
}

func TestStatusCodeMatchFail(t *testing.T) {
mux := setUpTestMux()
ts := httptest.NewServer(mux)
defer ts.Close()

h := &HTTPResponse{
Log: testutil.Logger{},
Address: ts.URL + "/nocontent",
ResponseStatusCode: http.StatusOK,
ResponseTimeout: internal.Duration{Duration: time.Second * 20},
}

var acc testutil.Accumulator
err := h.Gather(&acc)
require.NoError(t, err)

expectedFields := map[string]interface{}{
"http_response_code": http.StatusNoContent,
"response_status_code_match": 0,
"result_type": "response_status_code_mismatch",
"result_code": 6,
"response_time": nil,
"content_length": nil,
}
expectedTags := map[string]interface{}{
"server": nil,
"method": http.MethodGet,
"status_code": "204",
"result": "response_status_code_mismatch",
}
checkOutput(t, &acc, expectedFields, expectedTags, nil, nil)
}

func TestStatusCodeMatch(t *testing.T) {
mux := setUpTestMux()
ts := httptest.NewServer(mux)
defer ts.Close()

h := &HTTPResponse{
Log: testutil.Logger{},
Address: ts.URL + "/nocontent",
ResponseStatusCode: http.StatusNoContent,
ResponseTimeout: internal.Duration{Duration: time.Second * 20},
}

var acc testutil.Accumulator
err := h.Gather(&acc)
require.NoError(t, err)

expectedFields := map[string]interface{}{
"http_response_code": http.StatusNoContent,
"response_status_code_match": 1,
"result_type": "success",
"result_code": 0,
"response_time": nil,
"content_length": nil,
}
expectedTags := map[string]interface{}{
"server": nil,
"method": http.MethodGet,
"status_code": "204",
"result": "success",
}
checkOutput(t, &acc, expectedFields, expectedTags, nil, nil)
}

func TestStatusCodeAndStringMatch(t *testing.T) {
mux := setUpTestMux()
ts := httptest.NewServer(mux)
defer ts.Close()

h := &HTTPResponse{
Log: testutil.Logger{},
Address: ts.URL + "/good",
ResponseStatusCode: http.StatusOK,
ResponseStringMatch: "hit the good page",
ResponseTimeout: internal.Duration{Duration: time.Second * 20},
}

var acc testutil.Accumulator
err := h.Gather(&acc)
require.NoError(t, err)

expectedFields := map[string]interface{}{
"http_response_code": http.StatusOK,
"response_status_code_match": 1,
"response_string_match": 1,
"result_type": "success",
"result_code": 0,
"response_time": nil,
"content_length": nil,
}
expectedTags := map[string]interface{}{
"server": nil,
"method": http.MethodGet,
"status_code": "200",
"result": "success",
}
checkOutput(t, &acc, expectedFields, expectedTags, nil, nil)
}

func TestStatusCodeAndStringMatchFail(t *testing.T) {
mux := setUpTestMux()
ts := httptest.NewServer(mux)
defer ts.Close()

h := &HTTPResponse{
Log: testutil.Logger{},
Address: ts.URL + "/nocontent",
ResponseStatusCode: http.StatusOK,
ResponseStringMatch: "hit the good page",
ResponseTimeout: internal.Duration{Duration: time.Second * 20},
}

var acc testutil.Accumulator
err := h.Gather(&acc)
require.NoError(t, err)

expectedFields := map[string]interface{}{
"http_response_code": http.StatusNoContent,
"response_status_code_match": 0,
"response_string_match": 0,
"result_type": "response_status_code_mismatch",
"result_code": 6,
"response_time": nil,
"content_length": nil,
}
expectedTags := map[string]interface{}{
"server": nil,
"method": http.MethodGet,
"status_code": "204",
"result": "response_status_code_mismatch",
}
checkOutput(t, &acc, expectedFields, expectedTags, nil, nil)
}