summaryrefslogtreecommitdiffstats
path: root/src/go/collectors/go.d.plugin/modules/httpcheck/collect.go
blob: 8d88dc02f84b0f10b41e75d270293c99bee3ef56 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
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))
}