diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index 55a932df204fe..bed1b6444ce21 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -14,6 +14,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/exec" _ "github.com/influxdata/telegraf/plugins/inputs/github_webhooks" _ "github.com/influxdata/telegraf/plugins/inputs/haproxy" + _ "github.com/influxdata/telegraf/plugins/inputs/http_response" _ "github.com/influxdata/telegraf/plugins/inputs/httpjson" _ "github.com/influxdata/telegraf/plugins/inputs/influxdb" _ "github.com/influxdata/telegraf/plugins/inputs/jolokia" diff --git a/plugins/inputs/http_response/README.md b/plugins/inputs/http_response/README.md new file mode 100644 index 0000000000000..e2bf75b5fd788 --- /dev/null +++ b/plugins/inputs/http_response/README.md @@ -0,0 +1,44 @@ +# Example Input Plugin + +This input plugin will test HTTP/HTTPS connections. + +### Configuration: + +``` +# List of UDP/TCP connections you want to check +[[inputs.http_response]] + ## Server address (default http://localhost) + address = "http://github.com" + ## Set response_timeout (default 5 seconds) + response_timeout = 5 + ## HTTP Request Method + method = "GET" + ## HTTP Request Headers + [inputs.http_response.headers] + Host = github.com + ## Whether to follow redirects from the server (defaults to false) + follow_redirects = true + ## Optional HTTP Request Body + body = ''' + {'fake':'data'} + ''' +``` + +### Measurements & Fields: + +- http_response + - response_time (float, seconds) + - http_response_code (int) #The code received + +### Tags: + +- All measurements have the following tags: + - server + - method + +### Example Output: + +``` +$ ./telegraf -config telegraf.conf -input-filter http_response -test +http_response,method=GET,server=http://www.github.com http_response_code=200i,response_time=6.223266528 1459419354977857955 +``` diff --git a/plugins/inputs/http_response/http_response.go b/plugins/inputs/http_response/http_response.go new file mode 100644 index 0000000000000..73533fed4c27c --- /dev/null +++ b/plugins/inputs/http_response/http_response.go @@ -0,0 +1,154 @@ +package http_response + +import ( + "errors" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/plugins/inputs" +) + +// HTTPResponse struct +type HTTPResponse struct { + Address string + Body string + Method string + ResponseTimeout int + Headers map[string]string + FollowRedirects bool +} + +// Description returns the plugin Description +func (h *HTTPResponse) Description() string { + return "HTTP/HTTPS request given an address a method and a timeout" +} + +var sampleConfig = ` + ## Server address (default http://localhost) + address = "http://github.com" + ## Set response_timeout (default 5 seconds) + response_timeout = 5 + ## HTTP Request Method + method = "GET" + ## HTTP Request Headers (all values must be strings) + [inputs.http_response.headers] + # Host = "github.com" + ## Whether to follow redirects from the server (defaults to false) + follow_redirects = true + ## Optional HTTP Request Body + body = ''' + {'fake':'data'} + ''' +` + +// SampleConfig returns the plugin SampleConfig +func (h *HTTPResponse) SampleConfig() string { + return sampleConfig +} + +// ErrRedirectAttempted indicates that a redirect occurred +var ErrRedirectAttempted = errors.New("redirect") + +// CreateHttpClient creates an http client which will timeout at the specified +// timeout period and can follow redirects if specified +func CreateHttpClient(followRedirects bool, ResponseTimeout time.Duration) *http.Client { + client := &http.Client{ + Timeout: time.Second * ResponseTimeout, + } + + if followRedirects == false { + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return ErrRedirectAttempted + } + } + return client +} + +// CreateHeaders takes a map of header strings and puts them +// into a http.Header Object +func CreateHeaders(headers map[string]string) http.Header { + httpHeaders := make(http.Header) + for key := range headers { + httpHeaders.Add(key, headers[key]) + } + return httpHeaders +} + +// HTTPGather gathers all fields and returns any errors it encounters +func (h *HTTPResponse) HTTPGather() (map[string]interface{}, error) { + // Prepare fields + fields := make(map[string]interface{}) + + client := CreateHttpClient(h.FollowRedirects, time.Duration(h.ResponseTimeout)) + + 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 + } + request.Header = CreateHeaders(h.Headers) + + // Start Timer + start := time.Now() + resp, err := client.Do(request) + if err != nil { + if h.FollowRedirects { + return nil, err + } + if urlError, ok := err.(*url.Error); ok && + urlError.Err == ErrRedirectAttempted { + err = nil + } else { + return nil, err + } + } + fields["response_time"] = time.Since(start).Seconds() + fields["http_response_code"] = resp.StatusCode + return fields, nil +} + +// Gather gets all metric fields and tags and returns any errors it encounters +func (h *HTTPResponse) Gather(acc telegraf.Accumulator) error { + // Set default values + if h.ResponseTimeout < 1 { + h.ResponseTimeout = 5 + } + // Check send and expected string + if h.Method == "" { + h.Method = "GET" + } + if h.Address == "" { + h.Address = "http://localhost" + } + addr, err := url.Parse(h.Address) + if err != nil { + return err + } + 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{} + // Gather data + fields, err = h.HTTPGather() + if err != nil { + return err + } + // Add metrics + acc.AddFields("http_response", fields, tags) + return nil +} + +func init() { + inputs.Add("http_response", func() telegraf.Input { + return &HTTPResponse{} + }) +} diff --git a/plugins/inputs/http_response/http_response_test.go b/plugins/inputs/http_response/http_response_test.go new file mode 100644 index 0000000000000..acdfeac7598b8 --- /dev/null +++ b/plugins/inputs/http_response/http_response_test.go @@ -0,0 +1,241 @@ +package http_response + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestCreateHeaders(t *testing.T) { + fakeHeaders := map[string]string{ + "Accept": "text/plain", + "Content-Type": "application/json", + "Cache-Control": "no-cache", + } + headers := CreateHeaders(fakeHeaders) + testHeaders := make(http.Header) + testHeaders.Add("Accept", "text/plain") + testHeaders.Add("Content-Type", "application/json") + testHeaders.Add("Cache-Control", "no-cache") + assert.Equal(t, testHeaders, headers) +} + +func setUpTestMux() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/redirect", func(w http.ResponseWriter, req *http.Request) { + http.Redirect(w, req, "/good", http.StatusMovedPermanently) + }) + mux.HandleFunc("/good", func(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "hit the good page!") + }) + mux.HandleFunc("/badredirect", func(w http.ResponseWriter, req *http.Request) { + http.Redirect(w, req, "/badredirect", http.StatusMovedPermanently) + }) + mux.HandleFunc("/mustbepostmethod", func(w http.ResponseWriter, req *http.Request) { + if req.Method != "POST" { + http.Error(w, "method wasn't post", http.StatusMethodNotAllowed) + return + } + fmt.Fprintf(w, "used post correctly!") + }) + mux.HandleFunc("/musthaveabody", func(w http.ResponseWriter, req *http.Request) { + body, err := ioutil.ReadAll(req.Body) + req.Body.Close() + if err != nil { + http.Error(w, "couldn't read request body", http.StatusBadRequest) + return + } + if string(body) == "" { + http.Error(w, "body was empty", http.StatusBadRequest) + return + } + fmt.Fprintf(w, "sent a body!") + }) + mux.HandleFunc("/twosecondnap", func(w http.ResponseWriter, req *http.Request) { + time.Sleep(time.Second * 2) + return + }) + return mux +} + +func TestFields(t *testing.T) { + mux := setUpTestMux() + ts := httptest.NewServer(mux) + defer ts.Close() + + h := &HTTPResponse{ + Address: ts.URL + "/good", + Body: "{ 'test': 'data'}", + Method: "GET", + ResponseTimeout: 20, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + FollowRedirects: true, + } + fields, err := h.HTTPGather() + require.NoError(t, err) + assert.NotEmpty(t, fields) + if assert.NotNil(t, fields["http_response_code"]) { + assert.Equal(t, http.StatusOK, fields["http_response_code"]) + } + assert.NotNil(t, fields["response_time"]) + +} + +func TestRedirects(t *testing.T) { + mux := setUpTestMux() + ts := httptest.NewServer(mux) + defer ts.Close() + + h := &HTTPResponse{ + Address: ts.URL + "/redirect", + Body: "{ 'test': 'data'}", + Method: "GET", + ResponseTimeout: 20, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + FollowRedirects: true, + } + fields, err := h.HTTPGather() + require.NoError(t, err) + assert.NotEmpty(t, fields) + if assert.NotNil(t, fields["http_response_code"]) { + assert.Equal(t, http.StatusOK, fields["http_response_code"]) + } + + h = &HTTPResponse{ + Address: ts.URL + "/badredirect", + Body: "{ 'test': 'data'}", + Method: "GET", + ResponseTimeout: 20, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + FollowRedirects: true, + } + fields, err = h.HTTPGather() + require.Error(t, err) +} + +func TestMethod(t *testing.T) { + mux := setUpTestMux() + ts := httptest.NewServer(mux) + defer ts.Close() + + h := &HTTPResponse{ + Address: ts.URL + "/mustbepostmethod", + Body: "{ 'test': 'data'}", + Method: "POST", + ResponseTimeout: 20, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + FollowRedirects: true, + } + fields, err := h.HTTPGather() + require.NoError(t, err) + assert.NotEmpty(t, fields) + if assert.NotNil(t, fields["http_response_code"]) { + assert.Equal(t, http.StatusOK, fields["http_response_code"]) + } + + h = &HTTPResponse{ + Address: ts.URL + "/mustbepostmethod", + Body: "{ 'test': 'data'}", + Method: "GET", + ResponseTimeout: 20, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + FollowRedirects: true, + } + fields, err = h.HTTPGather() + require.NoError(t, err) + assert.NotEmpty(t, fields) + if assert.NotNil(t, fields["http_response_code"]) { + assert.Equal(t, http.StatusMethodNotAllowed, fields["http_response_code"]) + } + + //check that lowercase methods work correctly + h = &HTTPResponse{ + Address: ts.URL + "/mustbepostmethod", + Body: "{ 'test': 'data'}", + Method: "head", + ResponseTimeout: 20, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + FollowRedirects: true, + } + fields, err = h.HTTPGather() + require.NoError(t, err) + assert.NotEmpty(t, fields) + if assert.NotNil(t, fields["http_response_code"]) { + assert.Equal(t, http.StatusMethodNotAllowed, fields["http_response_code"]) + } +} + +func TestBody(t *testing.T) { + mux := setUpTestMux() + ts := httptest.NewServer(mux) + defer ts.Close() + + h := &HTTPResponse{ + Address: ts.URL + "/musthaveabody", + Body: "{ 'test': 'data'}", + Method: "GET", + ResponseTimeout: 20, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + FollowRedirects: true, + } + fields, err := h.HTTPGather() + require.NoError(t, err) + assert.NotEmpty(t, fields) + if assert.NotNil(t, fields["http_response_code"]) { + assert.Equal(t, http.StatusOK, fields["http_response_code"]) + } + + h = &HTTPResponse{ + Address: ts.URL + "/musthaveabody", + Method: "GET", + ResponseTimeout: 20, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + FollowRedirects: true, + } + fields, err = h.HTTPGather() + require.NoError(t, err) + assert.NotEmpty(t, fields) + if assert.NotNil(t, fields["http_response_code"]) { + assert.Equal(t, http.StatusBadRequest, fields["http_response_code"]) + } +} + +func TestTimeout(t *testing.T) { + mux := setUpTestMux() + ts := httptest.NewServer(mux) + defer ts.Close() + + h := &HTTPResponse{ + Address: ts.URL + "/twosecondnap", + Body: "{ 'test': 'data'}", + Method: "GET", + ResponseTimeout: 1, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + FollowRedirects: true, + } + _, err := h.HTTPGather() + require.Error(t, err) +}