forked from Syncano/rabbitmq_exporter
-
Notifications
You must be signed in to change notification settings - Fork 197
/
bertmap.go
347 lines (312 loc) · 8.85 KB
/
bertmap.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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
package main
import (
"fmt"
"math/big"
bert "github.com/kbudde/gobert"
log "github.com/sirupsen/logrus"
)
// rabbitBERTReply (along with its RabbitReply interface
// implementation) allow parsing of BERT-encoded RabbitMQ replies in a
// way that's fully compatible with JSON parser from jsonmap.go
type rabbitBERTReply struct {
body []byte
objects bert.Term
}
func makeBERTReply(body []byte) (RabbitReply, error) {
rawObjects, err := bert.Decode(body)
return &rabbitBERTReply{body, rawObjects}, err
}
func (rep *rabbitBERTReply) MakeStatsInfo(labels []string) []StatsInfo {
rawObjects := rep.objects
objects, ok := rawObjects.([]bert.Term)
if !ok {
log.WithField("got", rawObjects).Error("Statistics reply should contain a slice of objects")
return make([]StatsInfo, 0)
}
statistics := make([]StatsInfo, 0, len(objects))
for _, v := range objects {
obj, ok := parseSingleStatsObject(v, labels)
if !ok {
log.WithField("got", v).Error("Ignoring unparseable stats object")
continue
}
statistics = append(statistics, *obj)
}
return statistics
}
func (rep *rabbitBERTReply) MakeMap() MetricMap {
flMap := make(MetricMap)
term := rep.objects
err := parseProplist(&flMap, "", term)
if err != nil {
log.WithField("error", err).Warn("Error parsing rabbitmq reply (bert, MakeMap)")
}
return flMap
}
// iterateBertKV helps to traverse any map-like structures returned by
// RabbitMQ with a user-provided function. We need it because
// different versions of RabbitMQ can encode a map in a bunch of
// different ways:
// - proplist
// - proplist additionally wrapped in a {struct, ...} tuple
// - map type available in modern erlang versions
//
// Non-nil error return means that an object can't be interpreted as a map in any way
//
// Provided function can return 'false' value to stop traversal earlier
func iterateBertKV(obj interface{}, elemFunc func(string, interface{}) bool) error {
switch obj := obj.(type) {
case []bert.Term:
pairs, ok := assertBertProplistPairs(obj)
if !ok {
return bertError("Doesn't look like a proplist", obj)
}
for _, v := range pairs {
key, value, ok := assertBertKeyedTuple(v)
if ok {
needToContinue := elemFunc(key, value)
if !needToContinue {
return nil
}
}
}
return nil
case bert.Map:
for keyRaw, value := range obj {
key, ok := parseBertStringy(keyRaw)
if ok {
needToContinue := elemFunc(key, value)
if !needToContinue {
return nil
}
}
}
return nil
default:
return bertError("Can't iterate over non-KV object", obj)
}
}
// parseSingleStatsObject extracts information about a named RabbitMQ
// object: both its vhost/name information and then the usual
// MetricMap.
func parseSingleStatsObject(obj interface{}, labels []string) (*StatsInfo, bool) {
var result StatsInfo
var objectOk = true
result.metrics = make(MetricMap)
result.labels = make(map[string]string)
for _, label := range labels {
result.labels[label] = ""
}
err := iterateBertKV(obj, func(key string, value interface{}) bool {
//Check if current key should be saved as label
for _, label := range labels {
if key == label {
tmp, ok := parseBertStringy(value)
if !ok {
log.WithField("got", value).WithField("label", label).Error("Non-string field")
objectOk = false
return false
}
result.labels[label] = tmp
}
}
arr, isSlice := assertBertSlice(value)
_, iSPropList := assertBertProplistPairs(value)
//save metrics for array length.
// An array is a slice which is not a proplist.
// Arrays with len()==0 are special. IsProplist is true
if isSlice && (!iSPropList || len(arr) == 0) {
result.metrics[key+"_len"] = float64(len(arr))
}
if floatValue, ok := parseFloaty(value); ok {
result.metrics[key] = floatValue
return true
}
// Nested structures don't need special
// processing, so we fallback to generic
// parser.
if err := parseProplist(&result.metrics, key, value); err == nil {
return true
}
return true
})
if err == nil && objectOk {
return &result, true
}
return nil, false
}
// parseProplist descends into an erlang data structure and stores
// everything remotely resembling a float in a toMap.
func parseProplist(toMap *MetricMap, basename string, maybeProplist interface{}) error {
prefix := ""
if basename != "" {
prefix = basename + "."
}
return iterateBertKV(maybeProplist, func(key string, value interface{}) bool {
if floatValue, ok := parseFloaty(value); ok {
(*toMap)[prefix+key] = floatValue
return true
}
if arraySize, ok := parseArray(value); ok {
(*toMap)[prefix+key+"_len"] = arraySize
}
err := parseProplist(toMap, prefix+key, value) // This can fail, but we don't care
log.WithField("error", err).Debug("Error parsing rabbitmq reply (bert, parseProplist)")
return true
})
}
// assertBertSlice checks whether the provided value is something
// that's represented as a slice by BERT parcer (list or tuple).
func assertBertSlice(maybeSlice interface{}) ([]bert.Term, bool) {
switch it := maybeSlice.(type) {
case []bert.Term:
return it, true
default:
return nil, false
}
}
// assertBertKeyedTuple checks whether the provided value looks like
// an element of proplist - 2-element tuple where the first elemen is
// an atom.
func assertBertKeyedTuple(maybeTuple interface{}) (string, bert.Term, bool) {
tuple, ok := assertBertSlice(maybeTuple)
if !ok {
return "", nil, false
}
if len(tuple) != 2 {
return "", nil, false
}
key, ok := assertBertAtom(tuple[0])
if !ok {
return "", nil, false
}
return key, tuple[1], true
}
func assertBertAtom(val interface{}) (string, bool) {
if atom, ok := val.(bert.Atom); ok {
return string(atom), true
}
return "", false
}
// assertBertProplistPairs checks whether the provided value points to
// a proplist. Additional level of {struct, ...} wrapping can be
// removed in process.
func assertBertProplistPairs(maybeTaggedProplist interface{}) ([]bert.Term, bool) {
terms, ok := assertBertSlice(maybeTaggedProplist)
if !ok {
return nil, false
}
if len(terms) == 0 {
return terms, true
}
// Strip {struct, ...} tagging than is used to help RabbitMQ
// JSON encoder
key, value, ok := assertBertKeyedTuple(terms)
if ok && key == "struct" {
return assertBertProplistPairs(value)
}
// Minimal safety check - at least the first element should be
// a proplist pair
_, _, ok = assertBertKeyedTuple(terms[0])
if ok {
return terms, true
}
return nil, false
}
// parseArray tries to interpret the provided BERT value as an array.
// It returns the size of the array
func parseArray(arr interface{}) (float64, bool) {
switch t := arr.(type) {
case []bert.Term:
_, isPropList := assertBertProplistPairs(t)
if !isPropList || len(t) == 0 {
return float64(len(t)), true
}
}
return 0, false
}
// parseFloaty tries to interpret the provided BERT value as a
// float. Floats itself, integers and booleans are handled.
func parseFloaty(num interface{}) (float64, bool) {
switch num := num.(type) {
case int:
return float64(num), true
case int8:
return float64(num), true
case int16:
return float64(num), true
case int32:
return float64(num), true
case int64:
return float64(num), true
case uint:
return float64(num), true
case uint8:
return float64(num), true
case uint16:
return float64(num), true
case uint32:
return float64(num), true
case uint64:
return float64(num), true
case float32:
return float64(num), true
case float64:
return num, true
case bert.Atom:
if num == bert.TrueAtom {
return 1, true
} else if num == bert.FalseAtom {
return 0, true
}
case big.Int:
bigFloat := new(big.Float).SetInt(&num)
result, _ := bigFloat.Float64()
return result, true
}
return 0, false
}
// parseBertStringy tries to extract an Erlang value that can be
// represented as a Go string (binary or atom).
func parseBertStringy(val interface{}) (string, bool) {
if stringer, ok := val.(fmt.Stringer); ok {
return stringer.String(), true
} else if atom, ok := val.(bert.Atom); ok {
return string(atom), true
} else if s, ok := val.(string); ok {
return s, true
}
return "", false
}
type bertDecodeError struct {
message string
object interface{}
}
func (err *bertDecodeError) Error() string {
return fmt.Sprintf("%s while decoding: %s", err.message, err.object)
}
func bertError(message string, object interface{}) error {
return &bertDecodeError{message, object}
}
func (rep *rabbitBERTReply) GetString(label string) (string, bool) {
var resValue string
var result bool
result = false
err := iterateBertKV(rep.objects, func(key string, value interface{}) bool {
//Check if current key should be saved as label
if key == label {
tmp, ok := parseBertStringy(value)
if !ok {
return false
}
resValue = tmp
result = true
return false
}
return true
})
if err != nil {
log.WithField("error", err).Warn("Error parsing rabbitmq reply (bert, GetString)")
}
return resValue, result
}