/
http.go
142 lines (122 loc) · 4.08 KB
/
http.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package checks
import (
"bytes"
"context"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/amitizle/muffin/internal/logger"
"github.com/mitchellh/mapstructure"
"github.com/rs/zerolog"
)
// TODO
// * Use jsonpath notation to verify status based on JSON path in the response (https://github.com/tidwall/gjson)
// * on all config, use better option method, i.e sending an `Option` type to the new struct creation.
// * Support configuring headers (such as auth headers)
// HTTPCheck is a struct that defines the HTTP check.
// It holds the HTTP client and an HTTPCheckConfig struct.
type HTTPCheck struct {
client *http.Client
config *HTTPCheckConfig
ctx context.Context
logger zerolog.Logger
}
// HTTPCheckConfig is a struct that holds the configuration required for the HTTP check.
// It is populated with `mapstructure` and holds some private fields that suppose to
// hold a parsed/verified version of the configuration input.
type HTTPCheckConfig struct {
URL string `mapstructure:"url"`
Method string `mapstructure:"method"`
Payload []byte `mapstructure:"payload"`
ErrorHTTPStatusCodes []int `mapstructure:"error_http_status_codes"`
// private fields
errorHTTPStatusCodesMap map[int]bool
parsedURL *url.URL
}
// useDefaultErrorCodes populated an HTTPCheckConfig's HTTP error codes
// with default ones (400 - 599)
func (checkConfig *HTTPCheckConfig) useDefaultErrorCodes() {
for i := 400; i < 600; i++ {
if http.StatusText(i) != "" {
checkConfig.errorHTTPStatusCodesMap[i] = true
}
}
}
// Initialize initializing an HTTP client for the HTTPCheck.
func (check *HTTPCheck) Initialize(ctx context.Context) error {
check.client = http.DefaultClient
check.ctx = ctx
lg, err := logger.GetContext(ctx)
if err != nil {
return err
}
check.logger = lg
return nil
}
// Configure decodes map[string]interface{} to an HTTPCheckConfig struct instance.
// It does so using `mapstructure`.
// After decoding, it configures some default values in case they were not given in
// the configuration.
func (check *HTTPCheck) Configure(config map[string]interface{}) error {
httpConfig := &HTTPCheckConfig{
errorHTTPStatusCodesMap: map[int]bool{},
}
if err := mapstructure.Decode(config, httpConfig); err != nil {
return err
}
u, err := url.ParseRequestURI(httpConfig.URL)
if err != nil {
return err
}
httpConfig.parsedURL = u
if httpConfig.ErrorHTTPStatusCodes != nil {
for _, errStatusCode := range httpConfig.ErrorHTTPStatusCodes {
httpConfig.errorHTTPStatusCodesMap[errStatusCode] = true
}
} else {
httpConfig.useDefaultErrorCodes()
}
if httpConfig.Method == "" {
httpConfig.Method = http.MethodHead
}
if httpConfig.Payload == nil {
httpConfig.Payload = []byte{}
}
check.config = httpConfig
return nil
}
// Run runs the HTTP check
func (check *HTTPCheck) Run() ([]byte, error) {
check.logger.Debug().Msg("running check")
req, err := http.NewRequest(check.config.Method, check.config.parsedURL.String(), bytes.NewBuffer(check.config.Payload))
if err != nil {
check.logger.Error().Err(err).Msg("check encountered an error")
return []byte{}, check.wrapError(err)
}
resp, err := check.client.Do(req)
// if resp is not nil it means that the HTTP request failed, however the
// check itself should be reporting an error, thus we won't return nil
if err != nil && resp == nil {
check.logger.Error().Err(err).Msg("check encountered an error")
return []byte{}, check.wrapError(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
check.logger.Error().Err(err).Msg("check encountered an error")
return []byte{}, check.wrapError(err)
}
if check.config.errorHTTPStatusCodesMap[resp.StatusCode] {
return body, check.wrapError(errors.New(resp.Status))
}
return body, nil
}
// GetFullURL returns the string represantation of the check's URL
func (check *HTTPCheck) GetFullURL() string {
return check.config.parsedURL.String()
}
func (check *HTTPCheck) wrapError(err error) error {
return fmt.Errorf("HTTP check failed to %s: %s", check.config.parsedURL.String(), err)
}