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