Skip to content

Commit 0f87085

Browse files
simonswineshelldandy
authored andcommitted
chore: Refactor labelset from og/segment and og/flameql (grafana#3848)
In order to be able to parse the labelset in the classic ingest endpoints in Grafana Alloy, I am refactoring this bit of the OG code into the api package so I can use this within Grafana Alloy.
1 parent ae99fa0 commit 0f87085

26 files changed

+470
-1068
lines changed

api/model/labelset/labelset.go

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
// Package labelset implements parsing/normalizing of string representation of the Label Set of a profile.
2+
//
3+
// This is used by the /ingest endpoint, as described in the original Pyroscope API. We keep this mainly for backwards compatability.
4+
package labelset
5+
6+
import (
7+
"bytes"
8+
"errors"
9+
"sort"
10+
"strconv"
11+
"strings"
12+
"sync"
13+
"time"
14+
)
15+
16+
type LabelSet struct {
17+
labels map[string]string
18+
}
19+
20+
type ParserState int
21+
22+
const (
23+
nameParserState ParserState = iota
24+
tagLabelSetParserState
25+
tagValueParserState
26+
doneParserState
27+
)
28+
29+
func New(labels map[string]string) *LabelSet { return &LabelSet{labels: labels} }
30+
31+
func Parse(name string) (*LabelSet, error) {
32+
k := &LabelSet{labels: make(map[string]string)}
33+
p := parserPool.Get().(*parser)
34+
defer parserPool.Put(p)
35+
p.reset()
36+
var err error
37+
for _, r := range name + "{" {
38+
switch p.parserState {
39+
case nameParserState:
40+
err = p.nameParserCase(r, k)
41+
case tagLabelSetParserState:
42+
p.tagLabelSetParserCase(r)
43+
case tagValueParserState:
44+
err = p.tagValueParserCase(r, k)
45+
}
46+
if err != nil {
47+
return nil, err
48+
}
49+
}
50+
return k, nil
51+
}
52+
53+
func ValidateLabelSet(k *LabelSet) error {
54+
if k == nil {
55+
return ErrInvalidLabelSet
56+
}
57+
58+
for key, v := range k.labels {
59+
if key == ReservedLabelNameName {
60+
if err := ValidateServiceName(v); err != nil {
61+
return err
62+
}
63+
} else {
64+
if err := ValidateLabelName(key); err != nil {
65+
return err
66+
}
67+
}
68+
}
69+
return nil
70+
}
71+
72+
type parser struct {
73+
parserState ParserState
74+
key *bytes.Buffer
75+
value *bytes.Buffer
76+
}
77+
78+
var parserPool = sync.Pool{
79+
New: func() any {
80+
return &parser{
81+
parserState: nameParserState,
82+
key: new(bytes.Buffer),
83+
value: new(bytes.Buffer),
84+
}
85+
},
86+
}
87+
88+
func (p *parser) reset() {
89+
p.parserState = nameParserState
90+
p.key.Reset()
91+
p.value.Reset()
92+
}
93+
94+
// Parse's nameParserState switch case
95+
func (p *parser) nameParserCase(r int32, k *LabelSet) error {
96+
switch r {
97+
case '{':
98+
p.parserState = tagLabelSetParserState
99+
serviceName := strings.TrimSpace(p.value.String())
100+
if err := ValidateServiceName(serviceName); err != nil {
101+
return err
102+
}
103+
k.labels["__name__"] = serviceName
104+
default:
105+
p.value.WriteRune(r)
106+
}
107+
return nil
108+
}
109+
110+
// Parse's tagLabelSetParserState switch case
111+
func (p *parser) tagLabelSetParserCase(r rune) {
112+
switch r {
113+
case '}':
114+
p.parserState = doneParserState
115+
case '=':
116+
p.parserState = tagValueParserState
117+
p.value.Reset()
118+
default:
119+
p.key.WriteRune(r)
120+
}
121+
}
122+
123+
// Parse's tagValueParserState switch case
124+
func (p *parser) tagValueParserCase(r rune, k *LabelSet) error {
125+
switch r {
126+
case ',', '}':
127+
p.parserState = tagLabelSetParserState
128+
key := strings.TrimSpace(p.key.String())
129+
if !IsLabelNameReserved(key) {
130+
if err := ValidateLabelName(key); err != nil {
131+
return err
132+
}
133+
}
134+
k.labels[key] = strings.TrimSpace(p.value.String())
135+
p.key.Reset()
136+
default:
137+
p.value.WriteRune(r)
138+
}
139+
return nil
140+
}
141+
142+
func (k *LabelSet) LabelSet() string {
143+
return k.Normalized()
144+
}
145+
146+
const ProfileIDLabelName = "profile_id"
147+
148+
func (k *LabelSet) HasProfileID() bool {
149+
v, ok := k.labels[ProfileIDLabelName]
150+
return ok && v != ""
151+
}
152+
153+
func (k *LabelSet) ProfileID() (string, bool) {
154+
id, ok := k.labels[ProfileIDLabelName]
155+
return id, ok
156+
}
157+
158+
func ServiceNameLabelSet(appName string) string { return appName + "{}" }
159+
160+
func TreeLabelSet(k string, depth int, unixTime int64) string {
161+
return k + ":" + strconv.Itoa(depth) + ":" + strconv.FormatInt(unixTime, 10)
162+
}
163+
164+
func (k *LabelSet) TreeLabelSet(depth int, t time.Time) string {
165+
return TreeLabelSet(k.Normalized(), depth, t.Unix())
166+
}
167+
168+
var errLabelSetInvalid = errors.New("invalid key")
169+
170+
// ParseTreeLabelSet retrieves tree time and depth level from the given key.
171+
func ParseTreeLabelSet(k string) (time.Time, int, error) {
172+
a := strings.Split(k, ":")
173+
if len(a) < 3 {
174+
return time.Time{}, 0, errLabelSetInvalid
175+
}
176+
level, err := strconv.Atoi(a[1])
177+
if err != nil {
178+
return time.Time{}, 0, err
179+
}
180+
v, err := strconv.Atoi(a[2])
181+
if err != nil {
182+
return time.Time{}, 0, err
183+
}
184+
return time.Unix(int64(v), 0), level, err
185+
}
186+
187+
func (k *LabelSet) DictLabelSet() string {
188+
return k.labels[ReservedLabelNameName]
189+
}
190+
191+
// FromTreeToDictLabelSet returns app name from tree key k: given tree key
192+
// "foo{}:0:1234567890", the call returns "foo".
193+
//
194+
// Before tags support, segment key form (i.e. app name + tags: foo{key=value})
195+
// has been used to reference a dictionary (trie).
196+
func FromTreeToDictLabelSet(k string) string {
197+
return k[0:strings.IndexAny(k, "{")]
198+
}
199+
200+
func (l *LabelSet) Normalized() string {
201+
var sb strings.Builder
202+
203+
labelNames := make([]string, 0, len(l.labels))
204+
for k, v := range l.labels {
205+
if k == ReservedLabelNameName {
206+
sb.WriteString(v)
207+
} else {
208+
labelNames = append(labelNames, k)
209+
}
210+
}
211+
212+
sort.Slice(labelNames, func(i, j int) bool {
213+
return labelNames[i] < labelNames[j]
214+
})
215+
216+
sb.WriteString("{")
217+
for i, k := range labelNames {
218+
v := l.labels[k]
219+
if i != 0 {
220+
sb.WriteString(",")
221+
}
222+
sb.WriteString(k)
223+
sb.WriteString("=")
224+
sb.WriteString(v)
225+
}
226+
sb.WriteString("}")
227+
228+
return sb.String()
229+
}
230+
231+
func (k *LabelSet) Clone() *LabelSet {
232+
newMap := make(map[string]string)
233+
for k, v := range k.labels {
234+
newMap[k] = v
235+
}
236+
return &LabelSet{labels: newMap}
237+
}
238+
239+
func (k *LabelSet) ServiceName() string {
240+
return k.labels[ReservedLabelNameName]
241+
}
242+
243+
func (k *LabelSet) Labels() map[string]string {
244+
return k.labels
245+
}
246+
247+
func (k *LabelSet) Add(key, value string) {
248+
if value == "" {
249+
delete(k.labels, key)
250+
} else {
251+
k.labels[key] = value
252+
}
253+
}

pkg/og/storage/segment/key_bech_test.go renamed to api/model/labelset/labelset_bench_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package segment
1+
package labelset
22

33
import (
44
"math/rand"
@@ -19,13 +19,13 @@ func BenchmarkKey_Parse(b *testing.B) {
1919
}
2020

2121
labels["__name__"] = "benchmark.key.parse"
22-
keyStr := NewKey(labels).Normalized()
22+
keyStr := New(labels).Normalized()
2323

2424
b.ReportAllocs()
2525
b.ResetTimer()
2626

2727
for i := 0; i < b.N; i++ {
28-
if _, err := ParseKey(keyStr); err != nil {
28+
if _, err := Parse(keyStr); err != nil {
2929
b.Fatal(err)
3030
}
3131
}

api/model/labelset/labelset_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package labelset
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestParseLabelSet(t *testing.T) {
10+
t.Run("no tags version works", func(t *testing.T) {
11+
k, err := Parse("foo")
12+
require.NoError(t, err)
13+
require.Equal(t, map[string]string{"__name__": "foo"}, k.labels)
14+
})
15+
16+
t.Run("simple values work", func(t *testing.T) {
17+
k, err := Parse("foo{bar=1,baz=2}")
18+
require.NoError(t, err)
19+
require.Equal(t, map[string]string{"__name__": "foo", "bar": "1", "baz": "2"}, k.labels)
20+
})
21+
22+
t.Run("simple values with spaces work", func(t *testing.T) {
23+
k, err := Parse(" foo { bar = 1 , baz = 2 } ")
24+
require.NoError(t, err)
25+
require.Equal(t, map[string]string{"__name__": "foo", "bar": "1", "baz": "2"}, k.labels)
26+
})
27+
}
28+
29+
func TestKeyNormalize(t *testing.T) {
30+
t.Run("no tags version works", func(t *testing.T) {
31+
k, err := Parse("foo")
32+
require.NoError(t, err)
33+
require.Equal(t, "foo{}", k.Normalized())
34+
})
35+
36+
t.Run("simple values work", func(t *testing.T) {
37+
k, err := Parse("foo{bar=1,baz=2}")
38+
require.NoError(t, err)
39+
require.Equal(t, "foo{bar=1,baz=2}", k.Normalized())
40+
})
41+
42+
t.Run("unsorted values work", func(t *testing.T) {
43+
k, err := Parse("foo{baz=1,bar=2}")
44+
require.NoError(t, err)
45+
require.Equal(t, "foo{bar=2,baz=1}", k.Normalized())
46+
})
47+
}

0 commit comments

Comments
 (0)