Skip to content

Commit 343aff9

Browse files
committed
Add new limit: max_series_label_size_bytes
Signed-off-by: 🌲 Harry 🌊 John 🏔 <johrry@amazon.com>
1 parent 04bb64d commit 343aff9

File tree

8 files changed

+91
-1
lines changed

8 files changed

+91
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
* [FEATURE] Compactor: Added configurations for Azure MSI in blocks-storage, ruler-storage and alertmanager-storage. #4818
5050
* [FEATURE] Ruler: Add support to pass custom implementations of queryable and pusher. #4782
5151
* [FEATURE] Create OpenTelemetry Bridge for Tracing. Now cortex can send traces to multiple destinations using OTEL Collectors. #4834
52+
* [FEATURE] Distributor: Added a new limit `-validation.max-labela-size-bytes` allowing to limit the combined size of labels for each timeseries. #4848
5253
* [BUGFIX] Memberlist: Add join with no retrying when starting service. #4804
5354
* [BUGFIX] Ruler: Fix /ruler/rule_groups returns YAML with extra fields. #4767
5455

docs/configuration/config-file-reference.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2553,6 +2553,11 @@ The `limits_config` configures default and per-tenant limits imposed by Cortex s
25532553
# CLI flag: -validation.max-label-names-per-series
25542554
[max_label_names_per_series: <int> | default = 30]
25552555
2556+
# Maximum combined size in bytes of all labels and label values accepted for a
2557+
# series. 0 to disable the limit.
2558+
# CLI flag: -validation.max-labels-size-bytes
2559+
[max_series_label_size_bytes: <int> | default = 0]
2560+
25562561
# Maximum length accepted for metric metadata. Metadata refers to Metric Name,
25572562
# HELP and UNIT.
25582563
# CLI flag: -validation.max-metadata-length

pkg/distributor/distributor_test.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1680,6 +1680,34 @@ func BenchmarkDistributor_Push(b *testing.B) {
16801680
},
16811681
expectedErr: "label value too long",
16821682
},
1683+
"max label size bytes per series limit reached": {
1684+
prepareConfig: func(limits *validation.Limits) {
1685+
limits.MaxLabelsSizeBytes = 1024
1686+
},
1687+
prepareSeries: func() ([]labels.Labels, []cortexpb.Sample) {
1688+
metrics := make([]labels.Labels, numSeriesPerRequest)
1689+
samples := make([]cortexpb.Sample, numSeriesPerRequest)
1690+
1691+
for i := 0; i < numSeriesPerRequest; i++ {
1692+
lbls := labels.NewBuilder(labels.Labels{{Name: model.MetricNameLabel, Value: "foo"}})
1693+
for i := 0; i < 10; i++ {
1694+
lbls.Set(fmt.Sprintf("name_%d", i), fmt.Sprintf("value_%d", i))
1695+
}
1696+
1697+
// Add a label with a very long value.
1698+
lbls.Set("xxx", fmt.Sprintf("xxx_%0.2000d", 1))
1699+
1700+
metrics[i] = lbls.Labels()
1701+
samples[i] = cortexpb.Sample{
1702+
Value: float64(i),
1703+
TimestampMs: time.Now().UnixNano() / int64(time.Millisecond),
1704+
}
1705+
}
1706+
1707+
return metrics, samples
1708+
},
1709+
expectedErr: "series label size bytes exceeded",
1710+
},
16831711
"timestamp too old": {
16841712
prepareConfig: func(limits *validation.Limits) {
16851713
limits.RejectOldSamples = true
@@ -1770,7 +1798,7 @@ func BenchmarkDistributor_Push(b *testing.B) {
17701798
limits := validation.Limits{}
17711799
flagext.DefaultValues(&distributorCfg, &clientConfig, &limits)
17721800

1773-
limits.IngestionRate = 0 // Unlimited.
1801+
limits.IngestionRate = 10000000 // Unlimited.
17741802
testData.prepareConfig(&limits)
17751803

17761804
distributorCfg.ShardByAllLabels = true

pkg/util/validation/errors.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,26 @@ func newLabelValueTooLongError(series []cortexpb.LabelAdapter, labelValue string
6868
}
6969
}
7070

71+
// labelsSizeBytesExceededError is a customized ValidationError, in that the cause and the series are
72+
// formatted in different order in Error.
73+
type labelsSizeBytesExceededError struct {
74+
labelsSizeBytes int
75+
series []cortexpb.LabelAdapter
76+
limit int
77+
}
78+
79+
func (e *labelsSizeBytesExceededError) Error() string {
80+
return fmt.Sprintf("series label size bytes exceeded for metric (actual: %d, limit: %d) metric: %.200q", e.labelsSizeBytes, e.limit, formatLabelSet(e.series))
81+
}
82+
83+
func labelSizeBytesExceededError(series []cortexpb.LabelAdapter, labelsSizeBytes int, limit int) ValidationError {
84+
return &labelsSizeBytesExceededError{
85+
labelsSizeBytes: labelsSizeBytes,
86+
series: series,
87+
limit: limit,
88+
}
89+
}
90+
7191
func newInvalidLabelError(series []cortexpb.LabelAdapter, labelName string) ValidationError {
7292
return &genericValidationError{
7393
message: "sample invalid label: %.200q metric %.200q",

pkg/util/validation/limits.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type Limits struct {
4646
MaxLabelNameLength int `yaml:"max_label_name_length" json:"max_label_name_length"`
4747
MaxLabelValueLength int `yaml:"max_label_value_length" json:"max_label_value_length"`
4848
MaxLabelNamesPerSeries int `yaml:"max_label_names_per_series" json:"max_label_names_per_series"`
49+
MaxLabelsSizeBytes int `yaml:"max_series_label_size_bytes" json:"max_series_label_size_bytes"`
4950
MaxMetadataLength int `yaml:"max_metadata_length" json:"max_metadata_length"`
5051
RejectOldSamples bool `yaml:"reject_old_samples" json:"reject_old_samples"`
5152
RejectOldSamplesMaxAge model.Duration `yaml:"reject_old_samples_max_age" json:"reject_old_samples_max_age"`
@@ -126,6 +127,7 @@ func (l *Limits) RegisterFlags(f *flag.FlagSet) {
126127
f.IntVar(&l.MaxLabelNameLength, "validation.max-length-label-name", 1024, "Maximum length accepted for label names")
127128
f.IntVar(&l.MaxLabelValueLength, "validation.max-length-label-value", 2048, "Maximum length accepted for label value. This setting also applies to the metric name")
128129
f.IntVar(&l.MaxLabelNamesPerSeries, "validation.max-label-names-per-series", 30, "Maximum number of label names per series.")
130+
f.IntVar(&l.MaxLabelsSizeBytes, "validation.max-labels-size-bytes", 0, "Maximum combined size in bytes of all labels and label values accepted for a series. 0 to disable the limit.")
129131
f.IntVar(&l.MaxMetadataLength, "validation.max-metadata-length", 1024, "Maximum length accepted for metric metadata. Metadata refers to Metric Name, HELP and UNIT.")
130132
f.BoolVar(&l.RejectOldSamples, "validation.reject-old-samples", false, "Reject old samples.")
131133
_ = l.RejectOldSamplesMaxAge.Set("14d")
@@ -327,6 +329,11 @@ func (o *Overrides) MaxLabelNamesPerSeries(userID string) int {
327329
return o.getOverridesForUser(userID).MaxLabelNamesPerSeries
328330
}
329331

332+
// MaxLabelsSizeBytes returns maximum number of label/value pairs timeseries.
333+
func (o *Overrides) MaxLabelsSizeBytes(userID string) int {
334+
return o.getOverridesForUser(userID).MaxLabelsSizeBytes
335+
}
336+
330337
// MaxMetadataLength returns maximum length metadata can be. Metadata refers
331338
// to the Metric Name, HELP and UNIT.
332339
func (o *Overrides) MaxMetadataLength(userID string) int {

pkg/util/validation/limits_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,21 +92,25 @@ func TestOverridesManager_GetOverrides(t *testing.T) {
9292

9393
require.Equal(t, 100, ov.MaxLabelNamesPerSeries("user1"))
9494
require.Equal(t, 0, ov.MaxLabelValueLength("user1"))
95+
require.Equal(t, 0, ov.MaxLabelsSizeBytes("user1"))
9596

9697
// Update limits for tenant user1. We only update single field, the rest is copied from defaults.
9798
// (That is how limits work when loaded from YAML)
9899
l := defaults
99100
l.MaxLabelValueLength = 150
101+
l.MaxLabelsSizeBytes = 10
100102

101103
tenantLimits["user1"] = &l
102104

103105
// Checking whether overrides were enforced
104106
require.Equal(t, 100, ov.MaxLabelNamesPerSeries("user1"))
105107
require.Equal(t, 150, ov.MaxLabelValueLength("user1"))
108+
require.Equal(t, 10, ov.MaxLabelsSizeBytes("user1"))
106109

107110
// Verifying user2 limits are not impacted by overrides
108111
require.Equal(t, 100, ov.MaxLabelNamesPerSeries("user2"))
109112
require.Equal(t, 0, ov.MaxLabelValueLength("user2"))
113+
require.Equal(t, 0, ov.MaxLabelsSizeBytes("user2"))
110114
}
111115

112116
func TestLimitsLoadingFromYaml(t *testing.T) {

pkg/util/validation/validate.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const (
4444
duplicateLabelNames = "duplicate_label_names"
4545
labelsNotSorted = "labels_not_sorted"
4646
labelValueTooLong = "label_value_too_long"
47+
labelsSizeBytesExceeded = "labels_size_bytes_exceeded"
4748

4849
// Exemplar-specific validation reasons
4950
exemplarLabelsMissing = "exemplar_labels_missing"
@@ -168,6 +169,7 @@ type LabelValidationConfig interface {
168169
MaxLabelNamesPerSeries(userID string) int
169170
MaxLabelNameLength(userID string) int
170171
MaxLabelValueLength(userID string) int
172+
MaxLabelsSizeBytes(userID string) int
171173
}
172174

173175
// ValidateLabels returns an err if the labels are invalid.
@@ -195,6 +197,9 @@ func ValidateLabels(cfg LabelValidationConfig, userID string, ls []cortexpb.Labe
195197
maxLabelNameLength := cfg.MaxLabelNameLength(userID)
196198
maxLabelValueLength := cfg.MaxLabelValueLength(userID)
197199
lastLabelName := ""
200+
maxLabelsSizeBytes := cfg.MaxLabelsSizeBytes(userID)
201+
labelsSizeBytes := 0
202+
198203
for _, l := range ls {
199204
if !skipLabelNameValidation && !model.LabelName(l.Name).IsValid() {
200205
DiscardedSamples.WithLabelValues(invalidLabel, userID).Inc()
@@ -216,6 +221,11 @@ func ValidateLabels(cfg LabelValidationConfig, userID string, ls []cortexpb.Labe
216221
}
217222

218223
lastLabelName = l.Name
224+
labelsSizeBytes += l.Size()
225+
}
226+
if maxLabelsSizeBytes > 0 && labelsSizeBytes > maxLabelsSizeBytes {
227+
DiscardedSamples.WithLabelValues(labelsSizeBytesExceeded, userID).Inc()
228+
return labelSizeBytesExceededError(ls, labelsSizeBytes, maxLabelsSizeBytes)
219229
}
220230
return nil
221231
}

pkg/util/validation/validate_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type validateLabelsCfg struct {
2121
maxLabelNamesPerSeries int
2222
maxLabelNameLength int
2323
maxLabelValueLength int
24+
maxLabelsSizeBytes int
2425
}
2526

2627
func (v validateLabelsCfg) EnforceMetricName(userID string) bool {
@@ -39,6 +40,10 @@ func (v validateLabelsCfg) MaxLabelValueLength(userID string) int {
3940
return v.maxLabelValueLength
4041
}
4142

43+
func (v validateLabelsCfg) MaxLabelsSizeBytes(userID string) int {
44+
return v.maxLabelsSizeBytes
45+
}
46+
4247
type validateMetadataCfg struct {
4348
enforceMetadataMetricName bool
4449
maxMetadataLength int
@@ -59,6 +64,7 @@ func TestValidateLabels(t *testing.T) {
5964
cfg.maxLabelValueLength = 25
6065
cfg.maxLabelNameLength = 25
6166
cfg.maxLabelNamesPerSeries = 2
67+
cfg.maxLabelsSizeBytes = 90
6268
cfg.enforceMetricName = true
6369

6470
for _, c := range []struct {
@@ -114,6 +120,14 @@ func TestValidateLabels(t *testing.T) {
114120
{Name: "blip", Value: "blop"},
115121
}, 2),
116122
},
123+
{
124+
map[model.LabelName]model.LabelValue{model.MetricNameLabel: "exactly_twenty_five_chars", "exactly_twenty_five_chars": "exactly_twenty_five_chars"},
125+
false,
126+
labelSizeBytesExceededError([]cortexpb.LabelAdapter{
127+
{Name: model.MetricNameLabel, Value: "exactly_twenty_five_chars"},
128+
{Name: "exactly_twenty_five_chars", Value: "exactly_twenty_five_chars"},
129+
}, 91, cfg.maxLabelsSizeBytes),
130+
},
117131
{
118132
map[model.LabelName]model.LabelValue{model.MetricNameLabel: "foo", "invalid%label&name": "bar"},
119133
true,
@@ -135,6 +149,7 @@ func TestValidateLabels(t *testing.T) {
135149
cortex_discarded_samples_total{reason="max_label_names_per_series",user="testUser"} 1
136150
cortex_discarded_samples_total{reason="metric_name_invalid",user="testUser"} 1
137151
cortex_discarded_samples_total{reason="missing_metric_name",user="testUser"} 1
152+
cortex_discarded_samples_total{reason="labels_size_bytes_exceeded",user="testUser"} 1
138153
139154
cortex_discarded_samples_total{reason="random reason",user="different user"} 1
140155
`), "cortex_discarded_samples_total"))

0 commit comments

Comments
 (0)