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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
|
// SPDX-License-Identifier: GPL-3.0-or-later
package httpcheck
import (
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"strings"
"time"
"github.com/netdata/netdata/go/go.d.plugin/pkg/stm"
"github.com/netdata/netdata/go/go.d.plugin/pkg/web"
)
type reqErrCode int
const (
codeTimeout reqErrCode = iota
codeRedirect
codeNoConnection
)
func (hc *HTTPCheck) collect() (map[string]int64, error) {
req, err := web.NewHTTPRequest(hc.Request)
if err != nil {
return nil, fmt.Errorf("error on creating HTTP requests to %s : %v", hc.Request.URL, err)
}
if hc.CookieFile != "" {
if err := hc.readCookieFile(); err != nil {
return nil, fmt.Errorf("error on reading cookie file '%s': %v", hc.CookieFile, err)
}
}
start := time.Now()
resp, err := hc.httpClient.Do(req)
dur := time.Since(start)
defer closeBody(resp)
var mx metrics
if hc.isError(err, resp) {
hc.Debug(err)
hc.collectErrResponse(&mx, err)
} else {
mx.ResponseTime = durationToMs(dur)
hc.collectOKResponse(&mx, resp)
}
if hc.metrics.Status != mx.Status {
mx.InState = hc.UpdateEvery
} else {
mx.InState = hc.metrics.InState + hc.UpdateEvery
}
hc.metrics = mx
return stm.ToMap(mx), nil
}
func (hc *HTTPCheck) isError(err error, resp *http.Response) bool {
return err != nil && !(errors.Is(err, web.ErrRedirectAttempted) && hc.acceptedStatuses[resp.StatusCode])
}
func (hc *HTTPCheck) collectErrResponse(mx *metrics, err error) {
switch code := decodeReqError(err); code {
case codeNoConnection:
mx.Status.NoConnection = true
case codeTimeout:
mx.Status.Timeout = true
case codeRedirect:
mx.Status.Redirect = true
default:
panic(fmt.Sprintf("unknown request error code : %d", code))
}
}
func (hc *HTTPCheck) collectOKResponse(mx *metrics, resp *http.Response) {
hc.Debugf("endpoint '%s' returned %d (%s) HTTP status code", hc.URL, resp.StatusCode, resp.Status)
if !hc.acceptedStatuses[resp.StatusCode] {
mx.Status.BadStatusCode = true
return
}
bs, err := io.ReadAll(resp.Body)
// golang net/http closes body on redirect
if err != nil && !errors.Is(err, io.EOF) && !strings.Contains(err.Error(), "read on closed response body") {
hc.Warningf("error on reading body : %v", err)
mx.Status.BadContent = true
return
}
mx.ResponseLength = len(bs)
if hc.reResponse != nil && !hc.reResponse.Match(bs) {
mx.Status.BadContent = true
return
}
if ok := hc.checkHeader(resp); !ok {
mx.Status.BadHeader = true
return
}
mx.Status.Success = true
}
func (hc *HTTPCheck) checkHeader(resp *http.Response) bool {
for _, m := range hc.headerMatch {
value := resp.Header.Get(m.key)
var ok bool
switch {
case value == "":
ok = m.exclude
case m.valMatcher == nil:
ok = !m.exclude
default:
ok = m.valMatcher.MatchString(value)
}
if !ok {
hc.Debugf("header match: bad header: exlude '%v' key '%s' value '%s'", m.exclude, m.key, value)
return false
}
}
return true
}
func decodeReqError(err error) reqErrCode {
if err == nil {
panic("nil error")
}
if errors.Is(err, web.ErrRedirectAttempted) {
return codeRedirect
}
var v net.Error
if errors.As(err, &v) && v.Timeout() {
return codeTimeout
}
return codeNoConnection
}
func (hc *HTTPCheck) readCookieFile() error {
if hc.CookieFile == "" {
return nil
}
fi, err := os.Stat(hc.CookieFile)
if err != nil {
return err
}
if hc.cookieFileModTime.Equal(fi.ModTime()) {
hc.Debugf("cookie file '%s' modification time has not changed, using previously read data", hc.CookieFile)
return nil
}
hc.Debugf("reading cookie file '%s'", hc.CookieFile)
jar, err := loadCookieJar(hc.CookieFile)
if err != nil {
return err
}
hc.httpClient.Jar = jar
hc.cookieFileModTime = fi.ModTime()
return nil
}
func closeBody(resp *http.Response) {
if resp == nil || resp.Body == nil {
return
}
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}
func durationToMs(duration time.Duration) int {
return int(duration) / (int(time.Millisecond) / int(time.Nanosecond))
}
|