-
Notifications
You must be signed in to change notification settings - Fork 19
/
errors.go
220 lines (191 loc) · 5.07 KB
/
errors.go
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
package v2
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"github.com/grafana/synthetic-monitoring-agent/internal/pkg/prom"
)
// errKind enum used to carry the category of a push error.
type errKind int8
const (
errKindNoError errKind = iota // No error (never returned, always nil error).
errKindNetwork // Transient network error or other retriable error
errKindPayload // There is a problem with the data being sent. Discard it.
errKindWait // Sending too much data, delay publishing
errKindTenant // A problem with the tenant remotes. Fetch the tenant again.
errKindFatal // There is a problem that can't be fixed by fetching the tenant.
errKindTerminated // Push terminated (context canceled)
)
func (k errKind) String() string {
switch k {
case errKindNoError:
return "no error"
case errKindNetwork:
return "network error"
case errKindPayload:
return "payload error"
case errKindWait:
return "waitable error"
case errKindTenant:
return "tenant error"
case errKindFatal:
return "fatal error"
case errKindTerminated:
return "terminate error"
}
return "unknown error"
}
// pushError encapsulates an existing error with an errKind
type pushError struct {
kind errKind
inner error
}
func (e pushError) Error() string {
return fmt.Sprintf("%s: %s", e.kind.String(), e.inner)
}
func (e pushError) Unwrap() error {
return e.inner
}
func (e pushError) Kind() errKind {
return e.kind
}
func (e pushError) IsRetriable() bool {
return e.kind == errKindNetwork
}
type alternativeMapping struct {
substr string
kind errKind
}
// httpCodeMappings maps an HTTP Response Code to an errKind.
// Alternative mappings can be provided by inspecting the error returned by the server.
var httpCodeMappings = map[int]struct {
kind errKind
alternatives []alternativeMapping
}{
1: { // 1xx: retriable error
kind: errKindNetwork,
},
2: { // 2xx: No error
kind: errKindNoError,
},
3: { // 3xx: These usually indicate a misconfiguration (tenant is pointing to the wrong url?)
kind: errKindFatal,
},
4: { // 4xx: Not an error. Just part of the payload is unacceptable.
kind: errKindPayload,
},
5: { // 5xx: Transient error.
kind: errKindNetwork,
},
http.StatusInternalServerError: { // 500
kind: errKindNetwork,
alternatives: []alternativeMapping{
{
substr: "looks like there is an issue with this instance",
kind: errKindTenant,
},
},
},
http.StatusBadRequest: { // 400
kind: errKindPayload,
alternatives: []alternativeMapping{
{
substr: "err-mimir-max-series-per-user",
kind: errKindFatal,
},
},
},
http.StatusTooManyRequests: { // 429
kind: errKindWait,
alternatives: []alternativeMapping{
{
substr: "limit: 0 ",
kind: errKindFatal,
},
{
substr: "Maximum active stream limit exceeded",
kind: errKindFatal,
},
},
},
// Specific 4xx messages that don't translate to data error
http.StatusUnauthorized: { // 401
kind: errKindTenant,
},
http.StatusForbidden: { // 403
kind: errKindFatal,
},
http.StatusNotFound: { // 404
kind: errKindFatal,
},
http.StatusMethodNotAllowed: { // 405
kind: errKindFatal,
},
http.StatusRequestTimeout: { // 408
kind: errKindNetwork,
},
}
// parsePublishError parses the error resulting from a publish operation and converts it into a pushError.
// The only exception is any error that wraps a context.Canceled error. In that case, context.Canceled
// is returned.
func parsePublishError(err error) (httpStatusCode int, pushErr pushError) {
const noHTTPCode = 0
if err == nil {
return http.StatusOK, pushError{
kind: errKindNoError,
inner: nil,
}
}
// Context errors can be wrapped by various other error types, like
// prom.recoverableError and url.Error.
if errors.Is(err, context.Canceled) {
return noHTTPCode, pushError{
kind: errKindTerminated,
inner: context.Canceled,
}
}
// Any DeadlineExceeded is assumed to be a network timeout of some kind.
if errors.Is(err, context.DeadlineExceeded) {
return noHTTPCode, pushError{
kind: errKindNetwork,
inner: err,
}
}
code, hasStatusCode := prom.GetHttpStatusCode(err)
if !hasStatusCode {
// Errors without an HTTP Status code are treated as network errors.
return noHTTPCode, pushError{
kind: errKindNetwork,
inner: err,
}
}
mapping, found := httpCodeMappings[code]
if !found {
// No mapping for this specific HTTP status code. Try a general 5xx/4xx/etc.
if mapping, found = httpCodeMappings[code/100]; !found {
// No mapping for this http status at all?
// This should never happen.
return code, pushError{
kind: errKindFatal,
inner: err,
}
}
}
// Check specific alternatives that look into the error message.
errText := err.Error()
for _, alt := range mapping.alternatives {
if strings.Contains(errText, alt.substr) {
return code, pushError{
kind: alt.kind,
inner: err,
}
}
}
// return base mapping for this status code.
return code, pushError{
kind: mapping.kind,
inner: err,
}
}