99
1010 googlev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
1111 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
12+ "github.com/grafana/pyroscope/pkg/featureflags"
1213 phlaremodel "github.com/grafana/pyroscope/pkg/model"
1314 "github.com/grafana/pyroscope/pkg/pprof"
1415)
@@ -136,7 +137,7 @@ func TestValidateLabels(t *testing.T) {
136137 },
137138 } {
138139 t .Run (tt .name , func (t * testing.T ) {
139- err := ValidateLabels (MockLimits {
140+ err := ValidateLabels (nil , MockLimits {
140141 MaxLabelNamesPerSeriesValue : 4 ,
141142 MaxLabelNameLengthValue : 12 ,
142143 MaxLabelValueLengthValue : 10 ,
@@ -152,6 +153,183 @@ func TestValidateLabels(t *testing.T) {
152153 }
153154}
154155
156+ func TestValidateLabels_WithClientCapabilities (t * testing.T ) {
157+ for _ , tt := range []struct {
158+ name string
159+ clientCapabilities * featureflags.ClientCapabilities
160+ lbs []* typesv1.LabelPair
161+ expectedErr string
162+ expectedReason Reason
163+ expectedLabels []* typesv1.LabelPair // Expected labels after validation/sanitization
164+ }{
165+ {
166+ name : "UTF-8 labels allowed when capabilities enabled" ,
167+ clientCapabilities : & featureflags.ClientCapabilities {
168+ AllowUtf8LabelNames : true ,
169+ },
170+ lbs : []* typesv1.LabelPair {
171+ {Name : model .MetricNameLabel , Value : "cpu" },
172+ {Name : phlaremodel .LabelNameServiceName , Value : "svc" },
173+ {Name : "日本語" , Value : "test" },
174+ {Name : "emoji_🚀" , Value : "rocket" },
175+ },
176+ // Labels get sorted alphabetically
177+ expectedLabels : []* typesv1.LabelPair {
178+ {Name : model .MetricNameLabel , Value : "cpu" },
179+ {Name : "emoji_🚀" , Value : "rocket" },
180+ {Name : phlaremodel .LabelNameServiceName , Value : "svc" },
181+ {Name : "日本語" , Value : "test" },
182+ },
183+ },
184+ {
185+ name : "UTF-8 labels rejected when capabilities disabled" ,
186+ clientCapabilities : nil ,
187+ lbs : []* typesv1.LabelPair {
188+ {Name : model .MetricNameLabel , Value : "cpu" },
189+ {Name : phlaremodel .LabelNameServiceName , Value : "svc" },
190+ {Name : "日本語" , Value : "test" },
191+ },
192+ expectedErr : `invalid labels '{__name__="cpu", service_name="svc", 日本語="test"}' with error: invalid label name '日本語'` ,
193+ expectedReason : InvalidLabels ,
194+ },
195+ {
196+ name : "UTF-8 labels rejected when AllowUtf8LabelNames false" ,
197+ clientCapabilities : & featureflags.ClientCapabilities {
198+ AllowUtf8LabelNames : false ,
199+ },
200+ lbs : []* typesv1.LabelPair {
201+ {Name : model .MetricNameLabel , Value : "cpu" },
202+ {Name : phlaremodel .LabelNameServiceName , Value : "svc" },
203+ {Name : "café" , Value : "test" },
204+ },
205+ expectedErr : `invalid labels '{__name__="cpu", café="test", service_name="svc"}' with error: invalid label name 'café'` ,
206+ expectedReason : InvalidLabels ,
207+ },
208+ {
209+ name : "valid underscore and hyphen labels with UTF-8 enabled" ,
210+ clientCapabilities : & featureflags.ClientCapabilities {
211+ AllowUtf8LabelNames : true ,
212+ },
213+ lbs : []* typesv1.LabelPair {
214+ {Name : model .MetricNameLabel , Value : "cpu" },
215+ {Name : phlaremodel .LabelNameServiceName , Value : "svc" },
216+ {Name : "label_with_underscore" , Value : "test" },
217+ {Name : "label-with-hyphen" , Value : "test2" },
218+ },
219+ expectedLabels : []* typesv1.LabelPair {
220+ {Name : model .MetricNameLabel , Value : "cpu" },
221+ {Name : "label-with-hyphen" , Value : "test2" },
222+ {Name : "label_with_underscore" , Value : "test" },
223+ {Name : phlaremodel .LabelNameServiceName , Value : "svc" },
224+ },
225+ },
226+ {
227+ name : "legacy invalid label names rejected when capabilities disabled" ,
228+ clientCapabilities : nil ,
229+ lbs : []* typesv1.LabelPair {
230+ {Name : model .MetricNameLabel , Value : "cpu" },
231+ {Name : phlaremodel .LabelNameServiceName , Value : "svc" },
232+ {Name : "123invalid" , Value : "test" },
233+ },
234+ expectedErr : `invalid labels '{123invalid="test", __name__="cpu", service_name="svc"}' with error: invalid label name '123invalid'` ,
235+ expectedReason : InvalidLabels ,
236+ },
237+ {
238+ name : "dots allowed as-is with UTF-8 enabled" ,
239+ clientCapabilities : & featureflags.ClientCapabilities {
240+ AllowUtf8LabelNames : true ,
241+ },
242+ lbs : []* typesv1.LabelPair {
243+ {Name : model .MetricNameLabel , Value : "cpu" },
244+ {Name : phlaremodel .LabelNameServiceName , Value : "svc" },
245+ {Name : "app.kubernetes.io/name" , Value : "test" },
246+ },
247+ // With UTF-8 enabled, dots are allowed (Prometheus UTF-8 validation)
248+ expectedLabels : []* typesv1.LabelPair {
249+ {Name : model .MetricNameLabel , Value : "cpu" },
250+ {Name : "app.kubernetes.io/name" , Value : "test" },
251+ {Name : phlaremodel .LabelNameServiceName , Value : "svc" },
252+ },
253+ },
254+ {
255+ name : "dots rejected without UTF-8" ,
256+ clientCapabilities : nil ,
257+ lbs : []* typesv1.LabelPair {
258+ {Name : model .MetricNameLabel , Value : "cpu" },
259+ {Name : phlaremodel .LabelNameServiceName , Value : "svc" },
260+ {Name : "app.kubernetes.io/name" , Value : "test" },
261+ },
262+ expectedErr : `invalid labels '{__name__="cpu", app.kubernetes.io/name="test", service_name="svc"}' with error: invalid label name 'app.kubernetes.io/name'` ,
263+ expectedReason : InvalidLabels ,
264+ },
265+ {
266+ name : "hyphens allowed with UTF-8 enabled" ,
267+ clientCapabilities : & featureflags.ClientCapabilities {
268+ AllowUtf8LabelNames : true ,
269+ },
270+ lbs : []* typesv1.LabelPair {
271+ {Name : model .MetricNameLabel , Value : "cpu" },
272+ {Name : phlaremodel .LabelNameServiceName , Value : "svc" },
273+ {Name : "some-label-name" , Value : "test" },
274+ },
275+ expectedLabels : []* typesv1.LabelPair {
276+ {Name : model .MetricNameLabel , Value : "cpu" },
277+ {Name : phlaremodel .LabelNameServiceName , Value : "svc" },
278+ {Name : "some-label-name" , Value : "test" },
279+ },
280+ },
281+ {
282+ name : "mixed ASCII and UTF-8 labels with capabilities enabled" ,
283+ clientCapabilities : & featureflags.ClientCapabilities {
284+ AllowUtf8LabelNames : true ,
285+ },
286+ lbs : []* typesv1.LabelPair {
287+ {Name : model .MetricNameLabel , Value : "cpu" },
288+ {Name : phlaremodel .LabelNameServiceName , Value : "svc" },
289+ {Name : "region" , Value : "us-east-1" },
290+ {Name : "地域" , Value : "東京" },
291+ {Name : "environment" , Value : "prod" },
292+ },
293+ // Labels get sorted alphabetically
294+ expectedLabels : []* typesv1.LabelPair {
295+ {Name : model .MetricNameLabel , Value : "cpu" },
296+ {Name : "environment" , Value : "prod" },
297+ {Name : "region" , Value : "us-east-1" },
298+ {Name : phlaremodel .LabelNameServiceName , Value : "svc" },
299+ {Name : "地域" , Value : "東京" },
300+ },
301+ },
302+ } {
303+ t .Run (tt .name , func (t * testing.T ) {
304+ lbsCopy := make ([]* typesv1.LabelPair , len (tt .lbs ))
305+ for i , lb := range tt .lbs {
306+ lbsCopy [i ] = & typesv1.LabelPair {Name : lb .Name , Value : lb .Value }
307+ }
308+
309+ err := ValidateLabels (tt .clientCapabilities , MockLimits {
310+ MaxLabelNamesPerSeriesValue : 10 ,
311+ MaxLabelNameLengthValue : 1024 ,
312+ MaxLabelValueLengthValue : 2048 ,
313+ }, "test-tenant" , lbsCopy )
314+
315+ if tt .expectedErr != "" {
316+ require .Error (t , err )
317+ require .Equal (t , tt .expectedErr , err .Error ())
318+ require .Equal (t , tt .expectedReason , ReasonOf (err ))
319+ } else {
320+ require .NoError (t , err )
321+ if tt .expectedLabels != nil {
322+ require .Equal (t , len (tt .expectedLabels ), len (lbsCopy ), "label count mismatch" )
323+ for i , expected := range tt .expectedLabels {
324+ require .Equal (t , expected .Name , lbsCopy [i ].Name , "label name mismatch at index %d" , i )
325+ require .Equal (t , expected .Value , lbsCopy [i ].Value , "label value mismatch at index %d" , i )
326+ }
327+ }
328+ }
329+ })
330+ }
331+ }
332+
155333func Test_ValidateRangeRequest (t * testing.T ) {
156334 now := model .Now ()
157335 for _ , tt := range []struct {
0 commit comments