Openstatus
www.openstatus.dev
1package job
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "net/http"
8 "time"
9
10 "github.com/cenkalti/backoff/v5"
11 "github.com/google/uuid"
12 "github.com/openstatushq/openstatus/apps/checker/checker"
13 "github.com/openstatushq/openstatus/apps/checker/pkg/assertions"
14 v1 "github.com/openstatushq/openstatus/apps/checker/proto/private_location/v1"
15 "github.com/openstatushq/openstatus/apps/checker/request"
16)
17
18func ProtoNumberAssertionToComparator(assertion v1.NumberComparator) (request.NumberComparator, error) {
19 switch assertion {
20 case v1.NumberComparator_NUMBER_COMPARATOR_EQUAL:
21 return request.NumberEquals, nil
22 case v1.NumberComparator_NUMBER_COMPARATOR_NOT_EQUAL:
23 return request.NumberNotEquals, nil
24 case v1.NumberComparator_NUMBER_COMPARATOR_GREATER_THAN:
25 return request.NumberGreaterThan, nil
26 case v1.NumberComparator_NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL:
27 return request.NumberGreaterThanEqual, nil
28 case v1.NumberComparator_NUMBER_COMPARATOR_LESS_THAN:
29 return request.NumberLowerThan, nil
30 case v1.NumberComparator_NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL:
31 return request.NumberLowerThanEqual, nil
32 default:
33
34 }
35 return "", fmt.Errorf("unknown comparator type: %v", assertion)
36}
37
38func ProtoStringAssertionToComparator(assertion v1.StringComparator) (request.StringComparator, error) {
39 switch assertion {
40 case v1.StringComparator_STRING_COMPARATOR_CONTAINS:
41 return request.StringContains, nil
42 case v1.StringComparator_STRING_COMPARATOR_NOT_CONTAINS:
43 return request.StringNotContains, nil
44 case v1.StringComparator_STRING_COMPARATOR_EQUAL:
45 return request.StringEquals, nil
46 case v1.StringComparator_STRING_COMPARATOR_NOT_EQUAL:
47 return request.StringNotEquals, nil
48 case v1.StringComparator_STRING_COMPARATOR_EMPTY:
49 return request.StringEmpty, nil
50 case v1.StringComparator_STRING_COMPARATOR_NOT_EMPTY:
51 return request.StringNotEmpty, nil
52 }
53 return "", fmt.Errorf("unknown comparator type: %v", assertion)
54}
55
56func (jr jobRunner) HTTPJob(ctx context.Context, monitor *v1.HTTPMonitor) (*HttpPrivateRegionData, error) {
57
58 retry := monitor.Retry
59 if retry == 0 {
60 retry = 3
61 }
62
63 requestClient := &http.Client{
64 Timeout: time.Duration(monitor.Timeout) * time.Millisecond,
65 }
66 defer requestClient.CloseIdleConnections()
67
68 if !monitor.FollowRedirects {
69 requestClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
70 return http.ErrUseLastResponse
71 }
72 } else {
73 requestClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
74 if len(via) >= 10 {
75 return http.ErrUseLastResponse
76 }
77 return nil
78 }
79 }
80
81 var degradedAfter int64
82 if monitor.DegradedAt != nil {
83 degradedAfter = *monitor.DegradedAt
84 }
85
86 headers := make([]struct {
87 Key string `json:"key"`
88 Value string `json:"value"`
89 }, 0)
90 if monitor.Headers != nil {
91 for _, header := range monitor.Headers {
92 headers = append(headers, struct {
93 Key string `json:"key"`
94 Value string `json:"value"`
95 }{
96 Key: header.Key,
97 Value: header.Value,
98 })
99 }
100 }
101
102 req := request.HttpCheckerRequest{
103 URL: monitor.Url,
104 MonitorID: monitor.Id,
105 Method: monitor.Method,
106 Body: monitor.Body,
107 Retry: monitor.Retry,
108 Timeout: monitor.Timeout,
109 DegradedAfter: degradedAfter,
110 FollowRedirects: monitor.FollowRedirects,
111 Headers: headers,
112 }
113
114 var called int
115
116 op := func() (*HttpPrivateRegionData, error) {
117 called++
118 res, err := checker.Http(ctx, requestClient, req)
119 if err != nil {
120 return nil, fmt.Errorf("unable to ping: %w", err)
121 }
122
123 timingBytes, err := json.Marshal(res.Timing)
124 if err != nil {
125 return nil, fmt.Errorf("error while parsing timing data %s: %w", req.URL, err)
126 }
127 headersBytes, err := json.Marshal(res.Headers)
128 if err != nil {
129 return nil, fmt.Errorf("error while parsing headers %s: %w", req.URL, err)
130 }
131 id, err := uuid.NewV7()
132 if err != nil {
133 return nil, fmt.Errorf("error while generating uuid: %w", err)
134 }
135
136 status := statusCode(res.Status)
137 isSuccessful := status.IsSuccessful()
138 if len(monitor.HeaderAssertions) > 0 {
139 headersAsString, err := json.Marshal(res.Headers)
140 if err != nil {
141 return nil, fmt.Errorf("error while parsing headers %s: %w", req.URL, err)
142 }
143 for _, assertion := range monitor.HeaderAssertions {
144
145 a, err := ProtoStringAssertionToComparator(assertion.Comparator)
146 if err != nil {
147 return nil, fmt.Errorf("error while parsing header assertion comparator: %w", err)
148 }
149 assert := assertions.HeaderTarget{
150 Comparator: a,
151 Target: assertion.Target,
152 Key: assertion.Key,
153 }
154 assert.HeaderEvaluate(string(headersAsString))
155 }
156 }
157
158 if len(monitor.StatusCodeAssertions) > 0 {
159 for _, assertion := range monitor.StatusCodeAssertions {
160 a, err := ProtoNumberAssertionToComparator(assertion.Comparator)
161 if err != nil {
162 return nil, fmt.Errorf("error while parsing header assertion comparator: %w", err)
163 }
164
165 assert := assertions.StatusTarget{
166 Comparator: a,
167 Target: assertion.Target,
168 }
169 isSuccessful = isSuccessful && assert.StatusEvaluate(int64(res.Status))
170 }
171 }
172 if len(monitor.BodyAssertions) > 0 {
173 for _, assertion := range monitor.BodyAssertions {
174 a, err := ProtoStringAssertionToComparator(assertion.Comparator)
175 if err != nil {
176 return nil, fmt.Errorf("error while parsing header assertion comparator: %w", err)
177 }
178 assert := assertions.StringTargetType{
179 Comparator: a,
180 Target: assertion.Target,
181 }
182 isSuccessful = isSuccessful && assert.StringEvaluate(res.Body)
183 }
184 }
185
186 requestStatus := "success"
187 if !isSuccessful {
188 requestStatus = "error"
189 } else if req.DegradedAfter > 0 && res.Latency > req.DegradedAfter {
190 requestStatus = "degraded"
191 }
192
193 data := HttpPrivateRegionData{
194 ID: id.String(),
195 Latency: res.Latency,
196 StatusCode: res.Status,
197 Timestamp: res.Timestamp,
198 CronTimestamp: res.Timestamp,
199 URL: req.URL,
200 // Method: req.Method,
201 Timing: string(timingBytes),
202 Headers: string(headersBytes),
203 Body: "",
204 RequestStatus: requestStatus,
205 // Assertions: assertionAsString,
206 Error: 0,
207 }
208
209 if isSuccessful {
210 if req.DegradedAfter != 0 && res.Latency > req.DegradedAfter {
211 data.Body = res.Body
212 }
213 } else {
214 data.Error = 1
215 if called < int(retry) {
216 return nil, fmt.Errorf("unable to ping: %v with status %v", res, res.Status)
217 }
218 }
219
220 return &data, nil
221 }
222
223 resp, err := backoff.Retry(ctx, op, backoff.WithMaxTries(uint(retry)), backoff.WithBackOff(backoff.NewExponentialBackOff()))
224 if err != nil {
225 return nil, err
226 }
227 return resp, nil
228}