-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathformatter.go
262 lines (225 loc) · 8.67 KB
/
formatter.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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
package gerrors
import (
"fmt"
"regexp"
"text/template"
)
const (
defaultTemplate = "error: {{.Identifier}}({{.ErrorCode}}) - {{.Message}}"
// missingValueReplacement is the value that will be used to replace
// invalid provided values for labels if replacing these missing values
// are allowed.
missingValueReplacement = "no-value"
// keyMaxLength is the upper limit for the length of label key.
// This is to prevent the key from being too long and to follow
// errordetails protocol buffer message standard.
keyMaxLength = 64
)
var (
// DefaultFormatter holds a ready to use formatter with the default options.
// DefaultFormatter uses the default template with no pre-defined labels and
// no logger. It replaces invalid values with the missingValueReplacement.
// DefaultFormatter uses defaultCoreCallback function for translating error
// codes to human and machine readable strings and information.
DefaultFormatter = NewFormatter()
// keyRE is the regular expression to enforce label keys to have
// alphanumerical characters, dashes and underscores.
// This is to follow errordetails protocol buffer message standard.
keyRE = regexp.MustCompile(`[^a-zA-Z0-9-_]`)
)
// Formatter is the main component of this package which provide
// handlers to create new errors. Formatter controls how every error
// will be parsed and formatted.
// Errors generated by a single formatter, will have a certain set of
// similar behaviors. These behaviors can be customized while creating
// a new formatter.
type Formatter struct {
logger logger
labels map[string]string
template *template.Template
allowMissingValue bool
missingValueReplacement string
coreDataLookup Lookuper
}
// FormatterOption is the approach for customizing the formatter.
// Every option used during creation of a new formatter is of this type.
// Most of the functions starting with "With" are helpers to customize the formatter.
type FormatterOption func(*Formatter)
// NewFormatter creates a new formatter with the default options.
// Check [DefaultFormatter] for more information on the default options.
// It accepts a variadic number of FormatterOptions for customizing the returned
// formatter. Check helper functions that returns [FormatterOption] for more information.
func NewFormatter(opts ...FormatterOption) *Formatter {
tpl, err := template.New("gerror").Parse(defaultTemplate)
if err != nil {
panic(err)
}
defaultLookuper := NewMapper(Unknown, GetDefaultMapping())
f := &Formatter{
labels: make(map[string]string),
template: tpl,
allowMissingValue: true,
missingValueReplacement: missingValueReplacement,
coreDataLookup: defaultLookuper,
logger: nil,
}
for _, opt := range opts {
opt(f)
}
return f
}
// WithTemplate customizes formatter defaultTemplate.
// This template should follow text/template syntax. Function panics if template is invalid.
// Supported variables are:
//
// - {{.Identifier}}: the identifier of the error. (e.g. unavailable, internal, ...)
//
// - {{.ErrorCode}}: core error code. (e.g. 1, 2, ...)
//
// - {{.GrpcErrorCode}}: grpc error code. (e.g. 2, 5, ...)
//
// - {{.Message}}: the message of the provided error or default message of the error code.
//
// - {{.DefaultMessage}}: the default message of the error code.
//
// - {{.Labels}}: formatter's label plus error-specific labels. Treat it as a map.
//
// f := NewFormatter(WithTemplate("error: {{.Identifier}}(code {{.ErrorCode}}) - {{.Message}}"))
func WithTemplate(templateString string) FormatterOption {
tpl, err := template.New("gerror").Parse(templateString)
if err != nil {
panic(err)
}
return func(f *Formatter) {
f.template = tpl
}
}
// WithLogger attach a logger to the formatter.
// provided logger should at least implement the [logger] interface.
// If the provide logger implements other type of loggers as well
// (e.g [infoLogger], [traceLogger], ...), we can control how the created error
// should be logged by the formatter.
// If formatter is not configured with a logger or if the logger does not
// implement the provided logger, formatter simply ignore logging the error.
func WithLogger(logger logger) FormatterOption {
return func(f *Formatter) {
f.logger = logger
}
}
// WithLookuper customizes the default mapper that translates [Code] to [CoreError].
// Using this formatter option, it is possible to completely revamp all the Code
// constants from acceptable error code and define a new set of error codes and
// a new mapper that can translate these new error codes to more details.
func WithLookuper(l Lookuper) FormatterOption {
return func(f *Formatter) {
f.coreDataLookup = l
}
}
// WithMissingValueReplacement allows the formatter to accepts missing values
// when labels are being added to the formatter. In case a value is missing,
// or is invalid, formatter will replace it with this replacement string.
// Replacing missing values is allowed by default.
func WithMissingValueReplacement(replacement string) FormatterOption {
return func(f *Formatter) {
f.allowMissingValue = true
f.missingValueReplacement = replacement
}
}
// WithDisabledMissingValueReplacement allows user to disallow replacing missing values.
// In this case, a label with missing or invalid value will be ignored and not added to
// the error.
func WithDisabledMissingValueReplacement() FormatterOption {
return func(f *Formatter) {
f.allowMissingValue = false
}
}
// WithLabels add a set of default labels to the formatter.
// All these labels will be included in every error generated by the formatter.
// It can be used to group errors together in a function scope or a call scope.
func WithLabels(keyValues ...any) FormatterOption {
return func(f *Formatter) {
for i := 0; i < len(keyValues); i += 2 {
key, val, ok := f.getStringifiedKeyValue(keyValues, i)
if !ok {
continue
}
f.labels[key] = val
}
}
}
// Clone returns a copy of the formatter. Any change to this copy is safe since
// it would not change the original formatter.
// The global formatter can be cloned whenever you enter a new scope to be customized
// for that scope only.
func (f *Formatter) Clone() *Formatter {
newF := Formatter{
logger: f.logger,
labels: make(map[string]string),
template: f.template,
allowMissingValue: f.allowMissingValue,
missingValueReplacement: f.missingValueReplacement,
coreDataLookup: f.coreDataLookup,
}
for k, v := range f.labels {
newF.labels[k] = v
}
return &newF
}
// AddLabels adds a set of labels to the formatter.
// keyValues should be pairs of data, where the first element is a key and must be a
// string and follows [maxKeyLength] and [keyRE].
// The second element is the value and will be converted to string. If value is missing
// [missingValueReplacement] and [allowMissingValue] are used to decide how to handle it.
// If the key has invalid characters or is too long, it will be modified to a valid key.
func (f *Formatter) AddLabels(keyValues ...any) *Formatter {
for i := 0; i < len(keyValues); i += 2 {
key, val, ok := f.getStringifiedKeyValue(keyValues, i)
if !ok {
continue
}
f.labels[key] = val
}
return f
}
// MissingValueReplacement returns whether replacing missing values is allowed or not.
// And if it is allowed, what will be replaced for missing values.
func (f *Formatter) MissingValueReplacement() (string, bool) {
return f.missingValueReplacement, f.allowMissingValue
}
// LabelsSlice return formatter's default labels as a slice.
// Even indexed elements are keys and odd indexed elements are values
// sequentially attached to the slice.
// To have a map of labels, use LabelsMap.
func (f *Formatter) LabelsSlice() []string {
labels := make([]string, len(f.labels)*2)
index := 0
for k, v := range f.labels {
labels[index], labels[index+1] = k, v
index += 2
}
return labels
}
// LabelsMap returns formatter's default labels as a map.
// To have a slice of labels, use LabelsSlice.
func (f *Formatter) LabelsMap() map[string]string {
return f.labels
}
func (f *Formatter) getStringifiedKeyValue(keyValues []any, keyIndex int) (string, string, bool) {
key, ok := keyValues[keyIndex].(string)
if !ok {
return "", "", false
}
// Ensure that key follows the standard format
// @see https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/errdetails#ErrorInfo
key = keyRE.ReplaceAllString(key, "-")
if len(key) > keyMaxLength {
key = key[:keyMaxLength-1]
}
value := f.missingValueReplacement
if keyIndex+1 < len(keyValues) {
value = fmt.Sprintf("%v", keyValues[keyIndex+1])
} else if !f.allowMissingValue {
return "", "", false
}
return key, value, true
}