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 2 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
21 changes: 13 additions & 8 deletions plugins/inputs/http_response/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ This input plugin checks HTTP/HTTPS connections.
# response_string_match = "ok"
# response_string_match = "\".*_status\".?:.?\"up\""

## Optional status code match of the response
# response_status_code_match = 204
eraac marked this conversation as resolved.
Show resolved Hide resolved

## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
Expand Down Expand Up @@ -77,6 +80,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 +91,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
37 changes: 25 additions & 12 deletions plugins/inputs/http_response/http_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ type HTTPResponse struct {
Headers map[string]string
FollowRedirects bool
// Absolute path to file with Bearer token
BearerToken string `toml:"bearer_token"`
ResponseBodyField string `toml:"response_body_field"`
ResponseBodyMaxSize internal.Size `toml:"response_body_max_size"`
ResponseStringMatch string
Interface string
BearerToken string `toml:"bearer_token"`
ResponseBodyField string `toml:"response_body_field"`
ResponseBodyMaxSize internal.Size `toml:"response_body_max_size"`
ResponseStringMatch string
ResponseStatusCodeMatch int
Interface string
// HTTP Basic Auth Credentials
Username string `toml:"username"`
Password string `toml:"password"`
Expand Down Expand Up @@ -106,6 +107,9 @@ var sampleConfig = `
# response_string_match = "ok"
# response_string_match = "\".*_status\".?:.?\"up\""

## Optional status code match of the response
# response_status_code_match = 204
eraac marked this conversation as resolved.
Show resolved Hide resolved

## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
Expand Down Expand Up @@ -208,12 +212,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,7 +357,7 @@ func (h *HTTPResponse) httpGather(u string) (map[string]interface{}, map[string]
}
fields["content_length"] = len(bodyBytes)

// Check the response for a regex match.
// Check the response for a regex or status code match.
if h.ResponseStringMatch != "" {
if h.compiledStringMatch.Match(bodyBytes) {
setResult("success", fields, tags)
Expand All @@ -361,6 +366,14 @@ func (h *HTTPResponse) httpGather(u string) (map[string]interface{}, map[string]
setResult("response_string_mismatch", fields, tags)
fields["response_string_match"] = 0
}
} else if h.ResponseStatusCodeMatch > 0 {
eraac marked this conversation as resolved.
Show resolved Hide resolved
if resp.StatusCode == h.ResponseStatusCodeMatch {
setResult("success", fields, tags)
fields["response_status_code_match"] = 1
} else {
setResult("response_status_code_mismatch", fields, tags)
fields["response_status_code_match"] = 0
}
} else {
setResult("success", fields, tags)
}
Expand Down
69 changes: 69 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,69 @@ 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",
ResponseStatusCodeMatch: 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",
ResponseStatusCodeMatch: 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)
}