1
2
3
4
5
6
7
8
9
10
11
12
13
14 package model
15
16 import (
17 "fmt"
18 "regexp"
19 "sort"
20 "strings"
21 "unicode/utf8"
22
23 dto "github.com/prometheus/client_model/go"
24 "google.golang.org/protobuf/proto"
25 )
26
27 var (
28
29
30
31
32
33
34
35 NameValidationScheme = LegacyValidation
36
37
38
39
40 NameEscapingScheme = ValueEncodingEscaping
41 )
42
43
44
45 type ValidationScheme int
46
47 const (
48
49
50
51 LegacyValidation ValidationScheme = iota
52
53
54
55 UTF8Validation
56 )
57
58 type EscapingScheme int
59
60 const (
61
62
63
64 NoEscaping EscapingScheme = iota
65
66
67 UnderscoreEscaping
68
69
70
71 DotsEscaping
72
73
74
75
76 ValueEncodingEscaping
77 )
78
79 const (
80
81
82
83
84
85 EscapingKey = "escaping"
86
87
88 AllowUTF8 = "allow-utf-8"
89 EscapeUnderscores = "underscores"
90 EscapeDots = "dots"
91 EscapeValues = "values"
92 )
93
94
95
96
97 var MetricNameRE = regexp.MustCompile(`^[a-zA-Z_:][a-zA-Z0-9_:]*$`)
98
99
100
101 type Metric LabelSet
102
103
104 func (m Metric) Equal(o Metric) bool {
105 return LabelSet(m).Equal(LabelSet(o))
106 }
107
108
109 func (m Metric) Before(o Metric) bool {
110 return LabelSet(m).Before(LabelSet(o))
111 }
112
113
114 func (m Metric) Clone() Metric {
115 clone := make(Metric, len(m))
116 for k, v := range m {
117 clone[k] = v
118 }
119 return clone
120 }
121
122 func (m Metric) String() string {
123 metricName, hasName := m[MetricNameLabel]
124 numLabels := len(m) - 1
125 if !hasName {
126 numLabels = len(m)
127 }
128 labelStrings := make([]string, 0, numLabels)
129 for label, value := range m {
130 if label != MetricNameLabel {
131 labelStrings = append(labelStrings, fmt.Sprintf("%s=%q", label, value))
132 }
133 }
134
135 switch numLabels {
136 case 0:
137 if hasName {
138 return string(metricName)
139 }
140 return "{}"
141 default:
142 sort.Strings(labelStrings)
143 return fmt.Sprintf("%s{%s}", metricName, strings.Join(labelStrings, ", "))
144 }
145 }
146
147
148 func (m Metric) Fingerprint() Fingerprint {
149 return LabelSet(m).Fingerprint()
150 }
151
152
153
154 func (m Metric) FastFingerprint() Fingerprint {
155 return LabelSet(m).FastFingerprint()
156 }
157
158
159
160
161 func IsValidMetricName(n LabelValue) bool {
162 switch NameValidationScheme {
163 case LegacyValidation:
164 return IsValidLegacyMetricName(n)
165 case UTF8Validation:
166 if len(n) == 0 {
167 return false
168 }
169 return utf8.ValidString(string(n))
170 default:
171 panic(fmt.Sprintf("Invalid name validation scheme requested: %d", NameValidationScheme))
172 }
173 }
174
175
176
177
178
179 func IsValidLegacyMetricName(n LabelValue) bool {
180 if len(n) == 0 {
181 return false
182 }
183 for i, b := range n {
184 if !isValidLegacyRune(b, i) {
185 return false
186 }
187 }
188 return true
189 }
190
191
192
193
194
195 func EscapeMetricFamily(v *dto.MetricFamily, scheme EscapingScheme) *dto.MetricFamily {
196 if v == nil {
197 return nil
198 }
199
200 if scheme == NoEscaping {
201 return v
202 }
203
204 out := &dto.MetricFamily{
205 Help: v.Help,
206 Type: v.Type,
207 Unit: v.Unit,
208 }
209
210
211 if v.Name == nil || IsValidLegacyMetricName(LabelValue(v.GetName())) {
212 out.Name = v.Name
213 } else {
214 out.Name = proto.String(EscapeName(v.GetName(), scheme))
215 }
216 for _, m := range v.Metric {
217 if !metricNeedsEscaping(m) {
218 out.Metric = append(out.Metric, m)
219 continue
220 }
221
222 escaped := &dto.Metric{
223 Gauge: m.Gauge,
224 Counter: m.Counter,
225 Summary: m.Summary,
226 Untyped: m.Untyped,
227 Histogram: m.Histogram,
228 TimestampMs: m.TimestampMs,
229 }
230
231 for _, l := range m.Label {
232 if l.GetName() == MetricNameLabel {
233 if l.Value == nil || IsValidLegacyMetricName(LabelValue(l.GetValue())) {
234 escaped.Label = append(escaped.Label, l)
235 continue
236 }
237 escaped.Label = append(escaped.Label, &dto.LabelPair{
238 Name: proto.String(MetricNameLabel),
239 Value: proto.String(EscapeName(l.GetValue(), scheme)),
240 })
241 continue
242 }
243 if l.Name == nil || IsValidLegacyMetricName(LabelValue(l.GetName())) {
244 escaped.Label = append(escaped.Label, l)
245 continue
246 }
247 escaped.Label = append(escaped.Label, &dto.LabelPair{
248 Name: proto.String(EscapeName(l.GetName(), scheme)),
249 Value: l.Value,
250 })
251 }
252 out.Metric = append(out.Metric, escaped)
253 }
254 return out
255 }
256
257 func metricNeedsEscaping(m *dto.Metric) bool {
258 for _, l := range m.Label {
259 if l.GetName() == MetricNameLabel && !IsValidLegacyMetricName(LabelValue(l.GetValue())) {
260 return true
261 }
262 if !IsValidLegacyMetricName(LabelValue(l.GetName())) {
263 return true
264 }
265 }
266 return false
267 }
268
269 const (
270 lowerhex = "0123456789abcdef"
271 )
272
273
274
275
276
277 func EscapeName(name string, scheme EscapingScheme) string {
278 if len(name) == 0 {
279 return name
280 }
281 var escaped strings.Builder
282 switch scheme {
283 case NoEscaping:
284 return name
285 case UnderscoreEscaping:
286 if IsValidLegacyMetricName(LabelValue(name)) {
287 return name
288 }
289 for i, b := range name {
290 if isValidLegacyRune(b, i) {
291 escaped.WriteRune(b)
292 } else {
293 escaped.WriteRune('_')
294 }
295 }
296 return escaped.String()
297 case DotsEscaping:
298
299 for i, b := range name {
300 if b == '_' {
301 escaped.WriteString("__")
302 } else if b == '.' {
303 escaped.WriteString("_dot_")
304 } else if isValidLegacyRune(b, i) {
305 escaped.WriteRune(b)
306 } else {
307 escaped.WriteRune('_')
308 }
309 }
310 return escaped.String()
311 case ValueEncodingEscaping:
312 if IsValidLegacyMetricName(LabelValue(name)) {
313 return name
314 }
315 escaped.WriteString("U__")
316 for i, b := range name {
317 if isValidLegacyRune(b, i) {
318 escaped.WriteRune(b)
319 } else if !utf8.ValidRune(b) {
320 escaped.WriteString("_FFFD_")
321 } else if b < 0x100 {
322 escaped.WriteRune('_')
323 for s := 4; s >= 0; s -= 4 {
324 escaped.WriteByte(lowerhex[b>>uint(s)&0xF])
325 }
326 escaped.WriteRune('_')
327 } else if b < 0x10000 {
328 escaped.WriteRune('_')
329 for s := 12; s >= 0; s -= 4 {
330 escaped.WriteByte(lowerhex[b>>uint(s)&0xF])
331 }
332 escaped.WriteRune('_')
333 }
334 }
335 return escaped.String()
336 default:
337 panic(fmt.Sprintf("invalid escaping scheme %d", scheme))
338 }
339 }
340
341
342 func lower(c byte) byte {
343 return c | ('x' - 'X')
344 }
345
346
347
348
349 func UnescapeName(name string, scheme EscapingScheme) string {
350 if len(name) == 0 {
351 return name
352 }
353 switch scheme {
354 case NoEscaping:
355 return name
356 case UnderscoreEscaping:
357
358 return name
359 case DotsEscaping:
360 name = strings.ReplaceAll(name, "_dot_", ".")
361 name = strings.ReplaceAll(name, "__", "_")
362 return name
363 case ValueEncodingEscaping:
364 escapedName, found := strings.CutPrefix(name, "U__")
365 if !found {
366 return name
367 }
368
369 var unescaped strings.Builder
370 TOP:
371 for i := 0; i < len(escapedName); i++ {
372
373 if escapedName[i] != '_' {
374 unescaped.WriteByte(escapedName[i])
375 continue
376 }
377 i++
378 if i >= len(escapedName) {
379 return name
380 }
381
382 if escapedName[i] == '_' {
383 unescaped.WriteByte('_')
384 continue
385 }
386
387 var utf8Val uint
388 for j := 0; i < len(escapedName); j++ {
389
390 if j > 4 {
391 return name
392 }
393
394 if escapedName[i] == '_' {
395 utf8Rune := rune(utf8Val)
396 if !utf8.ValidRune(utf8Rune) {
397 return name
398 }
399 unescaped.WriteRune(utf8Rune)
400 continue TOP
401 }
402 r := lower(escapedName[i])
403 utf8Val *= 16
404 if r >= '0' && r <= '9' {
405 utf8Val += uint(r) - '0'
406 } else if r >= 'a' && r <= 'f' {
407 utf8Val += uint(r) - 'a' + 10
408 } else {
409 return name
410 }
411 i++
412 }
413
414 return name
415 }
416 return unescaped.String()
417 default:
418 panic(fmt.Sprintf("invalid escaping scheme %d", scheme))
419 }
420 }
421
422 func isValidLegacyRune(b rune, i int) bool {
423 return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || b == ':' || (b >= '0' && b <= '9' && i > 0)
424 }
425
426 func (e EscapingScheme) String() string {
427 switch e {
428 case NoEscaping:
429 return AllowUTF8
430 case UnderscoreEscaping:
431 return EscapeUnderscores
432 case DotsEscaping:
433 return EscapeDots
434 case ValueEncodingEscaping:
435 return EscapeValues
436 default:
437 panic(fmt.Sprintf("unknown format scheme %d", e))
438 }
439 }
440
441 func ToEscapingScheme(s string) (EscapingScheme, error) {
442 if s == "" {
443 return NoEscaping, fmt.Errorf("got empty string instead of escaping scheme")
444 }
445 switch s {
446 case AllowUTF8:
447 return NoEscaping, nil
448 case EscapeUnderscores:
449 return UnderscoreEscaping, nil
450 case EscapeDots:
451 return DotsEscaping, nil
452 case EscapeValues:
453 return ValueEncodingEscaping, nil
454 default:
455 return NoEscaping, fmt.Errorf("unknown format scheme " + s)
456 }
457 }
458
View as plain text