diff --git a/plugins/inputs/http_response/README.md b/plugins/inputs/http_response/README.md index 67d0dc067..4e01bc0bb 100644 --- a/plugins/inputs/http_response/README.md +++ b/plugins/inputs/http_response/README.md @@ -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: diff --git a/plugins/inputs/http_response/http_response.go b/plugins/inputs/http_response/http_response.go index 33888503b..bd3078e49 100644 --- a/plugins/inputs/http_response/http_response.go +++ b/plugins/inputs/http_response/http_response.go @@ -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) } diff --git a/plugins/inputs/http_response/http_response_test.go b/plugins/inputs/http_response/http_response_test.go index 5a256e6e5..adf4e7999 100644 --- a/plugins/inputs/http_response/http_response_test.go +++ b/plugins/inputs/http_response/http_response_test.go @@ -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) +}