Skip to content

Commit

Permalink
Add result related tags and fields to http_response (#3814)
Browse files Browse the repository at this point in the history
  • Loading branch information
mirath authored and danielnelson committed Mar 8, 2018
1 parent fe78df3 commit e9da4e5
Show file tree
Hide file tree
Showing 3 changed files with 496 additions and 111 deletions.
32 changes: 26 additions & 6 deletions plugins/inputs/http_response/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
133 changes: 100 additions & 33 deletions plugins/inputs/http_response/http_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package http_response

import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -133,18 +135,62 @@ 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 != "" {
body = strings.NewReader(h.Body)
}
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 {
Expand All @@ -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
}
fields["result_type"] = "connection_failed"
if h.FollowRedirects {
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
}
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
Expand All @@ -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()
Expand All @@ -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
Expand Down
Loading

0 comments on commit e9da4e5

Please sign in to comment.