diff --git a/plugins/inputs/http_response/README.md b/plugins/inputs/http_response/README.md index 924d35384..3bee4fdfe 100644 --- a/plugins/inputs/http_response/README.md +++ b/plugins/inputs/http_response/README.md @@ -44,18 +44,38 @@ This input plugin will test HTTP/HTTPS connections. ### Measurements & Fields: - http_response - - response_time (float, seconds) - - http_response_code (int) #The code received - - result_type (string) # success, timeout, response_string_mismatch, connection_failed + - response_time (float, seconds) # Not set if target is unreachable for any reason + - http_response_code (int) # The HTTP code received + - result_type (string) # Legacy field mantained for backwards compatibility + - result_code (int) # Details [here](#result-tag-and-result_code-field) + ### Tags: - All measurements have the following tags: - - server - - method + - server # Server URL used + - method # HTTP method used (GET, POST, PUT, etc) + - status_code # String with the HTTP status code + - result # Details [here](#result-tag-and-result_code-field) + +### Result tag and Result_code field +Upon finishing polling the target server, the plugin registers the result of the operation in the `result` tag, and adds a numeric field called `result_code` corresponding with that tag value. + +This tag is used to expose network and plugin errors. HTTP errors are considered a sucessful connection by the plugin. + +|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| +|body_read_error | 2 |The option `response_string_match` was used, but the plugin wans't able to read the body of the response. Responses with empty bodies (like 3xx, HEAD, etc) will trigger this error| +|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| + +NOTE: The error codes are derived from the error object returned by the `net/http` Go library, so the accuracy of the errors depends on the handling of error states by the `net/http` Go library. **If a more detailed error report is required use the `log_network_errors` setting.** ### Example Output: ``` -http_response,method=GET,server=http://www.github.com http_response_code=200i,response_time=6.223266528 1459419354977857955 +http_response,method=GET,server=http://www.github.com,status_code="200",result="success" http_response_code=200i,response_time=6.223266528,result_type="sucess",result_code=0i 1459419354977857955 ``` diff --git a/plugins/inputs/http_response/http_response.go b/plugins/inputs/http_response/http_response.go index 77aaf367e..b0f1009fc 100644 --- a/plugins/inputs/http_response/http_response.go +++ b/plugins/inputs/http_response/http_response.go @@ -2,6 +2,7 @@ package http_response import ( "errors" + "fmt" "io" "io/ioutil" "log" @@ -9,6 +10,7 @@ import ( "net/http" "net/url" "regexp" + "strconv" "strings" "time" @@ -133,10 +135,54 @@ func (h *HTTPResponse) createHttpClient() (*http.Client, error) { return client, nil } +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, + } + + tags["result"] = result_string + fields["result_type"] = result_string + fields["result_code"] = result_codes[result_string] +} + +func setError(err error, fields map[string]interface{}, tags map[string]string) error { + if timeoutError, ok := err.(net.Error); ok && timeoutError.Timeout() { + setResult("timeout", fields, tags) + return timeoutError + } + + urlErr, isUrlErr := err.(*url.Error) + if !isUrlErr { + return nil + } + + opErr, isNetErr := (urlErr.Err).(*net.OpError) + if isNetErr { + switch e := (opErr.Err).(type) { + case (*net.DNSError): + setResult("dns_error", fields, tags) + return e + case (*net.ParseError): + // Parse error has to do with parsing of IP addresses, so we + // group it with address errors + setResult("address_error", fields, tags) + return e + } + } + + return nil +} + // HTTPGather gathers all fields and returns any errors it encounters -func (h *HTTPResponse) httpGather() (map[string]interface{}, error) { - // Prepare fields +func (h *HTTPResponse) httpGather() (map[string]interface{}, map[string]string, error) { + // Prepare fields and tags fields := make(map[string]interface{}) + tags := map[string]string{"server": h.Address, "method": h.Method} var body io.Reader if h.Body != "" { @@ -144,7 +190,7 @@ func (h *HTTPResponse) httpGather() (map[string]interface{}, error) { } request, err := http.NewRequest(h.Method, h.Address, body) if err != nil { - return nil, err + return nil, nil, err } for key, val := range h.Headers { @@ -157,68 +203,87 @@ func (h *HTTPResponse) httpGather() (map[string]interface{}, error) { // Start Timer start := time.Now() resp, err := h.client.Do(request) + response_time := time.Since(start).Seconds() + // If an error in returned, it means we are dealing with a network error, as + // HTTP error codes do not generate errors in the net/http library if err != nil { - if netErr, ok := err.(net.Error); ok && netErr.Timeout() { - fields["result_type"] = "timeout" - return fields, nil + // Log error + log.Printf("D! Network error while polling %s: %s", h.Address, err.Error()) + + // Get error details + netErr := setError(err, fields, tags) + + // If recognize the returnded error, get out + if netErr != nil { + return fields, tags, nil } - fields["result_type"] = "connection_failed" - if h.FollowRedirects { - return fields, nil - } - if urlError, ok := err.(*url.Error); ok && - urlError.Err == ErrRedirectAttempted { + + // Any error not recognized by `set_error` is considered a "connection_failed" + setResult("connection_failed", fields, tags) + + // If the error is a redirect we continue processing and log the HTTP code + urlError, isUrlError := err.(*url.Error) + if !h.FollowRedirects && isUrlError && urlError.Err == ErrRedirectAttempted { err = nil } else { - return fields, nil + // If the error isn't a timeout or a redirect stop + // processing the request + return fields, tags, nil } } + + if _, ok := fields["response_time"]; !ok { + fields["response_time"] = response_time + } + + // This function closes the response body, as + // required by the net/http library defer func() { io.Copy(ioutil.Discard, resp.Body) resp.Body.Close() }() - fields["response_time"] = time.Since(start).Seconds() + // Set log the HTTP response code + tags["status_code"] = strconv.Itoa(resp.StatusCode) 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["result_type"] = "response_string_mismatch" - 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["result_type"] = "response_string_mismatch" + log.Printf("D! Failed to read body of HTTP Response : %s", err) + setResult("body_read_error", fields, tags) fields["response_string_match"] = 0 - return fields, nil + return fields, tags, nil } if h.compiledStringMatch.Match(bodyBytes) { - fields["result_type"] = "success" + setResult("success", fields, tags) fields["response_string_match"] = 1 } else { - fields["result_type"] = "response_string_mismatch" + setResult("response_string_mismatch", fields, tags) fields["response_string_match"] = 0 } } else { - fields["result_type"] = "success" + setResult("success", fields, tags) } - return fields, nil + return fields, tags, nil } // Gather gets all metric fields and tags and returns any errors it encounters func (h *HTTPResponse) Gather(acc telegraf.Accumulator) error { + // Compile the body regex if it exist + if h.compiledStringMatch == nil { + var err error + h.compiledStringMatch, err = regexp.Compile(h.ResponseStringMatch) + if err != nil { + return fmt.Errorf("Failed to compile regular expression %s : %s", h.ResponseStringMatch, err) + } + } + // Set default values if h.ResponseTimeout.Duration < time.Second { h.ResponseTimeout.Duration = time.Second * 5 @@ -237,9 +302,10 @@ func (h *HTTPResponse) Gather(acc telegraf.Accumulator) error { if addr.Scheme != "http" && addr.Scheme != "https" { return errors.New("Only http and https are supported") } + // Prepare data - tags := map[string]string{"server": h.Address, "method": h.Method} var fields map[string]interface{} + var tags map[string]string if h.client == nil { client, err := h.createHttpClient() @@ -250,10 +316,11 @@ func (h *HTTPResponse) Gather(acc telegraf.Accumulator) error { } // Gather data - fields, err = h.httpGather() + fields, tags, err = h.httpGather() if err != nil { return err } + // Add metrics acc.AddFields("http_response", fields, tags) return nil diff --git a/plugins/inputs/http_response/http_response_test.go b/plugins/inputs/http_response/http_response_test.go index 484a06f2f..c12d6c0f0 100644 --- a/plugins/inputs/http_response/http_response_test.go +++ b/plugins/inputs/http_response/http_response_test.go @@ -15,6 +15,68 @@ import ( "github.com/stretchr/testify/require" ) +// Receives a list with fields that are expected to be absent +func checkAbsentFields(t *testing.T, fields []string, acc testutil.Accumulator) { + for _, field := range fields { + ok := acc.HasField("http_response", field) + require.False(t, ok) + } +} + +// Receives a list with tags that are expected to be absent +func checkAbsentTags(t *testing.T, tags []string, acc testutil.Accumulator) { + for _, tag := range tags { + ok := acc.HasTag("http_response", tag) + require.False(t, ok) + } +} + +// Receives a dictionary and with expected fields and their values. If a value is nil, it will only check +// that the field exists, but not its contents +func checkFields(t *testing.T, fields map[string]interface{}, acc testutil.Accumulator) { + for key, field := range fields { + switch v := field.(type) { + case int: + value, ok := acc.IntField("http_response", key) + require.True(t, ok) + require.Equal(t, field, value) + case float64: + value, ok := acc.FloatField("http_response", key) + require.True(t, ok) + require.Equal(t, field, value) + case string: + value, ok := acc.StringField("http_response", key) + require.True(t, ok) + require.Equal(t, field, value) + case nil: + ok := acc.HasField("http_response", key) + require.True(t, ok) + default: + t.Log("Unsupported type for field: ", v) + t.Fail() + } + } +} + +// Receives a dictionary and with expected tags and their values. If a value is nil, it will only check +// that the tag exists, but not its contents +func checkTags(t *testing.T, tags map[string]interface{}, acc testutil.Accumulator) { + for key, tag := range tags { + switch v := tag.(type) { + case string: + ok := acc.HasTag("http_response", key) + require.True(t, ok) + require.Equal(t, tag, acc.TagValue("http_response", key)) + case nil: + ok := acc.HasTag("http_response", key) + require.True(t, ok) + default: + t.Log("Unsupported type for tag: ", v) + t.Fail() + } + } +} + func setUpTestMux() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/redirect", func(w http.ResponseWriter, req *http.Request) { @@ -56,6 +118,24 @@ func setUpTestMux() http.Handler { return mux } +func checkOutput(t *testing.T, acc testutil.Accumulator, presentFields map[string]interface{}, presentTags map[string]interface{}, absentFields []string, absentTags []string) { + if presentFields != nil { + checkFields(t, presentFields, acc) + } + + if presentTags != nil { + checkTags(t, presentTags, acc) + } + + if absentFields != nil { + checkAbsentFields(t, absentFields, acc) + } + + if absentTags != nil { + checkAbsentTags(t, absentTags, acc) + } +} + func TestHeaders(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cHeader := r.Header.Get("Content-Type") @@ -78,9 +158,20 @@ func TestHeaders(t *testing.T) { err := h.Gather(&acc) require.NoError(t, err) - value, ok := acc.IntField("http_response", "http_response_code") - require.True(t, ok) - require.Equal(t, http.StatusOK, value) + expectedFields := map[string]interface{}{ + "http_response_code": http.StatusOK, + "result_type": "success", + "result_code": 0, + "response_time": nil, + } + expectedTags := map[string]interface{}{ + "server": nil, + "method": "GET", + "status_code": "200", + "result": "success", + } + absentFields := []string{"response_string_match"} + checkOutput(t, acc, expectedFields, expectedTags, absentFields, nil) } func TestFields(t *testing.T) { @@ -103,12 +194,20 @@ func TestFields(t *testing.T) { err := h.Gather(&acc) require.NoError(t, err) - value, ok := acc.IntField("http_response", "http_response_code") - require.True(t, ok) - require.Equal(t, http.StatusOK, value) - response_value, ok := acc.StringField("http_response", "result_type") - require.True(t, ok) - require.Equal(t, "success", response_value) + expectedFields := map[string]interface{}{ + "http_response_code": http.StatusOK, + "result_type": "success", + "result_code": 0, + "response_time": nil, + } + expectedTags := map[string]interface{}{ + "server": nil, + "method": "GET", + "status_code": "200", + "result": "success", + } + absentFields := []string{"response_string_match"} + checkOutput(t, acc, expectedFields, expectedTags, absentFields, nil) } func TestRedirects(t *testing.T) { @@ -130,9 +229,20 @@ func TestRedirects(t *testing.T) { err := h.Gather(&acc) require.NoError(t, err) - value, ok := acc.IntField("http_response", "http_response_code") - require.True(t, ok) - require.Equal(t, http.StatusOK, value) + expectedFields := map[string]interface{}{ + "http_response_code": http.StatusOK, + "result_type": "success", + "result_code": 0, + "response_time": nil, + } + expectedTags := map[string]interface{}{ + "server": nil, + "method": "GET", + "status_code": "200", + "result": "success", + } + absentFields := []string{"response_string_match"} + checkOutput(t, acc, expectedFields, expectedTags, absentFields, nil) h = &HTTPResponse{ Address: ts.URL + "/badredirect", @@ -148,11 +258,21 @@ func TestRedirects(t *testing.T) { err = h.Gather(&acc) require.NoError(t, err) - value, ok = acc.IntField("http_response", "http_response_code") - require.False(t, ok) - response_value, ok := acc.StringField("http_response", "result_type") - require.True(t, ok) - require.Equal(t, "connection_failed", response_value) + expectedFields = map[string]interface{}{ + "result_type": "connection_failed", + "result_code": 3, + } + expectedTags = map[string]interface{}{ + "server": nil, + "method": "GET", + "result": "connection_failed", + } + absentFields = []string{"http_response_code", "response_time", "response_string_match"} + absentTags := []string{"status_code"} + checkOutput(t, acc, expectedFields, expectedTags, nil, nil) + + expectedFields = map[string]interface{}{"result_type": "connection_failed"} + checkOutput(t, acc, expectedFields, expectedTags, absentFields, absentTags) } func TestMethod(t *testing.T) { @@ -174,9 +294,20 @@ func TestMethod(t *testing.T) { err := h.Gather(&acc) require.NoError(t, err) - value, ok := acc.IntField("http_response", "http_response_code") - require.True(t, ok) - require.Equal(t, http.StatusOK, value) + expectedFields := map[string]interface{}{ + "http_response_code": http.StatusOK, + "result_type": "success", + "result_code": 0, + "response_time": nil, + } + expectedTags := map[string]interface{}{ + "server": nil, + "method": "POST", + "status_code": "200", + "result": "success", + } + absentFields := []string{"response_string_match"} + checkOutput(t, acc, expectedFields, expectedTags, absentFields, nil) h = &HTTPResponse{ Address: ts.URL + "/mustbepostmethod", @@ -192,9 +323,20 @@ func TestMethod(t *testing.T) { err = h.Gather(&acc) require.NoError(t, err) - value, ok = acc.IntField("http_response", "http_response_code") - require.True(t, ok) - require.Equal(t, http.StatusMethodNotAllowed, value) + expectedFields = map[string]interface{}{ + "http_response_code": http.StatusMethodNotAllowed, + "result_type": "success", + "result_code": 0, + "response_time": nil, + } + expectedTags = map[string]interface{}{ + "server": nil, + "method": "GET", + "status_code": "405", + "result": "success", + } + absentFields = []string{"response_string_match"} + checkOutput(t, acc, expectedFields, expectedTags, absentFields, nil) //check that lowercase methods work correctly h = &HTTPResponse{ @@ -211,9 +353,20 @@ func TestMethod(t *testing.T) { err = h.Gather(&acc) require.NoError(t, err) - value, ok = acc.IntField("http_response", "http_response_code") - require.True(t, ok) - require.Equal(t, http.StatusMethodNotAllowed, value) + expectedFields = map[string]interface{}{ + "http_response_code": http.StatusMethodNotAllowed, + "result_type": "success", + "result_code": 0, + "response_time": nil, + } + expectedTags = map[string]interface{}{ + "server": nil, + "method": "head", + "status_code": "405", + "result": "success", + } + absentFields = []string{"response_string_match"} + checkOutput(t, acc, expectedFields, expectedTags, absentFields, nil) } func TestBody(t *testing.T) { @@ -235,9 +388,20 @@ func TestBody(t *testing.T) { err := h.Gather(&acc) require.NoError(t, err) - value, ok := acc.IntField("http_response", "http_response_code") - require.True(t, ok) - require.Equal(t, http.StatusOK, value) + expectedFields := map[string]interface{}{ + "http_response_code": http.StatusOK, + "result_type": "success", + "result_code": 0, + "response_time": nil, + } + expectedTags := map[string]interface{}{ + "server": nil, + "method": "GET", + "status_code": "200", + "result": "success", + } + absentFields := []string{"response_string_match"} + checkOutput(t, acc, expectedFields, expectedTags, absentFields, nil) h = &HTTPResponse{ Address: ts.URL + "/musthaveabody", @@ -252,9 +416,19 @@ func TestBody(t *testing.T) { err = h.Gather(&acc) require.NoError(t, err) - value, ok = acc.IntField("http_response", "http_response_code") - require.True(t, ok) - require.Equal(t, http.StatusBadRequest, value) + expectedFields = map[string]interface{}{ + "http_response_code": http.StatusBadRequest, + "result_type": "success", + "result_code": 0, + } + expectedTags = map[string]interface{}{ + "server": nil, + "method": "GET", + "status_code": "400", + "result": "success", + } + absentFields = []string{"response_string_match"} + checkOutput(t, acc, expectedFields, expectedTags, absentFields, nil) } func TestStringMatch(t *testing.T) { @@ -277,17 +451,20 @@ func TestStringMatch(t *testing.T) { err := h.Gather(&acc) require.NoError(t, err) - value, ok := acc.IntField("http_response", "http_response_code") - require.True(t, ok) - require.Equal(t, http.StatusOK, value) - value, ok = acc.IntField("http_response", "response_string_match") - require.True(t, ok) - require.Equal(t, 1, value) - response_value, ok := acc.StringField("http_response", "result_type") - require.True(t, ok) - require.Equal(t, "success", response_value) - _, ok = acc.FloatField("http_response", "response_time") - require.True(t, ok) + expectedFields := map[string]interface{}{ + "http_response_code": http.StatusOK, + "response_string_match": 1, + "result_type": "success", + "result_code": 0, + "response_time": nil, + } + expectedTags := map[string]interface{}{ + "server": nil, + "method": "GET", + "status_code": "200", + "result": "success", + } + checkOutput(t, acc, expectedFields, expectedTags, nil, nil) } func TestStringMatchJson(t *testing.T) { @@ -310,17 +487,20 @@ func TestStringMatchJson(t *testing.T) { err := h.Gather(&acc) require.NoError(t, err) - value, ok := acc.IntField("http_response", "http_response_code") - require.True(t, ok) - require.Equal(t, http.StatusOK, value) - value, ok = acc.IntField("http_response", "response_string_match") - require.True(t, ok) - require.Equal(t, 1, value) - response_value, ok := acc.StringField("http_response", "result_type") - require.True(t, ok) - require.Equal(t, "success", response_value) - _, ok = acc.FloatField("http_response", "response_time") - require.True(t, ok) + expectedFields := map[string]interface{}{ + "http_response_code": http.StatusOK, + "response_string_match": 1, + "result_type": "success", + "result_code": 0, + "response_time": nil, + } + expectedTags := map[string]interface{}{ + "server": nil, + "method": "GET", + "status_code": "200", + "result": "success", + } + checkOutput(t, acc, expectedFields, expectedTags, nil, nil) } func TestStringMatchFail(t *testing.T) { @@ -344,17 +524,20 @@ func TestStringMatchFail(t *testing.T) { err := h.Gather(&acc) require.NoError(t, err) - value, ok := acc.IntField("http_response", "http_response_code") - require.True(t, ok) - require.Equal(t, http.StatusOK, value) - value, ok = acc.IntField("http_response", "response_string_match") - require.True(t, ok) - require.Equal(t, 0, value) - response_value, ok := acc.StringField("http_response", "result_type") - require.True(t, ok) - require.Equal(t, "response_string_mismatch", response_value) - _, ok = acc.FloatField("http_response", "response_time") - require.True(t, ok) + expectedFields := map[string]interface{}{ + "http_response_code": http.StatusOK, + "response_string_match": 0, + "result_type": "response_string_mismatch", + "result_code": 1, + "response_time": nil, + } + expectedTags := map[string]interface{}{ + "server": nil, + "method": "GET", + "status_code": "200", + "result": "response_string_mismatch", + } + checkOutput(t, acc, expectedFields, expectedTags, nil, nil) } func TestTimeout(t *testing.T) { @@ -380,11 +563,126 @@ func TestTimeout(t *testing.T) { err := h.Gather(&acc) require.NoError(t, err) - _, ok := acc.IntField("http_response", "http_response_code") - require.False(t, ok) - response_value, ok := acc.StringField("http_response", "result_type") - require.True(t, ok) - require.Equal(t, "timeout", response_value) - _, ok = acc.FloatField("http_response", "response_time") - require.False(t, ok) + expectedFields := map[string]interface{}{ + "result_type": "timeout", + "result_code": 4, + } + expectedTags := map[string]interface{}{ + "server": nil, + "method": "GET", + "result": "timeout", + } + absentFields := []string{"http_response_code", "response_time", "response_string_match"} + absentTags := []string{"status_code"} + checkOutput(t, acc, expectedFields, expectedTags, absentFields, absentTags) +} + +func TestPluginErrors(t *testing.T) { + mux := setUpTestMux() + ts := httptest.NewServer(mux) + defer ts.Close() + + // Bad regex test. Should return an error and return nothing + h := &HTTPResponse{ + Address: ts.URL + "/good", + Body: "{ 'test': 'data'}", + Method: "GET", + ResponseStringMatch: "bad regex:[[", + ResponseTimeout: internal.Duration{Duration: time.Second * 20}, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + FollowRedirects: true, + } + + var acc testutil.Accumulator + err := h.Gather(&acc) + require.Error(t, err) + + absentFields := []string{"http_response_code", "response_time", "response_string_match", "result_type", "result_code"} + absentTags := []string{"status_code", "result", "server", "method"} + checkOutput(t, acc, nil, nil, absentFields, absentTags) + + // Attempt to read empty body test + h = &HTTPResponse{ + Address: ts.URL + "/redirect", + Body: "", + Method: "GET", + ResponseStringMatch: ".*", + ResponseTimeout: internal.Duration{Duration: time.Second * 20}, + FollowRedirects: false, + } + + acc = testutil.Accumulator{} + err = h.Gather(&acc) + require.NoError(t, err) + + expectedFields := map[string]interface{}{ + "http_response_code": http.StatusMovedPermanently, + "response_string_match": 0, + "result_type": "body_read_error", + "result_code": 2, + "response_time": nil, + } + expectedTags := map[string]interface{}{ + "server": nil, + "method": "GET", + "status_code": "301", + "result": "body_read_error", + } + checkOutput(t, acc, expectedFields, expectedTags, nil, nil) +} + +func TestNetworkErrors(t *testing.T) { + // DNS error + h := &HTTPResponse{ + Address: "https://nonexistent.nonexistent", // Any non-resolvable URL works here + Body: "", + Method: "GET", + ResponseTimeout: internal.Duration{Duration: time.Second * 20}, + FollowRedirects: false, + } + + var acc testutil.Accumulator + err := h.Gather(&acc) + require.NoError(t, err) + + expectedFields := map[string]interface{}{ + "result_type": "dns_error", + "result_code": 5, + } + expectedTags := map[string]interface{}{ + "server": nil, + "method": "GET", + "result": "dns_error", + } + absentFields := []string{"http_response_code", "response_time", "response_string_match"} + absentTags := []string{"status_code"} + checkOutput(t, acc, expectedFields, expectedTags, absentFields, absentTags) + + // Connecton failed + h = &HTTPResponse{ + Address: "https://127.127.127.127", // Any non-routable IP works here + Body: "", + Method: "GET", + ResponseTimeout: internal.Duration{Duration: time.Second * 20}, + FollowRedirects: false, + } + + acc = testutil.Accumulator{} + err = h.Gather(&acc) + require.NoError(t, err) + + expectedFields = map[string]interface{}{ + "result_type": "connection_failed", + "result_code": 3, + } + expectedTags = map[string]interface{}{ + "server": nil, + "method": "GET", + "result": "connection_failed", + } + absentFields = []string{"http_response_code", "response_time", "response_string_match"} + absentTags = []string{"status_code"} + checkOutput(t, acc, expectedFields, expectedTags, absentFields, absentTags) }