firehol/netdata

View on GitHub
src/go/plugin/go.d/modules/httpcheck/collect.go

Summary

Maintainability
A
35 mins
Test Coverage
// 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/plugins/plugin/go.d/pkg/stm"
    "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
)

type reqErrCode int

const (
    codeTimeout reqErrCode = iota
    codeRedirect
    codeNoConnection
)

func (hc *HTTPCheck) collect() (map[string]int64, error) {
    req, err := web.NewHTTPRequest(hc.RequestConfig)
    if err != nil {
        return nil, fmt.Errorf("error on creating HTTP requests to %s : %v", hc.RequestConfig.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 web.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 durationToMs(duration time.Duration) int {
    return int(duration) / (int(time.Millisecond) / int(time.Nanosecond))
}