checkers/http.go
package checkers
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
)
const (
defaultHTTPTimeout = time.Duration(3) * time.Second
)
// HTTPConfig is used for configuring an HTTP check. The only required field is `URL`.
//
// "Method" is optional and defaults to `GET` if undefined.
//
// "Payload" is optional and can accept `string`, `[]byte` or will attempt to
// marshal the input to JSON for use w/ `bytes.NewReader()`.
//
// "StatusCode" is optional and defaults to `200`.
//
// "Expect" is optional; if defined, operates as a basic "body should contain <string>".
//
// "Client" is optional; if undefined, a new client will be created using "Timeout".
//
// "Timeout" is optional and defaults to "3s".
type HTTPConfig struct {
URL *url.URL // Required
Method string // Optional (default GET)
Payload interface{} // Optional
StatusCode int // Optional (default 200)
Expect string // Optional
Client *http.Client // Optional
Timeout time.Duration // Optional (default 3s)
}
// HTTP implements the "ICheckable" interface.
type HTTP struct {
Config *HTTPConfig
}
// NewHTTP creates a new HTTP checker that can be used for ".AddCheck(s)".
func NewHTTP(cfg *HTTPConfig) (*HTTP, error) {
if cfg == nil {
return nil, fmt.Errorf("Passed in config cannot be nil")
}
if err := cfg.prepare(); err != nil {
return nil, fmt.Errorf("Unable to prepare given config: %v", err)
}
return &HTTP{
Config: cfg,
}, nil
}
// Status is used for performing an HTTP check against a dependency; it satisfies
// the "ICheckable" interface.
func (h *HTTP) Status() (interface{}, error) {
resp, err := h.do()
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Check if StatusCode matches
if resp.StatusCode != h.Config.StatusCode {
return nil, fmt.Errorf("Received status code '%v' does not match expected status code '%v'",
resp.StatusCode, h.Config.StatusCode)
}
// If Expect is set, verify if returned response contains expected data
if h.Config.Expect != "" {
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("Unable to read response body to perform content expectancy check: %v", err)
}
if !strings.Contains(string(data), h.Config.Expect) {
return nil, fmt.Errorf("Received response body '%v' does not contain expected content '%v'",
string(data), h.Config.Expect)
}
}
return nil, nil
}
func (h *HTTP) do() (*http.Response, error) {
payload, err := parsePayload(h.Config.Payload)
if err != nil {
return nil, fmt.Errorf("error parsing payload: %v", err)
}
req, err := http.NewRequest(h.Config.Method, h.Config.URL.String(), payload)
if err != nil {
return nil, fmt.Errorf("Unable to create new HTTP request for HTTPMonitor check: %v", err)
}
resp, err := h.Config.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("Ran into error while performing '%v' request: %v", h.Config.Method, err)
}
return resp, nil
}
func (h *HTTPConfig) prepare() error {
if h.URL == nil {
return errors.New("URL cannot be nil")
}
// Default StatusCode to 200
if h.StatusCode == 0 {
h.StatusCode = http.StatusOK
}
// Default to GET
if h.Method == "" {
h.Method = "GET"
}
if h.Timeout == 0 {
h.Timeout = defaultHTTPTimeout
}
if h.Client == nil {
h.Client = &http.Client{Timeout: h.Timeout}
} else {
h.Client.Timeout = h.Timeout
}
return nil
}
func parsePayload(b interface{}) (io.Reader, error) {
if b == nil {
return nil, nil
}
switch b.(type) {
case []byte:
return bytes.NewReader(b.([]byte)), nil
case string:
return bytes.NewReader([]byte(b.(string))), nil
default:
jb, err := json.Marshal(b)
if err != nil {
return nil, fmt.Errorf("failed to marshal json body: %v", err)
}
return bytes.NewReader(jb), nil
}
}