Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 228 lines 6.7 kB view raw
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}