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
|
// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
// SPDX-License-Identifier: Apache-2.0
package microprom
import (
"fmt"
"strings"
)
// Labels holds a label set, formatted according to the text protocol of [OpenMetrics 1.0].
// Instances are constructed through [MetricSet.FormatLabels].
//
// [OpenMetrics 1.0]: https://prometheus.io/docs/specs/om/open_metrics_spec/
type Labels string
// LabelNames holds a set of label names.
type LabelNames struct {
// NOTE: This is an opaque struct because, when adding support for OpenMetrics 2.0,
// it will be useful to precompute escaped forms for these names where necessary.
//
// Another interesting addition might be alphabetical sorting of labels, where we
// would need to remember the sort order because we need to apply it to the values
// slice during Format().
names []string
}
// NewLabelNames constructs a LabelNames instance.
//
// Per the [OpenMetrics 1.0] spec, label names must match the following regular expression:
//
// [a-zA-Z_][a-zA-Z0-9_]*
//
// [OpenMetrics 1.0]: https://prometheus.io/docs/specs/om/open_metrics_spec/
func NewLabelNames(names ...string) LabelNames {
for _, name := range names {
if !labelNameRx.MatchString(name) {
panic(fmt.Sprintf("invalid label name: %q", name))
}
}
return LabelNames{names}
}
// FormatLabels serializes a Prometheus labelset into the string format used in Prometheus text expositions.
// For example:
//
// // once, e.g. during func init()
// var names = microprom.NewLabelNames("foo", "hello")
//
// // during microprom.Handler.Collect()
// labels := ms.FormatLabels(names, "bar", "world")
// assert.Equal(t, labels, `foo="bar",hello="world"`)
func (ms *MetricSet) FormatLabels(n LabelNames, values ...string) Labels {
// NOTE on API structure:
// - This is not part of ms.Add() to allow reusing label sets for multiple metrics.
// - This is a method of MetricSet because we want to be able to depend on the negotiated syntax in the future (e.g. for escaping rules).
if len(n.names) != len(values) {
panic(fmt.Sprintf("expected %d label values, but got %d", len(n.names), len(values)))
}
if len(n.names) == 0 {
return ""
}
// estimate the perfect number of bytes for the result string to avoid reallocations
capacity := len(n.names) - 1 // number of "," between pairs
needsEscaping := make([]bool, len(n.names))
for idx, value := range values {
// base length for an encoding in the form `label="value"`
capacity += len(n.names[idx]) + len(value) + 3
// some characters within `value` need escaping (TODO: this could be optimized to only iterate through `value` once)
toEscape := strings.Count(value, "\n") + strings.Count(value, "\"") + strings.Count(value, "\\")
needsEscaping[idx] = toEscape > 0
capacity += toEscape
}
var b strings.Builder
b.Grow(capacity)
for idx, value := range values {
if idx > 0 {
_ = b.WriteByte(',')
}
_, _ = b.WriteString(n.names[idx])
_ = b.WriteByte('=')
_ = b.WriteByte('"')
if needsEscaping[idx] {
// TODO: this could be optimized, but since this branch is unlikely in practice, I did not bother yet
value = strings.ReplaceAll(value, "\\", "\\\\")
value = strings.ReplaceAll(value, "\"", "\\\"")
value = strings.ReplaceAll(value, "\n", "\\n")
}
_, _ = b.WriteString(value)
_ = b.WriteByte('"')
}
return Labels(b.String())
}
|