Skip to content

Commit acf806f

Browse files
committed
Add name_validation_scheme limit config
Signed-off-by: SungJin1212 <tjdwls1201@gmail.com>
1 parent 988a8b1 commit acf806f

File tree

10 files changed

+260
-8
lines changed

10 files changed

+260
-8
lines changed

docs/configuration/config-file-reference.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4344,6 +4344,11 @@ query_rejection:
43444344

43454345
# list of rule groups to disable
43464346
[disabled_rule_groups: <list of DisabledRuleGroup> | default = []]
4347+
4348+
# Name validation scheme for metric names and label names, Support values are:
4349+
# legacy, utf8.
4350+
# CLI flag: -validation.name-validation-scheme
4351+
[name_validation_scheme: <int> | default = legacy]
43474352
```
43484353
43494354
### `memberlist_config`

integration/e2e/service.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/go-kit/log"
1616
"github.com/pkg/errors"
1717
"github.com/prometheus/common/expfmt"
18+
"github.com/prometheus/common/model"
1819
"github.com/thanos-io/thanos/pkg/runutil"
1920

2021
"github.com/cortexproject/cortex/pkg/util/backoff"
@@ -623,7 +624,7 @@ func (s *HTTPService) SumMetrics(metricNames []string, opts ...MetricsOption) ([
623624
return nil, err
624625
}
625626

626-
var tp expfmt.TextParser
627+
tp := expfmt.NewTextParser(model.LegacyValidation)
627628
families, err := tp.TextToMetricFamilies(strings.NewReader(metrics))
628629
if err != nil {
629630
return nil, err
@@ -670,7 +671,7 @@ func (s *HTTPService) WaitRemovedMetric(metricName string, opts ...MetricsOption
670671
}
671672

672673
// Parse metrics.
673-
var tp expfmt.TextParser
674+
tp := expfmt.NewTextParser(model.LegacyValidation)
674675
families, err := tp.TextToMetricFamilies(strings.NewReader(metrics))
675676
if err != nil {
676677
return err

integration/utf8_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//go:build requires_docker
2+
// +build requires_docker
3+
4+
package integration
5+
6+
import (
7+
"fmt"
8+
"path/filepath"
9+
"testing"
10+
"time"
11+
12+
"github.com/prometheus/prometheus/prompb"
13+
"github.com/stretchr/testify/require"
14+
15+
"github.com/cortexproject/cortex/integration/e2e"
16+
e2edb "github.com/cortexproject/cortex/integration/e2e/db"
17+
"github.com/cortexproject/cortex/integration/e2ecortex"
18+
)
19+
20+
func Test_RulerExternalLabels_UTF8Validation(t *testing.T) {
21+
s, err := e2e.NewScenario(networkName)
22+
require.NoError(t, err)
23+
defer s.Close()
24+
25+
// Start dependencies.
26+
minio := e2edb.NewMinio(9000, bucketName)
27+
require.NoError(t, s.StartAndWaitReady(minio))
28+
29+
runtimeConfigYamlFile := `
30+
overrides:
31+
'user-2':
32+
name_validation_scheme: utf8
33+
ruler_external_labels:
34+
test.utf8.metric: 😄
35+
`
36+
require.NoError(t, writeFileToSharedDir(s, runtimeConfigFile, []byte(runtimeConfigYamlFile)))
37+
filePath := filepath.Join(e2e.ContainerSharedDir, runtimeConfigFile)
38+
39+
// Start Cortex components.
40+
require.NoError(t, copyFileToSharedDir(s, "docs/configuration/single-process-config-blocks.yaml", cortexConfigFile))
41+
42+
flags := map[string]string{
43+
"-auth.enabled": "true",
44+
"-runtime-config.file": filePath,
45+
"-runtime-config.backend": "filesystem",
46+
// ingester
47+
"-blocks-storage.s3.access-key-id": e2edb.MinioAccessKey,
48+
"-blocks-storage.s3.secret-access-key": e2edb.MinioSecretKey,
49+
"-blocks-storage.s3.bucket-name": bucketName,
50+
"-blocks-storage.s3.endpoint": fmt.Sprintf("%s-minio-9000:9000", networkName),
51+
"-blocks-storage.s3.insecure": "true",
52+
// alert manager
53+
"-alertmanager.web.external-url": "http://localhost/alertmanager",
54+
"-alertmanager-storage.backend": "local",
55+
"-alertmanager-storage.local.path": filepath.Join(e2e.ContainerSharedDir, "alertmanager_configs"),
56+
}
57+
// make alert manager config dir
58+
require.NoError(t, writeFileToSharedDir(s, "alertmanager_configs", []byte{}))
59+
60+
// The external labels validation should be success
61+
cortex := e2ecortex.NewSingleBinaryWithConfigFile("cortex-1", cortexConfigFile, flags, "", 9009, 9095)
62+
require.NoError(t, s.StartAndWaitReady(cortex))
63+
}
64+
65+
func Test_Distributor_UTF8ValidationPerTenant(t *testing.T) {
66+
s, err := e2e.NewScenario(networkName)
67+
require.NoError(t, err)
68+
defer s.Close()
69+
70+
// Start dependencies.
71+
minio := e2edb.NewMinio(9000, bucketName)
72+
require.NoError(t, s.StartAndWaitReady(minio))
73+
74+
runtimeConfigYamlFile := `
75+
overrides:
76+
'user-2':
77+
name_validation_scheme: utf8
78+
`
79+
80+
require.NoError(t, writeFileToSharedDir(s, runtimeConfigFile, []byte(runtimeConfigYamlFile)))
81+
filePath := filepath.Join(e2e.ContainerSharedDir, runtimeConfigFile)
82+
83+
// Start Cortex components.
84+
require.NoError(t, copyFileToSharedDir(s, "docs/configuration/single-process-config-blocks.yaml", cortexConfigFile))
85+
86+
flags := map[string]string{
87+
"-auth.enabled": "true",
88+
"-runtime-config.file": filePath,
89+
"-runtime-config.backend": "filesystem",
90+
// ingester
91+
"-blocks-storage.s3.access-key-id": e2edb.MinioAccessKey,
92+
"-blocks-storage.s3.secret-access-key": e2edb.MinioSecretKey,
93+
"-blocks-storage.s3.bucket-name": bucketName,
94+
"-blocks-storage.s3.endpoint": fmt.Sprintf("%s-minio-9000:9000", networkName),
95+
"-blocks-storage.s3.insecure": "true",
96+
// alert manager
97+
"-alertmanager.web.external-url": "http://localhost/alertmanager",
98+
"-alertmanager-storage.backend": "local",
99+
"-alertmanager-storage.local.path": filepath.Join(e2e.ContainerSharedDir, "alertmanager_configs"),
100+
}
101+
// make alert manager config dir
102+
require.NoError(t, writeFileToSharedDir(s, "alertmanager_configs", []byte{}))
103+
104+
cortex := e2ecortex.NewSingleBinaryWithConfigFile("cortex-1", cortexConfigFile, flags, "", 9009, 9095)
105+
require.NoError(t, s.StartAndWaitReady(cortex))
106+
107+
// user-1 uses legacy validation
108+
user1Client, err := e2ecortex.NewClient(cortex.HTTPEndpoint(), "", "", "", "user-1")
109+
require.NoError(t, err)
110+
111+
// user-2 uses utf8 validation
112+
user2Client, err := e2ecortex.NewClient(cortex.HTTPEndpoint(), "", "", "", "user-2")
113+
require.NoError(t, err)
114+
115+
now := time.Now()
116+
117+
utf8Series, _ := generateSeries("series_1", now, prompb.Label{Name: "test.utf8.metric", Value: "😄"})
118+
legacySeries, _ := generateSeries("series_2", now, prompb.Label{Name: "job", Value: "test"})
119+
120+
res, err := user1Client.Push(legacySeries)
121+
require.NoError(t, err)
122+
require.Equal(t, 200, res.StatusCode)
123+
124+
// utf8Series push should be fail for user-1
125+
res, err = user1Client.Push(utf8Series)
126+
require.NoError(t, err)
127+
require.Equal(t, 400, res.StatusCode)
128+
129+
res, err = user2Client.Push(legacySeries)
130+
require.NoError(t, err)
131+
require.Equal(t, 200, res.StatusCode)
132+
133+
// utf8Series push should be success for user-2
134+
res, err = user2Client.Push(utf8Series)
135+
require.NoError(t, err)
136+
require.Equal(t, 200, res.StatusCode)
137+
}

pkg/ruler/external_labels_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ func TestUserExternalLabels(t *testing.T) {
3939
userExternalLabels: labels.FromStrings("from", "cloud", "tag", "local"),
4040
expectedExternalLabels: labels.FromStrings("from", "cloud", "tag", "local"),
4141
},
42+
{
43+
name: "utf8",
44+
removeBeforeTest: true,
45+
exists: false,
46+
userExternalLabels: labels.FromStrings("test.utf8.metric", "😄"),
47+
expectedExternalLabels: labels.FromStrings("from", "cortex", "test.utf8.metric", "😄"),
48+
},
4249
}
4350

4451
const userID = "test-user"

pkg/util/validation/exporter.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,13 @@ func ExtractNumericalValues(l *Limits) map[string]float64 {
6464

6565
switch field.Kind() {
6666
case reflect.Int, reflect.Int64:
67-
if field.Type().String() == "model.Duration" {
67+
switch fieldType.Type.String() {
68+
case "model.Duration":
6869
// we export the model.Duration in seconds
6970
metrics[tag] = time.Duration(field.Int()).Seconds()
70-
} else {
71+
case "model.ValidationScheme":
72+
// skip
73+
default:
7174
metrics[tag] = float64(field.Int())
7275
}
7376
case reflect.Uint, reflect.Uint64:

pkg/util/validation/limits.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ type Limits struct {
245245
AlertmanagerMaxSilencesCount int `yaml:"alertmanager_max_silences_count" json:"alertmanager_max_silences_count"`
246246
AlertmanagerMaxSilencesSizeBytes int `yaml:"alertmanager_max_silences_size_bytes" json:"alertmanager_max_silences_size_bytes"`
247247
DisabledRuleGroups DisabledRuleGroups `yaml:"disabled_rule_groups" json:"disabled_rule_groups" doc:"nocli|description=list of rule groups to disable"`
248+
249+
NameValidationScheme model.ValidationScheme `yaml:"name_validation_scheme" json:"name_validation_scheme"`
248250
}
249251

250252
// RegisterFlags adds the flags required to config this to the given FlagSet
@@ -354,6 +356,9 @@ func (l *Limits) RegisterFlags(f *flag.FlagSet) {
354356
f.IntVar(&l.AlertmanagerMaxAlertsSizeBytes, "alertmanager.max-alerts-size-bytes", 0, "Maximum total size of alerts that a single user can have, alert size is the sum of the bytes of its labels, annotations and generatorURL. Inserting more alerts will fail with a log message and metric increment. 0 = no limit.")
355357
f.IntVar(&l.AlertmanagerMaxSilencesCount, "alertmanager.max-silences-count", 0, "Maximum number of silences that a single user can have, including expired silences. 0 = no limit.")
356358
f.IntVar(&l.AlertmanagerMaxSilencesSizeBytes, "alertmanager.max-silences-size-bytes", 0, "Maximum size of individual silences that a single user can have. 0 = no limit.")
359+
360+
_ = l.NameValidationScheme.Set(model.LegacyValidation.String())
361+
f.Var(&l.NameValidationScheme, "validation.name-validation-scheme", fmt.Sprintf("Name validation scheme for metric names and label names, Support values are: %s.", strings.Join([]string{model.LegacyValidation.String(), model.UTF8Validation.String()}, ", ")))
357362
}
358363

359364
// Validate the limits config and returns an error if the validation
@@ -376,8 +381,18 @@ func (l *Limits) Validate(shardByAllLabels bool, activeSeriesMetricsEnabled bool
376381
return errMaxLocalNativeHistogramSeriesPerUserValidation
377382
}
378383

384+
var nameValidationScheme model.ValidationScheme
385+
switch l.NameValidationScheme {
386+
case model.LegacyValidation, model.UTF8Validation:
387+
nameValidationScheme = l.NameValidationScheme
388+
case model.UnsetValidation:
389+
nameValidationScheme = model.LegacyValidation
390+
default:
391+
return fmt.Errorf("unsupported name validation scheme: %s", l.NameValidationScheme)
392+
}
393+
379394
if err := l.RulerExternalLabels.Validate(func(l labels.Label) error {
380-
if !model.LabelName(l.Name).IsValid() {
395+
if !nameValidationScheme.IsValidLabelName(l.Name) {
381396
return fmt.Errorf("%w: %q", errInvalidLabelName, l.Name)
382397
}
383398
if !model.LabelValue(l.Value).IsValid() {

pkg/util/validation/limits_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ func TestLimits_Validate(t *testing.T) {
123123
limits: Limits{RulerExternalLabels: labels.FromStrings("good", string([]byte{0xff, 0xfe, 0xfd}))},
124124
expected: errInvalidLabelValue,
125125
},
126+
"utf8: external-labels utf8 label name and value": {
127+
limits: Limits{NameValidationScheme: model.UTF8Validation, RulerExternalLabels: labels.FromStrings("test.utf8.metric", "😄")},
128+
expected: nil,
129+
},
130+
"utf8: external-labels invalid label name": {
131+
limits: Limits{NameValidationScheme: model.UTF8Validation, RulerExternalLabels: labels.FromStrings("test.\xc5.metric", "😄")},
132+
expected: errInvalidLabelName,
133+
},
134+
"utf8: external-labels invalid label value": {
135+
limits: Limits{NameValidationScheme: model.UTF8Validation, RulerExternalLabels: labels.FromStrings("test.utf8.metric", "test.\xc5.value")},
136+
expected: errInvalidLabelValue,
137+
},
126138
}
127139

128140
for testName, testData := range tests {
@@ -245,7 +257,7 @@ limits_per_label_set:
245257
err := yaml.Unmarshal([]byte(inputYAML), &limitsYAML)
246258
require.NoError(t, err)
247259
require.Len(t, limitsYAML.LimitsPerLabelSet, 1)
248-
require.Len(t, limitsYAML.LimitsPerLabelSet[0].LabelSet, 1)
260+
require.Equal(t, 1, limitsYAML.LimitsPerLabelSet[0].LabelSet.Len())
249261
require.Equal(t, limitsYAML.LimitsPerLabelSet[0].Limits.MaxSeries, 10)
250262

251263
duplicatedInputYAML := `

pkg/util/validation/validate.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ func ValidateLabels(validateMetrics *ValidateMetrics, limits *Limits, userID str
285285
return newNoMetricNameError()
286286
}
287287

288-
if !model.IsValidMetricName(model.LabelValue(unsafeMetricName)) {
288+
if !limits.NameValidationScheme.IsValidLabelName(unsafeMetricName) {
289289
validateMetrics.DiscardedSamples.WithLabelValues(invalidMetricName, userID).Inc()
290290
return newInvalidMetricNameError(unsafeMetricName)
291291
}
@@ -304,7 +304,7 @@ func ValidateLabels(validateMetrics *ValidateMetrics, limits *Limits, userID str
304304
labelsSizeBytes := 0
305305

306306
for _, l := range ls {
307-
if !skipLabelNameValidation && !model.LabelName(l.Name).IsValid() {
307+
if !skipLabelNameValidation && !limits.NameValidationScheme.IsValidLabelName(l.Name) {
308308
validateMetrics.DiscardedSamples.WithLabelValues(invalidLabel, userID).Inc()
309309
return newInvalidLabelError(ls, l.Name)
310310
} else if len(l.Name) > maxLabelNameLength {

pkg/util/validation/validate_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,69 @@ import (
2121
util_log "github.com/cortexproject/cortex/pkg/util/log"
2222
)
2323

24+
func TestValidateLabels_UTF8(t *testing.T) {
25+
cfg := new(Limits)
26+
userID := "testUser"
27+
28+
reg := prometheus.NewRegistry()
29+
validateMetrics := NewValidateMetrics(reg)
30+
31+
cfg.MaxLabelValueLength = 25
32+
cfg.MaxLabelNameLength = 25
33+
cfg.MaxLabelNamesPerSeries = 2
34+
cfg.MaxLabelsSizeBytes = 90
35+
cfg.EnforceMetricName = true
36+
cfg.NameValidationScheme = model.UTF8Validation
37+
38+
tests := []struct {
39+
description string
40+
metric model.Metric
41+
skipLabelNameValidation bool
42+
expectedErr error
43+
}{
44+
{
45+
description: "empty metric name",
46+
metric: map[model.LabelName]model.LabelValue{},
47+
skipLabelNameValidation: false,
48+
expectedErr: newNoMetricNameError(),
49+
},
50+
{
51+
description: "utf8 metric name",
52+
metric: map[model.LabelName]model.LabelValue{model.MetricNameLabel: "test.utf8.metric"},
53+
skipLabelNameValidation: false,
54+
expectedErr: nil,
55+
},
56+
{
57+
description: "invalid utf8 metric name",
58+
metric: map[model.LabelName]model.LabelValue{model.MetricNameLabel: "test.\xc5.metric"},
59+
skipLabelNameValidation: false,
60+
expectedErr: newInvalidMetricNameError("test.\xc5.metric"),
61+
},
62+
{
63+
description: "invalid utf8 label name, but skipLabelNameValidation is true",
64+
metric: map[model.LabelName]model.LabelValue{model.MetricNameLabel: "test.utf8.metric", "label1": "test.\xc5.label"},
65+
skipLabelNameValidation: true,
66+
expectedErr: nil,
67+
},
68+
{
69+
description: "invalid utf8 label name, but skipLabelNameValidation is false",
70+
metric: map[model.LabelName]model.LabelValue{model.MetricNameLabel: "test.utf8.metric", "test.\xc5.label": "value"},
71+
skipLabelNameValidation: false,
72+
expectedErr: newInvalidLabelError([]cortexpb.LabelAdapter{
73+
{Name: model.MetricNameLabel, Value: "test.utf8.metric"},
74+
{Name: "test.\xc5.label", Value: "value"},
75+
}, "test.\xc5.label"),
76+
},
77+
}
78+
79+
for _, test := range tests {
80+
t.Run(test.description, func(t *testing.T) {
81+
err := ValidateLabels(validateMetrics, cfg, userID, cortexpb.FromMetricsToLabelAdapters(test.metric), test.skipLabelNameValidation)
82+
assert.Equal(t, test.expectedErr, err, "wrong error")
83+
})
84+
}
85+
}
86+
2487
func TestValidateLabels(t *testing.T) {
2588
cfg := new(Limits)
2689
userID := "testUser"
@@ -33,6 +96,7 @@ func TestValidateLabels(t *testing.T) {
3396
cfg.MaxLabelNamesPerSeries = 2
3497
cfg.MaxLabelsSizeBytes = 90
3598
cfg.EnforceMetricName = true
99+
cfg.NameValidationScheme = model.LegacyValidation
36100
cfg.LimitsPerLabelSet = []LimitsPerLabelSet{
37101
{
38102
Limits: LimitsPerLabelSetEntry{MaxSeries: 0},
@@ -271,6 +335,7 @@ func TestValidateLabelOrder(t *testing.T) {
271335
cfg.MaxLabelNameLength = 10
272336
cfg.MaxLabelNamesPerSeries = 10
273337
cfg.MaxLabelValueLength = 10
338+
cfg.NameValidationScheme = model.LegacyValidation
274339
reg := prometheus.NewRegistry()
275340
validateMetrics := NewValidateMetrics(reg)
276341
userID := "testUser"
@@ -299,6 +364,7 @@ func TestValidateLabelDuplication(t *testing.T) {
299364
cfg.MaxLabelNameLength = 10
300365
cfg.MaxLabelNamesPerSeries = 10
301366
cfg.MaxLabelValueLength = 10
367+
cfg.NameValidationScheme = model.LegacyValidation
302368
reg := prometheus.NewRegistry()
303369
validateMetrics := NewValidateMetrics(reg)
304370
userID := "testUser"

schemas/cortex-config-schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5184,6 +5184,12 @@
51845184
"description": "List of metric relabel configurations. Note that in most situations, it is more effective to use metrics relabeling directly in the Prometheus server, e.g. remote_write.write_relabel_configs.",
51855185
"type": "string"
51865186
},
5187+
"name_validation_scheme": {
5188+
"default": "legacy",
5189+
"description": "Name validation scheme for metric names and label names, Support values are: legacy, utf8.",
5190+
"type": "number",
5191+
"x-cli-flag": "validation.name-validation-scheme"
5192+
},
51875193
"native_histogram_ingestion_burst_size": {
51885194
"default": 0,
51895195
"description": "Per-user allowed native histogram ingestion burst size (in number of samples)",

0 commit comments

Comments
 (0)