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  }