master
go 196 lines 4.15 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package httpcheck
4
5 import (
6 "errors"
7 "fmt"
8 "io"
9 "log/slog"
10 "net"
11 "net/http"
12 "os"
13 "strings"
14 "time"
15
16 "github.com/netdata/netdata/go/plugins/logger"
17 "github.com/netdata/netdata/go/plugins/pkg/stm"
18 "github.com/netdata/netdata/go/plugins/pkg/web"
19 )
20
21 type reqErrCode int
22
23 const (
24 codeTimeout reqErrCode = iota
25 codeRedirect
26 codeNoConnection
27 )
28
29 func (c *Collector) collect() (map[string]int64, error) {
30 req, err := web.NewHTTPRequest(c.RequestConfig)
31 if err != nil {
32 return nil, fmt.Errorf("error on creating HTTP requests to %s : %v", c.RequestConfig.URL, err)
33 }
34
35 if c.CookieFile != "" {
36 if err := c.readCookieFile(); err != nil {
37 return nil, fmt.Errorf("error on reading cookie file '%s': %v", c.CookieFile, err)
38 }
39 }
40
41 start := time.Now()
42 resp, err := c.httpClient.Do(req)
43 dur := time.Since(start)
44
45 defer web.CloseBody(resp)
46
47 var mx metrics
48
49 if c.isError(err, resp) {
50 c.Debug(err)
51 c.collectErrResponse(&mx, err)
52 } else {
53 mx.ResponseTime = durationToMs(dur)
54 c.collectOKResponse(&mx, resp)
55 }
56
57 if c.metrics.Status != mx.Status {
58 mx.InState = c.UpdateEvery
59 } else {
60 mx.InState = c.metrics.InState + c.UpdateEvery
61 }
62 c.metrics = mx
63
64 return stm.ToMap(mx), nil
65 }
66
67 func (c *Collector) isError(err error, resp *http.Response) bool {
68 return err != nil && !(errors.Is(err, web.ErrRedirectAttempted) && c.acceptedStatuses[resp.StatusCode])
69 }
70
71 func (c *Collector) collectErrResponse(mx *metrics, err error) {
72 switch code := decodeReqError(err); code {
73 case codeNoConnection:
74 mx.Status.NoConnection = true
75 case codeTimeout:
76 mx.Status.Timeout = true
77 case codeRedirect:
78 mx.Status.Redirect = true
79 default:
80 panic(fmt.Sprintf("unknown request error code : %d", code))
81 }
82 }
83
84 func (c *Collector) collectOKResponse(mx *metrics, resp *http.Response) {
85 c.Debugf("endpoint '%s' returned %d (%s) HTTP status code", c.URL, resp.StatusCode, resp.Status)
86
87 if !c.acceptedStatuses[resp.StatusCode] {
88 mx.Status.BadStatusCode = true
89 return
90 }
91
92 bs, err := io.ReadAll(resp.Body)
93 // golang net/http closes body on redirect
94 if err != nil && !errors.Is(err, io.EOF) && !strings.Contains(err.Error(), "read on closed response body") {
95 c.Warningf("error on reading body : %v", err)
96 mx.Status.BadContent = true
97 return
98 }
99
100 mx.ResponseLength = len(bs)
101
102 if c.reResponse != nil {
103 matched := c.reResponse.Match(bs)
104
105 if logger.Level.Enabled(slog.LevelDebug) {
106 c.Debugf("response validation: pattern=%s, matched=%v, bodySize=%d", c.reResponse, matched, len(bs))
107 if len(bs) <= 1024 {
108 c.Debugf("response body: %s", string(bs))
109 } else {
110 c.Debugf("response body (first 1024 bytes): %s...", string(bs[:1024]))
111 }
112 }
113
114 if !matched {
115 mx.Status.BadContent = true
116 return
117 }
118 }
119
120 if ok := c.checkHeader(resp); !ok {
121 mx.Status.BadHeader = true
122 return
123 }
124
125 mx.Status.Success = true
126 }
127
128 func (c *Collector) checkHeader(resp *http.Response) bool {
129 for _, m := range c.headerMatch {
130 value := resp.Header.Get(m.key)
131
132 var ok bool
133 switch {
134 case value == "":
135 ok = m.exclude
136 case m.valMatcher == nil:
137 ok = !m.exclude
138 default:
139 ok = m.valMatcher.MatchString(value)
140 }
141
142 if !ok {
143 c.Debugf("header match: bad header: exlude '%v' key '%s' value '%s'", m.exclude, m.key, value)
144 return false
145 }
146 }
147
148 return true
149 }
150
151 func decodeReqError(err error) reqErrCode {
152 if err == nil {
153 panic("nil error")
154 }
155
156 if errors.Is(err, web.ErrRedirectAttempted) {
157 return codeRedirect
158 }
159 var v net.Error
160 if errors.As(err, &v) && v.Timeout() {
161 return codeTimeout
162 }
163 return codeNoConnection
164 }
165
166 func (c *Collector) readCookieFile() error {
167 if c.CookieFile == "" {
168 return nil
169 }
170
171 fi, err := os.Stat(c.CookieFile)
172 if err != nil {
173 return err
174 }
175
176 if c.cookieFileModTime.Equal(fi.ModTime()) {
177 c.Debugf("cookie file '%s' modification time has not changed, using previously read data", c.CookieFile)
178 return nil
179 }
180
181 c.Debugf("reading cookie file '%s'", c.CookieFile)
182
183 jar, err := loadCookieJar(c.CookieFile)
184 if err != nil {
185 return err
186 }
187
188 c.httpClient.Jar = jar
189 c.cookieFileModTime = fi.ModTime()
190
191 return nil
192 }
193
194 func durationToMs(duration time.Duration) int {
195 return int(duration) / (int(time.Millisecond) / int(time.Nanosecond))
196 }