diff options
Diffstat (limited to 'src/go/collectors/go.d.plugin/modules/httpcheck/collect.go')
-rw-r--r-- | src/go/collectors/go.d.plugin/modules/httpcheck/collect.go | 189 |
1 files changed, 189 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/httpcheck/collect.go b/src/go/collectors/go.d.plugin/modules/httpcheck/collect.go new file mode 100644 index 000000000..8d88dc02f --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/httpcheck/collect.go @@ -0,0 +1,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)) +} |