Skip to content

Commit 72a3b80

Browse files
committed
feat: Allow utf-8 characters in label names
This feature is a relaxation in previous validation for label names. Given the backward incompatible nature of this change (for example, PromQL queries must be [updated to support utf-8 label names](https://prometheus.io/docs/guides/utf8/#querying)), it is gated behind a "client capability": a key/val that a client must set in the `Accept:` header.
1 parent 8f2bb8c commit 72a3b80

File tree

5 files changed

+124
-21
lines changed

5 files changed

+124
-21
lines changed

cmd/profilecli/client.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"fmt"
55
"net/http"
6+
"strings"
67

78
"connectrpc.com/connect"
89
"github.com/prometheus/common/version"
@@ -17,7 +18,21 @@ const (
1718
protocolTypeGRPCWeb = "grpc-web"
1819
)
1920

21+
var acceptHeaderFeatureFlags = []string{
22+
"allow-utf8-labelnames=true",
23+
}
24+
2025
var userAgentHeader = fmt.Sprintf("pyroscope/%s", version.Version)
26+
var acceptHeaderMimeType = "*/*"
27+
28+
func buildAcceptHeader(featureFlags []string) string {
29+
acceptHeader := acceptHeaderMimeType
30+
if len(acceptHeaderFeatureFlags) > 0 {
31+
acceptHeader += "; " + strings.Join(featureFlags, "; ")
32+
}
33+
34+
return acceptHeader
35+
}
2136

2237
type phlareClient struct {
2338
TenantID string
@@ -46,7 +61,10 @@ func (a *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
4661
}
4762
}
4863

64+
acceptHeader := buildAcceptHeader(acceptHeaderFeatureFlags)
65+
req.Header.Set("Accept", acceptHeader)
4966
req.Header.Set("User-Agent", userAgentHeader)
67+
5068
return a.next.RoundTrip(req)
5169
}
5270

pkg/distributor/distributor.go

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import (
4646
distributormodel "github.com/grafana/pyroscope/pkg/distributor/model"
4747
"github.com/grafana/pyroscope/pkg/distributor/sampling"
4848
"github.com/grafana/pyroscope/pkg/distributor/writepath"
49+
"github.com/grafana/pyroscope/pkg/featureflags"
4950
phlaremodel "github.com/grafana/pyroscope/pkg/model"
5051
"github.com/grafana/pyroscope/pkg/model/pprofsplit"
5152
"github.com/grafana/pyroscope/pkg/model/relabel"
@@ -248,7 +249,14 @@ func (d *Distributor) Push(ctx context.Context, grpcReq *connect.Request[pushv1.
248249
Series: make([]*distributormodel.ProfileSeries, 0, len(grpcReq.Msg.Series)),
249250
RawProfileType: distributormodel.RawProfileTypePPROF,
250251
}
252+
251253
allErrors := multierror.New()
254+
255+
clientCapabilities, parseErr := featureflags.ParseClientCapabilities(grpcReq.Header())
256+
if parseErr != nil {
257+
allErrors.Add(parseErr)
258+
}
259+
252260
for _, grpcSeries := range grpcReq.Msg.Series {
253261
for _, grpcSample := range grpcSeries.Samples {
254262
profile, err := pprof.RawFromBytes(grpcSample.RawProfile)
@@ -257,10 +265,11 @@ func (d *Distributor) Push(ctx context.Context, grpcReq *connect.Request[pushv1.
257265
continue
258266
}
259267
series := &distributormodel.ProfileSeries{
260-
Labels: grpcSeries.Labels,
261-
Profile: profile,
262-
RawProfile: grpcSample.RawProfile,
263-
ID: grpcSample.ID,
268+
Labels: grpcSeries.Labels,
269+
Profile: profile,
270+
RawProfile: grpcSample.RawProfile,
271+
ID: grpcSample.ID,
272+
ClientCapabilities: clientCapabilities,
264273
}
265274
req.Series = append(req.Series, series)
266275
}
@@ -630,10 +639,11 @@ func (d *Distributor) aggregate(ctx context.Context, req *distributormodel.Profi
630639
return handleErr
631640
}
632641
aggregated := &distributormodel.ProfileSeries{
633-
TenantID: req.TenantID,
634-
Labels: labels,
635-
Profile: pprof.RawFromProto(p.Profile()),
636-
Annotations: annotations,
642+
TenantID: req.TenantID,
643+
Labels: labels,
644+
Profile: pprof.RawFromProto(p.Profile()),
645+
Annotations: annotations,
646+
ClientCapabilities: req.ClientCapabilities,
637647
}
638648
return d.router.Send(localCtx, aggregated)
639649
})()
@@ -1132,9 +1142,10 @@ func (d *Distributor) visitSampleSeries(s *distributormodel.ProfileSeries, visit
11321142
var result []*distributormodel.ProfileSeries
11331143
usageGroups := d.usageGroupEvaluator.GetMatch(s.TenantID, usageConfig, s.Labels)
11341144
visitor := &sampleSeriesVisitor{
1135-
tenantID: s.TenantID,
1136-
limits: d.limits,
1137-
profile: s.Profile,
1145+
tenantID: s.TenantID,
1146+
limits: d.limits,
1147+
profile: s.Profile,
1148+
clientCapabilities: s.ClientCapabilities,
11381149
}
11391150
if err := visit(s.Profile.Profile, s.Labels, relabelingRules, visitor); err != nil {
11401151
validation.DiscardedProfiles.WithLabelValues(string(validation.ReasonOf(err)), s.TenantID).Add(float64(s.TotalProfiles))
@@ -1164,24 +1175,28 @@ func (d *Distributor) visitSampleSeries(s *distributormodel.ProfileSeries, visit
11641175
}
11651176

11661177
type sampleSeriesVisitor struct {
1167-
tenantID string
1168-
limits Limits
1169-
profile *pprof.Profile
1170-
exp *pprof.SampleExporter
1171-
series []*distributormodel.ProfileSeries
1178+
tenantID string
1179+
limits Limits
1180+
profile *pprof.Profile
1181+
exp *pprof.SampleExporter
1182+
series []*distributormodel.ProfileSeries
1183+
clientCapabilities []*featureflags.ClientCapability
11721184

11731185
discardedBytes int
11741186
discardedProfiles int
11751187
}
11761188

11771189
func (v *sampleSeriesVisitor) ValidateLabels(labels phlaremodel.Labels) error {
1178-
return validation.ValidateLabels(v.limits, v.tenantID, labels)
1190+
capability := featureflags.GetClientCapability(v.clientCapabilities, featureflags.AllowUtf8LabelNamesCapabilityName)
1191+
utf8LabelNamesEnabled := capability != nil && capability.Value == "true"
1192+
return validation.ValidateLabels(v.limits, v.tenantID, utf8LabelNamesEnabled, labels)
11791193
}
11801194

11811195
func (v *sampleSeriesVisitor) VisitProfile(labels phlaremodel.Labels) {
11821196
v.series = append(v.series, &distributormodel.ProfileSeries{
1183-
Profile: v.profile,
1184-
Labels: labels,
1197+
Profile: v.profile,
1198+
Labels: labels,
1199+
ClientCapabilities: v.clientCapabilities,
11851200
})
11861201
}
11871202

pkg/distributor/model/push.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"github.com/grafana/pyroscope/pkg/distributor/annotation"
66
"github.com/grafana/pyroscope/pkg/distributor/ingestlimits"
77
"github.com/grafana/pyroscope/pkg/distributor/sampling"
8+
"github.com/grafana/pyroscope/pkg/featureflags"
89
phlaremodel "github.com/grafana/pyroscope/pkg/model"
910
"github.com/grafana/pyroscope/pkg/pprof"
1011
)
@@ -30,6 +31,9 @@ type ProfileSeries struct {
3031
RawProfile []byte // may be nil if the Profile is composed not from pprof ( e.g. jfr)
3132
ID string
3233

34+
// List of features the client supports
35+
ClientCapabilities []*featureflags.ClientCapability
36+
3337
// todo split
3438
// Transient state
3539
TenantID string
@@ -89,6 +93,7 @@ func (req *ProfileSeries) Clone() *ProfileSeries {
8993
ID: req.ID,
9094
Language: req.Language,
9195
Annotations: req.Annotations,
96+
ClientCapabilities: req.ClientCapabilities,
9297
}
9398
return c
9499
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package featureflags
2+
3+
import (
4+
"fmt"
5+
"mime"
6+
"net/http"
7+
)
8+
9+
const (
10+
// Capability names
11+
AllowUtf8LabelNamesCapabilityName = "allow-utf8-labelnames"
12+
)
13+
14+
type ClientCapability struct {
15+
Name string
16+
Value string
17+
}
18+
19+
func ParseClientCapabilities(header http.Header) ([]*ClientCapability, error) {
20+
acceptHeader := header.Get("Accept")
21+
if acceptHeader != "" {
22+
if _, params, err := mime.ParseMediaType(acceptHeader); err != nil {
23+
return nil, err
24+
} else {
25+
capabilities := make([]*ClientCapability, 0, len(params))
26+
seenCapabilityNames := make(map[string]struct{})
27+
for k, v := range params {
28+
// Check for duplicates
29+
if _, ok := seenCapabilityNames[k]; ok {
30+
return nil, fmt.Errorf("duplicate client capabilities parsed from `Accept:` header: '%s'",
31+
params)
32+
}
33+
seenCapabilityNames[k] = struct{}{}
34+
35+
capabilities = append(capabilities, &ClientCapability{Name: k, Value: v})
36+
}
37+
38+
return capabilities, nil
39+
}
40+
}
41+
42+
return []*ClientCapability{}, nil
43+
}
44+
45+
func GetClientCapability(capabilities []*ClientCapability, capabilityName string) *ClientCapability {
46+
for _, capability := range capabilities {
47+
if capability.Name == capabilityName {
48+
return capability
49+
}
50+
}
51+
return nil
52+
}

pkg/validation/validate.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ type LabelValidationLimits interface {
115115
}
116116

117117
// ValidateLabels validates the labels of a profile.
118-
func ValidateLabels(limits LabelValidationLimits, tenantID string, ls []*typesv1.LabelPair) error {
118+
func ValidateLabels(limits LabelValidationLimits, tenantID string, utf8LabelNamesEnabled bool, ls []*typesv1.LabelPair) error {
119119
if len(ls) == 0 {
120120
return NewErrorf(MissingLabels, MissingLabelsErrorMsg)
121121
}
@@ -146,7 +146,12 @@ func ValidateLabels(limits LabelValidationLimits, tenantID string, ls []*typesv1
146146
if len(l.Value) > limits.MaxLabelValueLength(tenantID) {
147147
return NewErrorf(LabelValueTooLong, LabelValueTooLongErrorMsg, phlaremodel.LabelPairsString(ls), l.Value)
148148
}
149-
if origName, newName, ok := SanitizeLabelName(l.Name); ok && origName != newName {
149+
// Note this conditional falls back on legacy logic if not valid utf-8 label name
150+
if ok := ValidateUtf8LabelName(l.Name); utf8LabelNamesEnabled && ok {
151+
idx += 1
152+
continue
153+
} else if origName, newName, ok := SanitizeLabelName(l.Name); ok && origName != newName {
154+
// Legacy logic if client does not specify utf8LabelNamesEnabled
150155
var err error
151156
ls, idx, err = handleSanitizedLabel(ls, idx, origName, newName)
152157
if err != nil {
@@ -211,6 +216,14 @@ func handleSanitizedLabel(ls []*typesv1.LabelPair, origIdx int, origName, newNam
211216
return ls[:len(newSlice)], finalIdx, nil
212217
}
213218

219+
// ValidateUtf8LabelName validates the name is not empty and is a valid utf-8 string
220+
func ValidateUtf8LabelName(name string) bool {
221+
if len(name) == 0 {
222+
return false
223+
}
224+
return utf8.ValidString(name)
225+
}
226+
214227
// SanitizeLabelName reports whether the label name is valid,
215228
// and returns the sanitized value.
216229
//

0 commit comments

Comments
 (0)