http_response: match on status code (#8032)

This commit is contained in:
Labesse Kévin 2020-10-16 19:40:17 +02:00 committed by GitHub
parent 78cf0b7ea6
commit 527a11a656
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 187 additions and 17 deletions

View File

@ -51,6 +51,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"
@ -83,6 +89,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))
@ -93,14 +100,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:

View File

@ -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"`
@ -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"
@ -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
@ -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)
}

View File

@ -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
}
@ -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)
}