github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/pusher/v2/errors.go (about) 1 package v2 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net/http" 8 "strings" 9 10 "github.com/grafana/synthetic-monitoring-agent/internal/pkg/prom" 11 ) 12 13 // errKind enum used to carry the category of a push error. 14 type errKind int8 15 16 const ( 17 errKindNoError errKind = iota // No error (never returned, always nil error). 18 errKindNetwork // Transient network error or other retriable error 19 errKindPayload // There is a problem with the data being sent. Discard it. 20 errKindWait // Sending too much data, delay publishing 21 errKindTenant // A problem with the tenant remotes. Fetch the tenant again. 22 errKindFatal // There is a problem that can't be fixed by fetching the tenant. 23 errKindTerminated // Push terminated (context canceled) 24 errKindLimit // There is a problem with the limits of the tenant. Discard it. 25 ) 26 27 func (k errKind) String() string { 28 switch k { 29 case errKindNoError: 30 return "no error" 31 case errKindNetwork: 32 return "network error" 33 case errKindPayload: 34 return "payload error" 35 case errKindWait: 36 return "waitable error" 37 case errKindTenant: 38 return "tenant error" 39 case errKindFatal: 40 return "fatal error" 41 case errKindTerminated: 42 return "terminate error" 43 case errKindLimit: 44 return "limit error" 45 } 46 47 return "unknown error" 48 } 49 50 // pushError encapsulates an existing error with an errKind 51 type pushError struct { 52 kind errKind 53 inner error 54 } 55 56 func (e pushError) Error() string { 57 return fmt.Sprintf("%s: %s", e.kind.String(), e.inner) 58 } 59 60 func (e pushError) Unwrap() error { 61 return e.inner 62 } 63 64 func (e pushError) Kind() errKind { 65 return e.kind 66 } 67 68 func (e pushError) IsRetriable() bool { 69 return e.kind == errKindNetwork 70 } 71 72 type alternativeMapping struct { 73 substr string 74 kind errKind 75 } 76 77 // httpCodeMappings maps an HTTP Response Code to an errKind. 78 // Alternative mappings can be provided by inspecting the error returned by the server. 79 var httpCodeMappings = map[int]struct { 80 kind errKind 81 alternatives []alternativeMapping 82 }{ 83 1: { // 1xx: retriable error 84 kind: errKindNetwork, 85 }, 86 2: { // 2xx: No error 87 kind: errKindNoError, 88 }, 89 3: { // 3xx: These usually indicate a misconfiguration (tenant is pointing to the wrong url?) 90 kind: errKindFatal, 91 }, 92 4: { // 4xx: Not an error. Just part of the payload is unacceptable. 93 kind: errKindPayload, 94 }, 95 5: { // 5xx: Transient error. 96 kind: errKindNetwork, 97 }, 98 99 http.StatusInternalServerError: { // 500 100 kind: errKindNetwork, 101 alternatives: []alternativeMapping{ 102 { 103 substr: "looks like there is an issue with this instance", 104 kind: errKindTenant, 105 }, 106 }, 107 }, 108 109 http.StatusBadRequest: { // 400 110 kind: errKindPayload, 111 alternatives: []alternativeMapping{ 112 { 113 substr: "err-mimir-max-series-per-user", 114 kind: errKindFatal, 115 }, 116 }, 117 }, 118 119 http.StatusTooManyRequests: { // 429 120 kind: errKindWait, 121 alternatives: []alternativeMapping{ 122 { 123 substr: "limit: 0 ", 124 kind: errKindFatal, 125 }, 126 { 127 // We are hitting a limit in the maximum number of active streams allowed by Loki. 128 // There's nothing we can do about it other than reporting it. Because of the 129 // way Loki works, it's possible that we hit this limit once and don't hit it the next 130 // time we try to publish data. 131 substr: "Maximum active stream limit exceeded", 132 kind: errKindLimit, 133 }, 134 }, 135 }, 136 137 // Specific 4xx messages that don't translate to data error 138 139 http.StatusUnauthorized: { // 401 140 kind: errKindTenant, 141 }, 142 143 http.StatusForbidden: { // 403 144 kind: errKindFatal, 145 }, 146 147 http.StatusNotFound: { // 404 148 kind: errKindFatal, 149 }, 150 151 http.StatusMethodNotAllowed: { // 405 152 kind: errKindFatal, 153 }, 154 155 http.StatusRequestTimeout: { // 408 156 kind: errKindNetwork, 157 }, 158 } 159 160 // parsePublishError parses the error resulting from a publish operation and converts it into a pushError. 161 // The only exception is any error that wraps a context.Canceled error. In that case, context.Canceled 162 // is returned. 163 func parsePublishError(err error) (httpStatusCode int, pushErr pushError) { 164 const noHTTPCode = 0 165 166 if err == nil { 167 return http.StatusOK, pushError{ 168 kind: errKindNoError, 169 inner: nil, 170 } 171 } 172 173 // Context errors can be wrapped by various other error types, like 174 // prom.recoverableError and url.Error. 175 if errors.Is(err, context.Canceled) { 176 return noHTTPCode, pushError{ 177 kind: errKindTerminated, 178 inner: context.Canceled, 179 } 180 } 181 182 // Any DeadlineExceeded is assumed to be a network timeout of some kind. 183 if errors.Is(err, context.DeadlineExceeded) { 184 return noHTTPCode, pushError{ 185 kind: errKindNetwork, 186 inner: err, 187 } 188 } 189 190 code, hasStatusCode := prom.GetHttpStatusCode(err) 191 if !hasStatusCode { 192 // Errors without an HTTP Status code are treated as network errors. 193 return noHTTPCode, pushError{ 194 kind: errKindNetwork, 195 inner: err, 196 } 197 } 198 199 mapping, found := httpCodeMappings[code] 200 if !found { 201 // No mapping for this specific HTTP status code. Try a general 5xx/4xx/etc. 202 if mapping, found = httpCodeMappings[code/100]; !found { 203 // No mapping for this http status at all? 204 // This should never happen. 205 return code, pushError{ 206 kind: errKindFatal, 207 inner: err, 208 } 209 } 210 } 211 212 // Check specific alternatives that look into the error message. 213 errText := err.Error() 214 for _, alt := range mapping.alternatives { 215 if strings.Contains(errText, alt.substr) { 216 return code, pushError{ 217 kind: alt.kind, 218 inner: err, 219 } 220 } 221 } 222 223 // return base mapping for this status code. 224 return code, pushError{ 225 kind: mapping.kind, 226 inner: err, 227 } 228 }