diff --git a/cmd/promtool/main.go b/cmd/promtool/main.go index a2f710d147..a72183f701 100644 --- a/cmd/promtool/main.go +++ b/cmd/promtool/main.go @@ -631,9 +631,9 @@ func checkRules(filename string, lintSettings lintConfig) (int, []error) { errMessage := fmt.Sprintf("%d duplicate rule(s) found.\n", len(dRules)) for _, n := range dRules { errMessage += fmt.Sprintf("Metric: %s\nLabel(s):\n", n.metric) - for _, l := range n.label { + n.label.Range(func(l labels.Label) { errMessage += fmt.Sprintf("\t%s: %s\n", l.Name, l.Value) - } + }) } errMessage += "Might cause inconsistency while recording expressions" return 0, []error{fmt.Errorf("%w %s", lintError, errMessage)} diff --git a/cmd/promtool/rules.go b/cmd/promtool/rules.go index 4dbef34ebb..2b5ed1d781 100644 --- a/cmd/promtool/rules.go +++ b/cmd/promtool/rules.go @@ -158,14 +158,15 @@ func (importer *ruleImporter) importRule(ctx context.Context, ruleExpr, ruleName // Setting the rule labels after the output of the query, // so they can override query output. - for _, l := range ruleLabels { + ruleLabels.Range(func(l labels.Label) { lb.Set(l.Name, l.Value) - } + }) lb.Set(labels.MetricName, ruleName) + lbls := lb.Labels(labels.EmptyLabels()) for _, value := range sample.Values { - if err := app.add(ctx, lb.Labels(nil), timestamp.FromTime(value.Timestamp.Time()), float64(value.Value)); err != nil { + if err := app.add(ctx, lbls, timestamp.FromTime(value.Timestamp.Time()), float64(value.Value)); err != nil { return fmt.Errorf("add: %w", err) } } diff --git a/cmd/promtool/rules_test.go b/cmd/promtool/rules_test.go index caa930616a..fb582ed0dd 100644 --- a/cmd/promtool/rules_test.go +++ b/cmd/promtool/rules_test.go @@ -100,7 +100,7 @@ func TestBackfillRuleIntegration(t *testing.T) { require.Equal(t, 1, len(gRules)) require.Equal(t, "rule1", gRules[0].Name()) require.Equal(t, "ruleExpr", gRules[0].Query().String()) - require.Equal(t, 1, len(gRules[0].Labels())) + require.Equal(t, 1, gRules[0].Labels().Len()) group2 := ruleImporter.groups[path2+";group2"] require.NotNil(t, group2) @@ -109,7 +109,7 @@ func TestBackfillRuleIntegration(t *testing.T) { require.Equal(t, 2, len(g2Rules)) require.Equal(t, "grp2_rule1", g2Rules[0].Name()) require.Equal(t, "grp2_rule1_expr", g2Rules[0].Query().String()) - require.Equal(t, 0, len(g2Rules[0].Labels())) + require.Equal(t, 0, g2Rules[0].Labels().Len()) // Backfill all recording rules then check the blocks to confirm the correct data was created. errs = ruleImporter.importAll(ctx) @@ -132,12 +132,12 @@ func TestBackfillRuleIntegration(t *testing.T) { for selectedSeries.Next() { seriesCount++ series := selectedSeries.At() - if len(series.Labels()) != 3 { - require.Equal(t, 2, len(series.Labels())) + if series.Labels().Len() != 3 { + require.Equal(t, 2, series.Labels().Len()) x := labels.FromStrings("__name__", "grp2_rule1", "name1", "val1") require.Equal(t, x, series.Labels()) } else { - require.Equal(t, 3, len(series.Labels())) + require.Equal(t, 3, series.Labels().Len()) } it := series.Iterator(nil) for it.Next() == chunkenc.ValFloat { diff --git a/cmd/promtool/tsdb.go b/cmd/promtool/tsdb.go index 91b97f5c51..36e33049d1 100644 --- a/cmd/promtool/tsdb.go +++ b/cmd/promtool/tsdb.go @@ -315,7 +315,7 @@ func readPrometheusLabels(r io.Reader, n int) ([]labels.Labels, error) { i := 0 for scanner.Scan() && i < n { - m := make(labels.Labels, 0, 10) + m := make([]labels.Label, 0, 10) r := strings.NewReplacer("\"", "", "{", "", "}", "") s := r.Replace(scanner.Text()) @@ -325,13 +325,12 @@ func readPrometheusLabels(r io.Reader, n int) ([]labels.Labels, error) { split := strings.Split(labelChunk, ":") m = append(m, labels.Label{Name: split[0], Value: split[1]}) } - // Order of the k/v labels matters, don't assume we'll always receive them already sorted. - sort.Sort(m) - h := m.Hash() + ml := labels.New(m...) // This sorts by name - order of the k/v labels matters, don't assume we'll always receive them already sorted. + h := ml.Hash() if _, ok := hashes[h]; ok { continue } - mets = append(mets, m) + mets = append(mets, ml) hashes[h] = struct{}{} i++ } @@ -470,21 +469,21 @@ func analyzeBlock(path, blockID string, limit int, runExtended bool) error { if err != nil { return err } - lbls := labels.Labels{} chks := []chunks.Meta{} + builder := labels.ScratchBuilder{} for p.Next() { - if err = ir.Series(p.At(), &lbls, &chks); err != nil { + if err = ir.Series(p.At(), &builder, &chks); err != nil { return err } // Amount of the block time range not covered by this series. uncovered := uint64(meta.MaxTime-meta.MinTime) - uint64(chks[len(chks)-1].MaxTime-chks[0].MinTime) - for _, lbl := range lbls { + builder.Labels().Range(func(lbl labels.Label) { key := lbl.Name + "=" + lbl.Value labelsUncovered[lbl.Name] += uncovered labelpairsUncovered[key] += uncovered labelpairsCount[key]++ entries++ - } + }) } if p.Err() != nil { return p.Err() @@ -589,10 +588,10 @@ func analyzeCompaction(block tsdb.BlockReader, indexr tsdb.IndexReader) (err err nBuckets := 10 histogram := make([]int, nBuckets) totalChunks := 0 + var builder labels.ScratchBuilder for postingsr.Next() { - lbsl := labels.Labels{} var chks []chunks.Meta - if err := indexr.Series(postingsr.At(), &lbsl, &chks); err != nil { + if err := indexr.Series(postingsr.At(), &builder, &chks); err != nil { return err } diff --git a/cmd/promtool/unittest.go b/cmd/promtool/unittest.go index f6b16a6bbe..cc40ac9d02 100644 --- a/cmd/promtool/unittest.go +++ b/cmd/promtool/unittest.go @@ -284,8 +284,8 @@ func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]i for _, a := range ar.ActiveAlerts() { if a.State == rules.StateFiring { alerts = append(alerts, labelAndAnnotation{ - Labels: append(labels.Labels{}, a.Labels...), - Annotations: append(labels.Labels{}, a.Annotations...), + Labels: a.Labels.Copy(), + Annotations: a.Annotations.Copy(), }) } } diff --git a/config/config.go b/config/config.go index 8e8460d4c5..8eb88eb51a 100644 --- a/config/config.go +++ b/config/config.go @@ -80,7 +80,8 @@ func Load(s string, expandExternalLabels bool, logger log.Logger) (*Config, erro return cfg, nil } - for i, v := range cfg.GlobalConfig.ExternalLabels { + b := labels.ScratchBuilder{} + cfg.GlobalConfig.ExternalLabels.Range(func(v labels.Label) { newV := os.Expand(v.Value, func(s string) string { if s == "$" { return "$" @@ -93,10 +94,10 @@ func Load(s string, expandExternalLabels bool, logger log.Logger) (*Config, erro }) if newV != v.Value { level.Debug(logger).Log("msg", "External label replaced", "label", v.Name, "input", v.Value, "output", newV) - v.Value = newV - cfg.GlobalConfig.ExternalLabels[i] = v } - } + b.Add(v.Name, newV) + }) + cfg.GlobalConfig.ExternalLabels = b.Labels() return cfg, nil } @@ -361,13 +362,16 @@ func (c *GlobalConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } - for _, l := range gc.ExternalLabels { + if err := gc.ExternalLabels.Validate(func(l labels.Label) error { if !model.LabelName(l.Name).IsValid() { return fmt.Errorf("%q is not a valid label name", l.Name) } if !model.LabelValue(l.Value).IsValid() { return fmt.Errorf("%q is not a valid label value", l.Value) } + return nil + }); err != nil { + return err } // First set the correct scrape interval, then check that the timeout @@ -394,7 +398,7 @@ func (c *GlobalConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { // isZero returns true iff the global config is the zero value. func (c *GlobalConfig) isZero() bool { - return c.ExternalLabels == nil && + return c.ExternalLabels.IsEmpty() && c.ScrapeInterval == 0 && c.ScrapeTimeout == 0 && c.EvaluationInterval == 0 && diff --git a/model/labels/labels.go b/model/labels/labels.go index aafba218aa..36a0e6cb35 100644 --- a/model/labels/labels.go +++ b/model/labels/labels.go @@ -357,9 +357,7 @@ func EmptyLabels() Labels { // The caller has to guarantee that all label names are unique. func New(ls ...Label) Labels { set := make(Labels, 0, len(ls)) - for _, l := range ls { - set = append(set, l) - } + set = append(set, ls...) sort.Sort(set) return set @@ -414,6 +412,49 @@ func Compare(a, b Labels) int { return len(a) - len(b) } +// Copy labels from b on top of whatever was in ls previously, reusing memory or expanding if needed. +func (ls *Labels) CopyFrom(b Labels) { + (*ls) = append((*ls)[:0], b...) +} + +// IsEmpty returns true if ls represents an empty set of labels. +func (ls Labels) IsEmpty() bool { + return len(ls) == 0 +} + +// Range calls f on each label. +func (ls Labels) Range(f func(l Label)) { + for _, l := range ls { + f(l) + } +} + +// Validate calls f on each label. If f returns a non-nil error, then it returns that error cancelling the iteration. +func (ls Labels) Validate(f func(l Label) error) error { + for _, l := range ls { + if err := f(l); err != nil { + return err + } + } + return nil +} + +// InternStrings calls intern on every string value inside ls, replacing them with what it returns. +func (ls *Labels) InternStrings(intern func(string) string) { + for i, l := range *ls { + (*ls)[i].Name = intern(l.Name) + (*ls)[i].Value = intern(l.Value) + } +} + +// ReleaseStrings calls release on every string value inside ls. +func (ls Labels) ReleaseStrings(release func(string)) { + for _, l := range ls { + release(l.Name) + release(l.Value) + } +} + // Builder allows modifying Labels. type Builder struct { base Labels @@ -470,7 +511,7 @@ Outer: return b } -// Set the name/value pair as a label. +// Set the name/value pair as a label. A value of "" means delete that label. func (b *Builder) Set(n, v string) *Builder { if v == "" { // Empty labels are the same as missing labels. @@ -525,3 +566,40 @@ Outer: } return res } + +// ScratchBuilder allows efficient construction of a Labels from scratch. +type ScratchBuilder struct { + add Labels +} + +// NewScratchBuilder creates a ScratchBuilder initialized for Labels with n entries. +func NewScratchBuilder(n int) ScratchBuilder { + return ScratchBuilder{add: make([]Label, 0, n)} +} + +func (b *ScratchBuilder) Reset() { + b.add = b.add[:0] +} + +// Add a name/value pair. +// Note if you Add the same name twice you will get a duplicate label, which is invalid. +func (b *ScratchBuilder) Add(name, value string) { + b.add = append(b.add, Label{Name: name, Value: value}) +} + +// Sort the labels added so far by name. +func (b *ScratchBuilder) Sort() { + sort.Sort(b.add) +} + +// Asssign is for when you already have a Labels which you want this ScratchBuilder to return. +func (b *ScratchBuilder) Assign(ls Labels) { + b.add = append(b.add[:0], ls...) // Copy on top of our slice, so we don't retain the input slice. +} + +// Return the name/value pairs added so far as a Labels object. +// Note: if you want them sorted, call Sort() first. +func (b *ScratchBuilder) Labels() Labels { + // Copy the slice, so the next use of ScratchBuilder doesn't overwrite. + return append([]Label{}, b.add...) +} diff --git a/model/labels/labels_test.go b/model/labels/labels_test.go index 0fd0edacc3..69beb2fc3e 100644 --- a/model/labels/labels_test.go +++ b/model/labels/labels_test.go @@ -36,10 +36,6 @@ func TestLabels_String(t *testing.T) { lables: Labels{}, expected: "{}", }, - { - lables: nil, - expected: "{}", - }, } for _, c := range cases { str := c.lables.String() @@ -316,18 +312,18 @@ func TestLabels_Equal(t *testing.T) { func TestLabels_FromStrings(t *testing.T) { labels := FromStrings("aaa", "111", "bbb", "222") - expected := Labels{ - { - Name: "aaa", - Value: "111", - }, - { - Name: "bbb", - Value: "222", - }, - } - - require.Equal(t, expected, labels, "unexpected labelset") + x := 0 + labels.Range(func(l Label) { + switch x { + case 0: + require.Equal(t, Label{Name: "aaa", Value: "111"}, l, "unexpected value") + case 1: + require.Equal(t, Label{Name: "bbb", Value: "222"}, l, "unexpected value") + default: + t.Fatalf("unexpected labelset value %d: %v", x, l) + } + x++ + }) require.Panics(t, func() { FromStrings("aaa", "111", "bbb") }) //nolint:staticcheck // Ignore SA5012, error is intentional test. } @@ -539,7 +535,6 @@ func TestBuilder(t *testing.T) { want: FromStrings("aaa", "111", "ccc", "333"), }, { - base: nil, set: []Label{{"aaa", "111"}, {"bbb", "222"}, {"ccc", "333"}}, del: []string{"bbb"}, want: FromStrings("aaa", "111", "ccc", "333"), @@ -601,11 +596,49 @@ func TestBuilder(t *testing.T) { } } +func TestScratchBuilder(t *testing.T) { + for i, tcase := range []struct { + add []Label + want Labels + }{ + { + add: []Label{}, + want: EmptyLabels(), + }, + { + add: []Label{{"aaa", "111"}}, + want: FromStrings("aaa", "111"), + }, + { + add: []Label{{"aaa", "111"}, {"bbb", "222"}, {"ccc", "333"}}, + want: FromStrings("aaa", "111", "bbb", "222", "ccc", "333"), + }, + { + add: []Label{{"bbb", "222"}, {"aaa", "111"}, {"ccc", "333"}}, + want: FromStrings("aaa", "111", "bbb", "222", "ccc", "333"), + }, + { + add: []Label{{"ddd", "444"}}, + want: FromStrings("ddd", "444"), + }, + } { + t.Run(fmt.Sprint(i), func(t *testing.T) { + b := ScratchBuilder{} + for _, lbl := range tcase.add { + b.Add(lbl.Name, lbl.Value) + } + b.Sort() + require.Equal(t, tcase.want, b.Labels()) + b.Assign(tcase.want) + require.Equal(t, tcase.want, b.Labels()) + }) + } +} + func TestLabels_Hash(t *testing.T) { lbls := FromStrings("foo", "bar", "baz", "qux") require.Equal(t, lbls.Hash(), lbls.Hash()) - require.NotEqual(t, lbls.Hash(), Labels{lbls[1], lbls[0]}.Hash(), "unordered labels match.") - require.NotEqual(t, lbls.Hash(), Labels{lbls[0]}.Hash(), "different labels match.") + require.NotEqual(t, lbls.Hash(), FromStrings("foo", "bar").Hash(), "different labels match.") } var benchmarkLabelsResult uint64 @@ -623,7 +656,7 @@ func BenchmarkLabels_Hash(b *testing.B) { // Label ~20B name, 50B value. b.Set(fmt.Sprintf("abcdefghijabcdefghijabcdefghij%d", i), fmt.Sprintf("abcdefghijabcdefghijabcdefghijabcdefghijabcdefghij%d", i)) } - return b.Labels(nil) + return b.Labels(EmptyLabels()) }(), }, { @@ -634,7 +667,7 @@ func BenchmarkLabels_Hash(b *testing.B) { // Label ~50B name, 50B value. b.Set(fmt.Sprintf("abcdefghijabcdefghijabcdefghijabcdefghijabcdefghij%d", i), fmt.Sprintf("abcdefghijabcdefghijabcdefghijabcdefghijabcdefghij%d", i)) } - return b.Labels(nil) + return b.Labels(EmptyLabels()) }(), }, { diff --git a/model/labels/test_utils.go b/model/labels/test_utils.go index a683588d16..05b8168825 100644 --- a/model/labels/test_utils.go +++ b/model/labels/test_utils.go @@ -17,7 +17,6 @@ import ( "bufio" "fmt" "os" - "sort" "strings" ) @@ -51,13 +50,14 @@ func ReadLabels(fn string, n int) ([]Labels, error) { defer f.Close() scanner := bufio.NewScanner(f) + b := ScratchBuilder{} var mets []Labels hashes := map[uint64]struct{}{} i := 0 for scanner.Scan() && i < n { - m := make(Labels, 0, 10) + b.Reset() r := strings.NewReplacer("\"", "", "{", "", "}", "") s := r.Replace(scanner.Text()) @@ -65,10 +65,11 @@ func ReadLabels(fn string, n int) ([]Labels, error) { labelChunks := strings.Split(s, ",") for _, labelChunk := range labelChunks { split := strings.Split(labelChunk, ":") - m = append(m, Label{Name: split[0], Value: split[1]}) + b.Add(split[0], split[1]) } // Order of the k/v labels matters, don't assume we'll always receive them already sorted. - sort.Sort(m) + b.Sort() + m := b.Labels() h := m.Hash() if _, ok := hashes[h]; ok { diff --git a/model/relabel/relabel.go b/model/relabel/relabel.go index c731f6e0d3..0cc6eeeb7e 100644 --- a/model/relabel/relabel.go +++ b/model/relabel/relabel.go @@ -203,20 +203,20 @@ func (re Regexp) String() string { // Process returns a relabeled copy of the given label set. The relabel configurations // are applied in order of input. -// If a label set is dropped, nil is returned. +// If a label set is dropped, EmptyLabels and false is returned. // May return the input labelSet modified. -func Process(lbls labels.Labels, cfgs ...*Config) labels.Labels { - lb := labels.NewBuilder(nil) +func Process(lbls labels.Labels, cfgs ...*Config) (ret labels.Labels, keep bool) { + lb := labels.NewBuilder(labels.EmptyLabels()) for _, cfg := range cfgs { - lbls = relabel(lbls, cfg, lb) - if lbls == nil { - return nil + lbls, keep = relabel(lbls, cfg, lb) + if !keep { + return labels.EmptyLabels(), false } } - return lbls + return lbls, true } -func relabel(lset labels.Labels, cfg *Config, lb *labels.Builder) labels.Labels { +func relabel(lset labels.Labels, cfg *Config, lb *labels.Builder) (ret labels.Labels, keep bool) { var va [16]string values := va[:0] if len(cfg.SourceLabels) > cap(values) { @@ -232,19 +232,19 @@ func relabel(lset labels.Labels, cfg *Config, lb *labels.Builder) labels.Labels switch cfg.Action { case Drop: if cfg.Regex.MatchString(val) { - return nil + return labels.EmptyLabels(), false } case Keep: if !cfg.Regex.MatchString(val) { - return nil + return labels.EmptyLabels(), false } case DropEqual: if lset.Get(cfg.TargetLabel) == val { - return nil + return labels.EmptyLabels(), false } case KeepEqual: if lset.Get(cfg.TargetLabel) != val { - return nil + return labels.EmptyLabels(), false } case Replace: indexes := cfg.Regex.FindStringSubmatchIndex(val) @@ -271,29 +271,29 @@ func relabel(lset labels.Labels, cfg *Config, lb *labels.Builder) labels.Labels mod := sum64(md5.Sum([]byte(val))) % cfg.Modulus lb.Set(cfg.TargetLabel, fmt.Sprintf("%d", mod)) case LabelMap: - for _, l := range lset { + lset.Range(func(l labels.Label) { if cfg.Regex.MatchString(l.Name) { res := cfg.Regex.ReplaceAllString(l.Name, cfg.Replacement) lb.Set(res, l.Value) } - } + }) case LabelDrop: - for _, l := range lset { + lset.Range(func(l labels.Label) { if cfg.Regex.MatchString(l.Name) { lb.Del(l.Name) } - } + }) case LabelKeep: - for _, l := range lset { + lset.Range(func(l labels.Label) { if !cfg.Regex.MatchString(l.Name) { lb.Del(l.Name) } - } + }) default: panic(fmt.Errorf("relabel: unknown relabel action type %q", cfg.Action)) } - return lb.Labels(lset) + return lb.Labels(lset), true } // sum64 sums the md5 hash to an uint64. diff --git a/model/relabel/relabel_test.go b/model/relabel/relabel_test.go index 0b0dfd511f..d277d778d1 100644 --- a/model/relabel/relabel_test.go +++ b/model/relabel/relabel_test.go @@ -28,6 +28,7 @@ func TestRelabel(t *testing.T) { input labels.Labels relabel []*Config output labels.Labels + drop bool }{ { input: labels.FromMap(map[string]string{ @@ -101,7 +102,7 @@ func TestRelabel(t *testing.T) { Action: Replace, }, }, - output: nil, + drop: true, }, { input: labels.FromMap(map[string]string{ @@ -115,7 +116,7 @@ func TestRelabel(t *testing.T) { Action: Drop, }, }, - output: nil, + drop: true, }, { input: labels.FromMap(map[string]string{ @@ -177,7 +178,7 @@ func TestRelabel(t *testing.T) { Action: Keep, }, }, - output: nil, + drop: true, }, { input: labels.FromMap(map[string]string{ @@ -483,7 +484,7 @@ func TestRelabel(t *testing.T) { TargetLabel: "__port1", }, }, - output: nil, + drop: true, }, { input: labels.FromMap(map[string]string{ @@ -517,7 +518,7 @@ func TestRelabel(t *testing.T) { TargetLabel: "__port2", }, }, - output: nil, + drop: true, }, } @@ -538,8 +539,11 @@ func TestRelabel(t *testing.T) { } } - res := Process(test.input, test.relabel...) - require.Equal(t, test.output, res) + res, keep := Process(test.input, test.relabel...) + require.Equal(t, !test.drop, keep) + if keep { + require.Equal(t, test.output, res) + } } } @@ -721,7 +725,7 @@ func BenchmarkRelabel(b *testing.B) { for _, tt := range tests { b.Run(tt.name, func(b *testing.B) { for i := 0; i < b.N; i++ { - _ = Process(tt.lbls, tt.cfgs...) + _, _ = Process(tt.lbls, tt.cfgs...) } }) } diff --git a/model/textparse/openmetricsparse.go b/model/textparse/openmetricsparse.go index 932a3d96db..3fc80b5d62 100644 --- a/model/textparse/openmetricsparse.go +++ b/model/textparse/openmetricsparse.go @@ -22,7 +22,6 @@ import ( "fmt" "io" "math" - "sort" "strings" "unicode/utf8" @@ -82,6 +81,7 @@ func (l *openMetricsLexer) Error(es string) { // This is based on the working draft https://docs.google.com/document/u/1/d/1KwV0mAXwwbvvifBvDKH_LU1YjyXE_wxCkHNoCGq1GX0/edit type OpenMetricsParser struct { l *openMetricsLexer + builder labels.ScratchBuilder series []byte text []byte mtype MetricType @@ -158,14 +158,11 @@ func (p *OpenMetricsParser) Comment() []byte { // Metric writes the labels of the current sample into the passed labels. // It returns the string from which the metric was parsed. func (p *OpenMetricsParser) Metric(l *labels.Labels) string { - // Allocate the full immutable string immediately, so we just - // have to create references on it below. + // Copy the buffer to a string: this is only necessary for the return value. s := string(p.series) - *l = append(*l, labels.Label{ - Name: labels.MetricName, - Value: s[:p.offsets[0]-p.start], - }) + p.builder.Reset() + p.builder.Add(labels.MetricName, s[:p.offsets[0]-p.start]) for i := 1; i < len(p.offsets); i += 4 { a := p.offsets[i] - p.start @@ -173,16 +170,16 @@ func (p *OpenMetricsParser) Metric(l *labels.Labels) string { c := p.offsets[i+2] - p.start d := p.offsets[i+3] - p.start + value := s[c:d] // Replacer causes allocations. Replace only when necessary. if strings.IndexByte(s[c:d], byte('\\')) >= 0 { - *l = append(*l, labels.Label{Name: s[a:b], Value: lvalReplacer.Replace(s[c:d])}) - continue + value = lvalReplacer.Replace(value) } - *l = append(*l, labels.Label{Name: s[a:b], Value: s[c:d]}) + p.builder.Add(s[a:b], value) } - // Sort labels. - sort.Sort(*l) + p.builder.Sort() + *l = p.builder.Labels() return s } @@ -204,17 +201,18 @@ func (p *OpenMetricsParser) Exemplar(e *exemplar.Exemplar) bool { e.Ts = p.exemplarTs } + p.builder.Reset() for i := 0; i < len(p.eOffsets); i += 4 { a := p.eOffsets[i] - p.start b := p.eOffsets[i+1] - p.start c := p.eOffsets[i+2] - p.start d := p.eOffsets[i+3] - p.start - e.Labels = append(e.Labels, labels.Label{Name: s[a:b], Value: s[c:d]}) + p.builder.Add(s[a:b], s[c:d]) } - // Sort the labels. - sort.Sort(e.Labels) + p.builder.Sort() + e.Labels = p.builder.Labels() return true } diff --git a/model/textparse/openmetricsparse_test.go b/model/textparse/openmetricsparse_test.go index 9453db5d5f..68b7fea8a0 100644 --- a/model/textparse/openmetricsparse_test.go +++ b/model/textparse/openmetricsparse_test.go @@ -246,7 +246,6 @@ foo_total 17.0 1520879607.789 # {xx="yy"} 5` require.Equal(t, true, found) require.Equal(t, *exp[i].e, e) } - res = res[:0] case EntryType: m, typ := p.Type() diff --git a/model/textparse/promparse.go b/model/textparse/promparse.go index a3bb8bb9bf..d503ff9a78 100644 --- a/model/textparse/promparse.go +++ b/model/textparse/promparse.go @@ -21,7 +21,6 @@ import ( "fmt" "io" "math" - "sort" "strconv" "strings" "unicode/utf8" @@ -144,6 +143,7 @@ func (l *promlexer) Error(es string) { // Prometheus text exposition format. type PromParser struct { l *promlexer + builder labels.ScratchBuilder series []byte text []byte mtype MetricType @@ -212,14 +212,11 @@ func (p *PromParser) Comment() []byte { // Metric writes the labels of the current sample into the passed labels. // It returns the string from which the metric was parsed. func (p *PromParser) Metric(l *labels.Labels) string { - // Allocate the full immutable string immediately, so we just - // have to create references on it below. + // Copy the buffer to a string: this is only necessary for the return value. s := string(p.series) - *l = append(*l, labels.Label{ - Name: labels.MetricName, - Value: s[:p.offsets[0]-p.start], - }) + p.builder.Reset() + p.builder.Add(labels.MetricName, s[:p.offsets[0]-p.start]) for i := 1; i < len(p.offsets); i += 4 { a := p.offsets[i] - p.start @@ -227,16 +224,16 @@ func (p *PromParser) Metric(l *labels.Labels) string { c := p.offsets[i+2] - p.start d := p.offsets[i+3] - p.start + value := s[c:d] // Replacer causes allocations. Replace only when necessary. if strings.IndexByte(s[c:d], byte('\\')) >= 0 { - *l = append(*l, labels.Label{Name: s[a:b], Value: lvalReplacer.Replace(s[c:d])}) - continue + value = lvalReplacer.Replace(value) } - *l = append(*l, labels.Label{Name: s[a:b], Value: s[c:d]}) + p.builder.Add(s[a:b], value) } - // Sort labels to maintain the sorted labels invariant. - sort.Sort(*l) + p.builder.Sort() + *l = p.builder.Labels() return s } diff --git a/model/textparse/promparse_test.go b/model/textparse/promparse_test.go index 6a1216da16..dadad34495 100644 --- a/model/textparse/promparse_test.go +++ b/model/textparse/promparse_test.go @@ -192,7 +192,6 @@ testmetric{label="\"bar\""} 1` require.Equal(t, exp[i].t, ts) require.Equal(t, exp[i].v, v) require.Equal(t, exp[i].lset, res) - res = res[:0] case EntryType: m, typ := p.Type() @@ -414,7 +413,7 @@ func BenchmarkParse(b *testing.B) { case EntrySeries: m, _, _ := p.Series() - res := make(labels.Labels, 0, 5) + var res labels.Labels p.Metric(&res) total += len(m) @@ -426,7 +425,7 @@ func BenchmarkParse(b *testing.B) { }) b.Run(parserName+"/decode-metric-reuse/"+fn, func(b *testing.B) { total := 0 - res := make(labels.Labels, 0, 5) + var res labels.Labels b.SetBytes(int64(len(buf) / promtestdataSampleCount)) b.ReportAllocs() @@ -451,7 +450,6 @@ func BenchmarkParse(b *testing.B) { total += len(m) i++ - res = res[:0] } } } diff --git a/model/textparse/protobufparse.go b/model/textparse/protobufparse.go index a9c940879e..37c6f0ebb0 100644 --- a/model/textparse/protobufparse.go +++ b/model/textparse/protobufparse.go @@ -19,7 +19,6 @@ import ( "fmt" "io" "math" - "sort" "strings" "unicode/utf8" @@ -59,6 +58,8 @@ type ProtobufParser struct { // that we have to decode the next MetricFamily. state Entry + builder labels.ScratchBuilder // held here to reduce allocations when building Labels + mf *dto.MetricFamily // The following are just shenanigans to satisfy the Parser interface. @@ -245,23 +246,19 @@ func (p *ProtobufParser) Comment() []byte { // Metric writes the labels of the current sample into the passed labels. // It returns the string from which the metric was parsed. func (p *ProtobufParser) Metric(l *labels.Labels) string { - *l = append(*l, labels.Label{ - Name: labels.MetricName, - Value: p.getMagicName(), - }) + p.builder.Reset() + p.builder.Add(labels.MetricName, p.getMagicName()) for _, lp := range p.mf.GetMetric()[p.metricPos].GetLabel() { - *l = append(*l, labels.Label{ - Name: lp.GetName(), - Value: lp.GetValue(), - }) + p.builder.Add(lp.GetName(), lp.GetValue()) } if needed, name, value := p.getMagicLabel(); needed { - *l = append(*l, labels.Label{Name: name, Value: value}) + p.builder.Add(name, value) } // Sort labels to maintain the sorted labels invariant. - sort.Sort(*l) + p.builder.Sort() + *l = p.builder.Labels() return p.metricBytes.String() } @@ -305,12 +302,12 @@ func (p *ProtobufParser) Exemplar(ex *exemplar.Exemplar) bool { ex.HasTs = true ex.Ts = ts.GetSeconds()*1000 + int64(ts.GetNanos()/1_000_000) } + p.builder.Reset() for _, lp := range exProto.GetLabel() { - ex.Labels = append(ex.Labels, labels.Label{ - Name: lp.GetName(), - Value: lp.GetValue(), - }) + p.builder.Add(lp.GetName(), lp.GetValue()) } + p.builder.Sort() + ex.Labels = p.builder.Labels() return true } diff --git a/model/textparse/protobufparse_test.go b/model/textparse/protobufparse_test.go index 33826ad8e1..b8b8681724 100644 --- a/model/textparse/protobufparse_test.go +++ b/model/textparse/protobufparse_test.go @@ -630,7 +630,6 @@ metric: < require.Equal(t, true, found) require.Equal(t, exp[i].e[0], e) } - res = res[:0] case EntryHistogram: m, ts, shs, fhs := p.Histogram() @@ -642,7 +641,6 @@ metric: < require.Equal(t, exp[i].t, int64(0)) } require.Equal(t, exp[i].lset, res) - res = res[:0] require.Equal(t, exp[i].m, string(m)) if shs != nil { require.Equal(t, exp[i].shs, shs) diff --git a/notifier/notifier.go b/notifier/notifier.go index fd89a029c7..79697d0796 100644 --- a/notifier/notifier.go +++ b/notifier/notifier.go @@ -353,11 +353,11 @@ func (n *Manager) Send(alerts ...*Alert) { for _, a := range alerts { lb := labels.NewBuilder(a.Labels) - for _, l := range n.opts.ExternalLabels { + n.opts.ExternalLabels.Range(func(l labels.Label) { if a.Labels.Get(l.Name) == "" { lb.Set(l.Name, l.Value) } - } + }) a.Labels = lb.Labels(a.Labels) } @@ -394,8 +394,8 @@ func (n *Manager) relabelAlerts(alerts []*Alert) []*Alert { var relabeledAlerts []*Alert for _, alert := range alerts { - labels := relabel.Process(alert.Labels, n.opts.RelabelConfigs...) - if labels != nil { + labels, keep := relabel.Process(alert.Labels, n.opts.RelabelConfigs...) + if keep { alert.Labels = labels relabeledAlerts = append(relabeledAlerts, alert) } @@ -570,9 +570,9 @@ func alertsToOpenAPIAlerts(alerts []*Alert) models.PostableAlerts { func labelsToOpenAPILabelSet(modelLabelSet labels.Labels) models.LabelSet { apiLabelSet := models.LabelSet{} - for _, label := range modelLabelSet { + modelLabelSet.Range(func(label labels.Label) { apiLabelSet[label.Name] = label.Value - } + }) return apiLabelSet } @@ -719,9 +719,9 @@ func AlertmanagerFromGroup(tg *targetgroup.Group, cfg *config.AlertmanagerConfig } } - lset := relabel.Process(labels.New(lbls...), cfg.RelabelConfigs...) - if lset == nil { - droppedAlertManagers = append(droppedAlertManagers, alertmanagerLabels{lbls}) + lset, keep := relabel.Process(labels.New(lbls...), cfg.RelabelConfigs...) + if !keep { + droppedAlertManagers = append(droppedAlertManagers, alertmanagerLabels{labels.New(lbls...)}) continue } diff --git a/promql/engine.go b/promql/engine.go index 0225f78d2a..0455779a4b 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1567,7 +1567,7 @@ func (ev *evaluator) eval(expr parser.Expr) (parser.Value, storage.Warnings) { case *parser.NumberLiteral: return ev.rangeEval(nil, func(v []parser.Value, _ [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, storage.Warnings) { - return append(enh.Out, Sample{Point: Point{V: e.Val}}), nil + return append(enh.Out, Sample{Point: Point{V: e.Val}, Metric: labels.EmptyLabels()}), nil }) case *parser.StringLiteral: @@ -2190,7 +2190,7 @@ func resultMetric(lhs, rhs labels.Labels, op parser.ItemType, matching *parser.V } } - ret := enh.lb.Labels(nil) + ret := enh.lb.Labels(labels.EmptyLabels()) enh.resultMetric[str] = ret return ret } @@ -2230,7 +2230,7 @@ func (ev *evaluator) VectorscalarBinop(op parser.ItemType, lhs Vector, rhs Scala } func dropMetricName(l labels.Labels) labels.Labels { - return labels.NewBuilder(l).Del(labels.MetricName).Labels(nil) + return labels.NewBuilder(l).Del(labels.MetricName).Labels(labels.EmptyLabels()) } // scalarBinop evaluates a binary operation between two Scalars. @@ -2357,7 +2357,7 @@ func (ev *evaluator) aggregation(op parser.ItemType, grouping []string, without } } - lb := labels.NewBuilder(nil) + lb := labels.NewBuilder(labels.EmptyLabels()) var buf []byte for si, s := range vec { metric := s.Metric @@ -2365,7 +2365,7 @@ func (ev *evaluator) aggregation(op parser.ItemType, grouping []string, without if op == parser.COUNT_VALUES { lb.Reset(metric) lb.Set(valueLabel, strconv.FormatFloat(s.V, 'f', -1, 64)) - metric = lb.Labels(nil) + metric = lb.Labels(labels.EmptyLabels()) // We've changed the metric so we have to recompute the grouping key. recomputeGroupingKey = true @@ -2389,7 +2389,7 @@ func (ev *evaluator) aggregation(op parser.ItemType, grouping []string, without } else { lb.Keep(grouping...) } - m := lb.Labels(nil) + m := lb.Labels(labels.EmptyLabels()) newAgg := &groupedAggregation{ labels: m, value: s.V, diff --git a/promql/engine_test.go b/promql/engine_test.go index ea19641ba7..5094df2a29 100644 --- a/promql/engine_test.go +++ b/promql/engine_test.go @@ -684,6 +684,7 @@ load 10s Result: Matrix{ Series{ Points: []Point{{V: 1, T: 0}, {V: 1, T: 1000}, {V: 1, T: 2000}}, + Metric: labels.EmptyLabels(), }, }, Start: time.Unix(0, 0), @@ -4008,7 +4009,7 @@ func TestSparseHistogram_Sum_Count_AddOperator(t *testing.T) { // sum(). queryString := fmt.Sprintf("sum(%s)", seriesName) queryAndCheck(queryString, []Sample{ - {Point{T: ts, H: &c.expected}, labels.Labels{}}, + {Point{T: ts, H: &c.expected}, labels.EmptyLabels()}, }) // + operator. @@ -4017,13 +4018,13 @@ func TestSparseHistogram_Sum_Count_AddOperator(t *testing.T) { queryString += fmt.Sprintf(` + ignoring(idx) %s{idx="%d"}`, seriesName, idx) } queryAndCheck(queryString, []Sample{ - {Point{T: ts, H: &c.expected}, labels.Labels{}}, + {Point{T: ts, H: &c.expected}, labels.EmptyLabels()}, }) // count(). queryString = fmt.Sprintf("count(%s)", seriesName) queryAndCheck(queryString, []Sample{ - {Point{T: ts, V: 3}, labels.Labels{}}, + {Point{T: ts, V: 3}, labels.EmptyLabels()}, }) }) } diff --git a/promql/functions.go b/promql/functions.go index d481cb7358..c5922002b0 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -957,7 +957,7 @@ func funcHistogramQuantile(vals []parser.Value, args parser.Expressions, enh *Ev if !ok { sample.Metric = labels.NewBuilder(sample.Metric). Del(excludedLabels...). - Labels(nil) + Labels(labels.EmptyLabels()) mb = &metricWithBuckets{sample.Metric, nil} enh.signatureToMetricWithBuckets[string(enh.lblBuf)] = mb @@ -1077,7 +1077,7 @@ func funcLabelReplace(vals []parser.Value, args parser.Expressions, enh *EvalNod if len(res) > 0 { lb.Set(dst, string(res)) } - outMetric = lb.Labels(nil) + outMetric = lb.Labels(labels.EmptyLabels()) enh.Dmn[h] = outMetric } } @@ -1145,7 +1145,7 @@ func funcLabelJoin(vals []parser.Value, args parser.Expressions, enh *EvalNodeHe lb.Set(dst, strval) } - outMetric = lb.Labels(nil) + outMetric = lb.Labels(labels.EmptyLabels()) enh.Dmn[h] = outMetric } @@ -1383,7 +1383,7 @@ func (s *vectorByReverseValueHeap) Pop() interface{} { // createLabelsForAbsentFunction returns the labels that are uniquely and exactly matched // in a given expression. It is used in the absent functions. func createLabelsForAbsentFunction(expr parser.Expr) labels.Labels { - m := labels.Labels{} + b := labels.NewBuilder(labels.EmptyLabels()) var lm []*labels.Matcher switch n := expr.(type) { @@ -1392,25 +1392,26 @@ func createLabelsForAbsentFunction(expr parser.Expr) labels.Labels { case *parser.MatrixSelector: lm = n.VectorSelector.(*parser.VectorSelector).LabelMatchers default: - return m + return labels.EmptyLabels() } - empty := []string{} + // The 'has' map implements backwards-compatibility for historic behaviour: + // e.g. in `absent(x{job="a",job="b",foo="bar"})` then `job` is removed from the output. + // Note this gives arguably wrong behaviour for `absent(x{job="a",job="a",foo="bar"})`. + has := make(map[string]bool, len(lm)) for _, ma := range lm { if ma.Name == labels.MetricName { continue } - if ma.Type == labels.MatchEqual && !m.Has(ma.Name) { - m = labels.NewBuilder(m).Set(ma.Name, ma.Value).Labels(nil) + if ma.Type == labels.MatchEqual && !has[ma.Name] { + b.Set(ma.Name, ma.Value) + has[ma.Name] = true } else { - empty = append(empty, ma.Name) + b.Del(ma.Name) } } - for _, v := range empty { - m = labels.NewBuilder(m).Del(v).Labels(nil) - } - return m + return b.Labels(labels.EmptyLabels()) } func stringFromArg(e parser.Expr) string { diff --git a/promql/parser/generated_parser.y b/promql/parser/generated_parser.y index 433f45259c..461e854ac1 100644 --- a/promql/parser/generated_parser.y +++ b/promql/parser/generated_parser.y @@ -16,13 +16,13 @@ package parser import ( "math" - "sort" "strconv" "time" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/value" ) + %} %union { @@ -32,6 +32,7 @@ import ( matcher *labels.Matcher label labels.Label labels labels.Labels + lblList []labels.Label strings []string series []SequenceValue uint uint64 @@ -138,10 +139,9 @@ START_METRIC_SELECTOR // Type definitions for grammar rules. %type label_match_list %type label_matcher - %type aggregate_op grouping_label match_op maybe_label metric_identifier unary_op at_modifier_preprocessors - -%type label_set label_set_list metric +%type label_set metric +%type label_set_list %type