telegraf/plugins/inputs/http_response/http_response.go

436 lines
11 KiB
Go
Raw Normal View History

//go:generate ../../../tools/readme_config_includer/generator
2016-03-31 15:33:28 +08:00
package http_response
import (
_ "embed"
2016-03-31 15:33:28 +08:00
"errors"
"fmt"
2016-03-31 19:06:47 +08:00
"io"
"net"
2016-03-31 15:33:28 +08:00
"net/http"
"net/url"
"os"
"regexp"
"strconv"
2016-03-31 17:53:51 +08:00
"strings"
2016-03-31 15:33:28 +08:00
"time"
"unicode/utf8"
2016-03-31 15:33:28 +08:00
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/common/tls"
2016-03-31 15:33:28 +08:00
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample.conf
var sampleConfig string
const (
// defaultResponseBodyMaxSize is the default maximum response body size, in bytes.
// if the response body is over this size, we will raise a body_read_error.
defaultResponseBodyMaxSize = 32 * 1024 * 1024
)
2016-03-31 17:53:51 +08:00
// HTTPResponse struct
type HTTPResponse struct {
Address string `toml:"address" deprecated:"1.12.0;use 'urls' instead"`
URLs []string `toml:"urls"`
HTTPProxy string `toml:"http_proxy"`
Body string
BodyForm map[string][]string `toml:"body_form"`
Method string
ResponseTimeout config.Duration
HTTPHeaderTags map[string]string `toml:"http_header_tags"`
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 config.Size `toml:"response_body_max_size"`
ResponseStringMatch string
ResponseStatusCode int
Interface string
// HTTP Basic Auth Credentials
Username config.Secret `toml:"username"`
Password config.Secret `toml:"password"`
2018-05-05 07:33:23 +08:00
tls.ClientConfig
Log telegraf.Logger
compiledStringMatch *regexp.Regexp
2020-12-05 01:08:11 +08:00
client httpClient
}
type httpClient interface {
Do(req *http.Request) (*http.Response, error)
2016-03-31 15:33:28 +08:00
}
// Set the proxy. A configured proxy overwrites the system wide proxy.
func getProxyFunc(httpProxy string) func(*http.Request) (*url.URL, error) {
if httpProxy == "" {
return http.ProxyFromEnvironment
}
proxyURL, err := url.Parse(httpProxy)
if err != nil {
return func(_ *http.Request) (*url.URL, error) {
return nil, errors.New("bad proxy: " + err.Error())
}
}
return func(r *http.Request) (*url.URL, error) {
return proxyURL, nil
}
}
// createHTTPClient creates an http client which will timeout at the specified
2016-04-04 10:20:07 +08:00
// timeout period and can follow redirects if specified
func (h *HTTPResponse) createHTTPClient() (*http.Client, error) {
2018-05-05 07:33:23 +08:00
tlsCfg, err := h.ClientConfig.TLSConfig()
if err != nil {
return nil, err
}
dialer := &net.Dialer{}
if h.Interface != "" {
dialer.LocalAddr, err = localAddress(h.Interface)
if err != nil {
return nil, err
}
}
2016-03-31 15:33:28 +08:00
client := &http.Client{
Transport: &http.Transport{
Proxy: getProxyFunc(h.HTTPProxy),
DialContext: dialer.DialContext,
DisableKeepAlives: true,
TLSClientConfig: tlsCfg,
},
Timeout: time.Duration(h.ResponseTimeout),
2016-03-31 15:33:28 +08:00
}
2021-03-26 01:57:01 +08:00
if !h.FollowRedirects {
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
}
return client, nil
2016-04-04 10:20:07 +08:00
}
func localAddress(interfaceName string) (net.Addr, error) {
i, err := net.InterfaceByName(interfaceName)
if err != nil {
return nil, err
}
addrs, err := i.Addrs()
if err != nil {
return nil, err
}
for _, addr := range addrs {
if naddr, ok := addr.(*net.IPNet); ok {
// leaving port set to zero to let kernel pick
return &net.TCPAddr{IP: naddr.IP}, nil
}
}
return nil, fmt.Errorf("cannot create local address for interface %q", interfaceName)
}
func setResult(resultString string, fields map[string]interface{}, tags map[string]string) {
resultCodes := map[string]int{
"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"] = resultString
fields["result_type"] = resultString
fields["result_code"] = resultCodes[resultString]
}
func setError(err error, fields map[string]interface{}, tags map[string]string) error {
var timeoutError net.Error
if errors.As(err, &timeoutError) && timeoutError.Timeout() {
setResult("timeout", fields, tags)
return timeoutError
}
var urlErr *url.Error
if !errors.As(err, &urlErr) {
return nil
}
var opErr *net.OpError
if errors.As(urlErr, &opErr) {
var dnsErr *net.DNSError
var parseErr *net.ParseError
if errors.As(opErr, &dnsErr) {
setResult("dns_error", fields, tags)
return dnsErr
} else if errors.As(opErr, &parseErr) {
// Parse error has to do with parsing of IP addresses, so we
// group it with address errors
setResult("address_error", fields, tags)
return parseErr
}
}
return nil
}
2016-04-04 10:20:07 +08:00
// HTTPGather gathers all fields and returns any errors it encounters
func (h *HTTPResponse) httpGather(u string) (map[string]interface{}, map[string]string, error) {
// Prepare fields and tags
2016-04-04 10:20:07 +08:00
fields := make(map[string]interface{})
tags := map[string]string{"server": u, "method": h.Method}
2016-04-04 10:20:07 +08:00
2016-03-31 19:06:47 +08:00
var body io.Reader
if h.Body != "" {
body = strings.NewReader(h.Body)
} else if len(h.BodyForm) != 0 {
values := url.Values{}
for k, vs := range h.BodyForm {
for _, v := range vs {
values.Add(k, v)
}
}
body = strings.NewReader(values.Encode())
2016-03-31 19:06:47 +08:00
}
request, err := http.NewRequest(h.Method, u, body)
2016-03-31 15:33:28 +08:00
if err != nil {
return nil, nil, err
2016-03-31 15:33:28 +08:00
}
if _, uaPresent := h.Headers["User-Agent"]; !uaPresent {
request.Header.Set("User-Agent", internal.ProductToken())
}
if h.BearerToken != "" {
token, err := os.ReadFile(h.BearerToken)
if err != nil {
return nil, nil, err
}
bearer := "Bearer " + strings.Trim(string(token), "\n")
request.Header.Add("Authorization", bearer)
}
for key, val := range h.Headers {
request.Header.Add(key, val)
if key == "Host" {
request.Host = val
}
}
if err := h.setRequestAuth(request); err != nil {
return nil, nil, err
}
2016-03-31 15:33:28 +08:00
// Start Timer
start := time.Now()
resp, err := h.client.Do(request)
responseTime := 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
2016-03-31 15:33:28 +08:00
if err != nil {
// Log error
h.Log.Debugf("Network error while polling %s: %s", u, err.Error())
// Get error details
if setError(err, fields, tags) == nil {
// Any error not recognized by `set_error` is considered a "connection_failed"
setResult("connection_failed", fields, tags)
}
return fields, tags, nil
2016-03-31 15:33:28 +08:00
}
if _, ok := fields["response_time"]; !ok {
fields["response_time"] = responseTime
}
// This function closes the response body, as
// required by the net/http library
defer resp.Body.Close()
// Add the response headers
for headerName, tag := range h.HTTPHeaderTags {
headerValues, foundHeader := resp.Header[headerName]
if foundHeader && len(headerValues) > 0 {
tags[tag] = headerValues[0]
}
}
// Set log the HTTP response code
tags["status_code"] = strconv.Itoa(resp.StatusCode)
2016-03-31 15:33:28 +08:00
fields["http_response_code"] = resp.StatusCode
if h.ResponseBodyMaxSize == 0 {
h.ResponseBodyMaxSize = config.Size(defaultResponseBodyMaxSize)
}
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, int64(h.ResponseBodyMaxSize)+1))
// Check first if the response body size exceeds the limit.
if err == nil && int64(len(bodyBytes)) > int64(h.ResponseBodyMaxSize) {
h.setBodyReadError("The body of the HTTP Response is too large", bodyBytes, fields, tags)
return fields, tags, nil
} else if err != nil {
h.setBodyReadError("Failed to read body of HTTP Response : "+err.Error(), bodyBytes, fields, tags)
return fields, tags, nil //nolint:nilerr // error is handled properly
}
// Add the body of the response if expected
if len(h.ResponseBodyField) > 0 {
// Check that the content of response contains only valid utf-8 characters.
if !utf8.Valid(bodyBytes) {
h.setBodyReadError("The body of the HTTP Response is not a valid utf-8 string", bodyBytes, fields, tags)
return fields, tags, nil
}
fields[h.ResponseBodyField] = string(bodyBytes)
}
fields["content_length"] = len(bodyBytes)
var success = true
// Check the response for a regex
if h.ResponseStringMatch != "" {
if h.compiledStringMatch.Match(bodyBytes) {
fields["response_string_match"] = 1
} else {
success = false
setResult("response_string_mismatch", fields, tags)
fields["response_string_match"] = 0
}
}
// 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)
}
return fields, tags, nil
2016-03-31 15:33:28 +08:00
}
// Set result in case of a body read error
func (h *HTTPResponse) setBodyReadError(errorMsg string, bodyBytes []byte, fields map[string]interface{}, tags map[string]string) {
h.Log.Debugf(errorMsg)
setResult("body_read_error", fields, tags)
fields["content_length"] = len(bodyBytes)
if h.ResponseStringMatch != "" {
fields["response_string_match"] = 0
}
}
func (*HTTPResponse) SampleConfig() string {
return sampleConfig
}
2016-03-31 17:53:51 +08:00
// 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 %q: %w", h.ResponseStringMatch, err)
}
}
2016-03-31 15:33:28 +08:00
// Set default values
if h.ResponseTimeout < config.Duration(time.Second) {
h.ResponseTimeout = config.Duration(time.Second * 5)
2016-03-31 15:33:28 +08:00
}
// Check send and expected string
2016-03-31 17:53:51 +08:00
if h.Method == "" {
h.Method = "GET"
2016-03-31 15:33:28 +08:00
}
if len(h.URLs) == 0 {
if h.Address == "" {
h.URLs = []string{"http://localhost"}
} else {
h.URLs = []string{h.Address}
}
}
if h.client == nil {
client, err := h.createHTTPClient()
if err != nil {
return err
}
h.client = client
}
for _, u := range h.URLs {
addr, err := url.Parse(u)
if err != nil {
acc.AddError(err)
continue
}
if addr.Scheme != "http" && addr.Scheme != "https" {
2021-03-26 01:57:01 +08:00
acc.AddError(errors.New("only http and https are supported"))
continue
}
// Prepare data
var fields map[string]interface{}
var tags map[string]string
// Gather data
fields, tags, err = h.httpGather(u)
if err != nil {
acc.AddError(err)
continue
}
// Add metrics
acc.AddFields("http_response", fields, tags)
2016-03-31 15:33:28 +08:00
}
2016-03-31 15:33:28 +08:00
return nil
}
func (h *HTTPResponse) setRequestAuth(request *http.Request) error {
if h.Username.Empty() || h.Password.Empty() {
return nil
}
username, err := h.Username.Get()
if err != nil {
return fmt.Errorf("getting username failed: %w", err)
}
defer username.Destroy()
password, err := h.Password.Get()
if err != nil {
return fmt.Errorf("getting password failed: %w", err)
}
defer password.Destroy()
request.SetBasicAuth(username.String(), password.String())
return nil
}
2016-03-31 15:33:28 +08:00
func init() {
inputs.Add("http_response", func() telegraf.Input {
2016-03-31 17:53:51 +08:00
return &HTTPResponse{}
2016-03-31 15:33:28 +08:00
})
}