diff --git a/prometheus/desc.go b/prometheus/desc.go index ad347113c..5e50cb2c0 100644 --- a/prometheus/desc.go +++ b/prometheus/desc.go @@ -22,8 +22,6 @@ import ( dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/model" "google.golang.org/protobuf/proto" - - "github.com/prometheus/client_golang/prometheus/internal" ) // Desc is the descriptor used by every Prometheus Metric. It is essentially @@ -47,12 +45,15 @@ type Desc struct { fqName string // help provides some helpful information about this metric. help string - // constLabelPairs contains precalculated DTO label pairs based on - // the constant labels. - constLabelPairs []*dto.LabelPair // variableLabels contains names of labels and normalization function for // which the metric maintains variable values. variableLabels *compiledLabels + // constLabelPairs contains the sorted DTO label pairs based on the constant labels + // and variable labels + constLabelPairs []*dto.LabelPair + // orderedLabels contains the sorted labels with necessary fields to construct the + // final label pairs when needed. + orderedLabels []labelMapping // id is a hash of the values of the ConstLabels and fqName. This // must be unique among all registered descriptors and can therefore be // used as an identifier of the descriptor. @@ -66,6 +67,21 @@ type Desc struct { err error } +type labelMapping struct { + constLabelIndex int + + variableLabelIndex int + variableLabelName *string +} + +func newLabelMapping() labelMapping { + return labelMapping{ + constLabelIndex: -1, + variableLabelIndex: -1, + variableLabelName: nil, + } +} + // NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc // and will be reported on registration time. variableLabels and constLabels can // be nil if no such labels should be set. fqName must not be empty. @@ -133,7 +149,7 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const d.err = fmt.Errorf("%q is not a valid label name for metric %q", label, fqName) return d } - labelNames = append(labelNames, "$"+label) + labelNames = append(labelNames, label) labelNameSet[label] = struct{}{} } if len(labelNames) != len(labelNameSet) { @@ -161,13 +177,26 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const d.dimHash = xxh.Sum64() d.constLabelPairs = make([]*dto.LabelPair, 0, len(constLabels)) - for n, v := range constLabels { - d.constLabelPairs = append(d.constLabelPairs, &dto.LabelPair{ - Name: proto.String(n), - Value: proto.String(v), - }) + d.orderedLabels = make([]labelMapping, len(labelNames)) + for i, n := range labelNames { + lm := newLabelMapping() + if l, ok := constLabels[n]; ok { + d.constLabelPairs = append(d.constLabelPairs, &dto.LabelPair{ + Name: proto.String(n), + Value: proto.String(l), + }) + lm.constLabelIndex = len(d.constLabelPairs) - 1 + } else { + for variableLabelIndex, variableLabel := range variableLabels.labelNames() { + if variableLabel == n { + lm.variableLabelIndex = variableLabelIndex + lm.variableLabelName = proto.String(variableLabel) + } + } + } + d.orderedLabels[i] = lm } - sort.Sort(internal.LabelPairSorter(d.constLabelPairs)) + return d } diff --git a/prometheus/desc_test.go b/prometheus/desc_test.go index 5a8429009..3e0d8cab7 100644 --- a/prometheus/desc_test.go +++ b/prometheus/desc_test.go @@ -14,6 +14,7 @@ package prometheus import ( + "fmt" "testing" ) @@ -61,3 +62,31 @@ func TestNewInvalidDesc_String(t *testing.T) { t.Errorf("String: unexpected output: %s", desc.String()) } } + +func BenchmarkNewDesc(b *testing.B) { + for _, bm := range []struct { + labelCount int + descFunc func() *Desc + }{ + { + labelCount: 1, + descFunc: new1LabelDescFunc, + }, + { + labelCount: 3, + descFunc: new3LabelsDescFunc, + }, + { + labelCount: 10, + descFunc: new10LabelsDescFunc, + }, + } { + b.Run(fmt.Sprintf("labels=%v", bm.labelCount), func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bm.descFunc() + } + }) + } +} diff --git a/prometheus/registry.go b/prometheus/registry.go index c6fd2f58b..3bc546605 100644 --- a/prometheus/registry.go +++ b/prometheus/registry.go @@ -962,28 +962,31 @@ func checkDescConsistency( } // Is the desc consistent with the content of the metric? - lpsFromDesc := make([]*dto.LabelPair, len(desc.constLabelPairs), len(dtoMetric.Label)) - copy(lpsFromDesc, desc.constLabelPairs) - for _, l := range desc.variableLabels.names { - lpsFromDesc = append(lpsFromDesc, &dto.LabelPair{ - Name: proto.String(l), - }) - } - if len(lpsFromDesc) != len(dtoMetric.Label) { + if len(desc.orderedLabels) != len(dtoMetric.Label) { return fmt.Errorf( "labels in collected metric %s %s are inconsistent with descriptor %s", metricFamily.GetName(), dtoMetric, desc, ) } - sort.Sort(internal.LabelPairSorter(lpsFromDesc)) - for i, lpFromDesc := range lpsFromDesc { + for i, lm := range desc.orderedLabels { lpFromMetric := dtoMetric.Label[i] - if lpFromDesc.GetName() != lpFromMetric.GetName() || - lpFromDesc.Value != nil && lpFromDesc.GetValue() != lpFromMetric.GetValue() { - return fmt.Errorf( - "labels in collected metric %s %s are inconsistent with descriptor %s", - metricFamily.GetName(), dtoMetric, desc, - ) + if lm.constLabelIndex > -1 { + lpFromDesc := desc.constLabelPairs[lm.constLabelIndex] + if lpFromDesc.GetName() != lpFromMetric.GetName() || + lpFromDesc.Value != nil && lpFromDesc.GetValue() != lpFromMetric.GetValue() { + return fmt.Errorf( + "labels in collected metric %s %s are inconsistent with descriptor %s", + metricFamily.GetName(), dtoMetric, desc, + ) + } + } else { + variableLabelName := *lm.variableLabelName + if variableLabelName != lpFromMetric.GetName() { + return fmt.Errorf( + "labels in collected metric %s %s are inconsistent with descriptor %s", + metricFamily.GetName(), dtoMetric, desc, + ) + } } } return nil diff --git a/prometheus/value.go b/prometheus/value.go index cc23011fa..e240ef8b4 100644 --- a/prometheus/value.go +++ b/prometheus/value.go @@ -16,12 +16,9 @@ package prometheus import ( "errors" "fmt" - "sort" "time" "unicode/utf8" - "github.com/prometheus/client_golang/prometheus/internal" - dto "github.com/prometheus/client_model/go" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" @@ -215,8 +212,7 @@ func populateMetric( // This function is only needed for custom Metric implementations. See MetricVec // example. func MakeLabelPairs(desc *Desc, labelValues []string) []*dto.LabelPair { - totalLen := len(desc.variableLabels.names) + len(desc.constLabelPairs) - if totalLen == 0 { + if len(desc.orderedLabels) == 0 { // Super fast path. return nil } @@ -224,15 +220,17 @@ func MakeLabelPairs(desc *Desc, labelValues []string) []*dto.LabelPair { // Moderately fast path. return desc.constLabelPairs } - labelPairs := make([]*dto.LabelPair, 0, totalLen) - for i, l := range desc.variableLabels.names { - labelPairs = append(labelPairs, &dto.LabelPair{ - Name: proto.String(l), - Value: proto.String(labelValues[i]), - }) + labelPairs := make([]*dto.LabelPair, len(desc.orderedLabels)) + for i, lm := range desc.orderedLabels { + if lm.constLabelIndex != -1 { + labelPairs[i] = desc.constLabelPairs[lm.constLabelIndex] + } else { + labelPairs[i] = &dto.LabelPair{ + Name: lm.variableLabelName, + Value: proto.String(labelValues[lm.variableLabelIndex]), + } + } } - labelPairs = append(labelPairs, desc.constLabelPairs...) - sort.Sort(internal.LabelPairSorter(labelPairs)) return labelPairs } diff --git a/prometheus/value_test.go b/prometheus/value_test.go index 23da6b217..e6ad18ce6 100644 --- a/prometheus/value_test.go +++ b/prometheus/value_test.go @@ -14,10 +14,13 @@ package prometheus import ( + "fmt" + "reflect" "testing" "time" dto "github.com/prometheus/client_model/go" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -108,3 +111,174 @@ func TestNewConstMetricWithCreatedTimestamp(t *testing.T) { }) } } + +func TestMakeLabelPairs(t *testing.T) { + tests := []struct { + name string + desc *Desc + labelValues []string + want []*dto.LabelPair + }{ + { + name: "no labels", + desc: NewDesc("metric-1", "", nil, nil), + labelValues: nil, + want: nil, + }, + { + name: "only constant labels", + desc: NewDesc("metric-1", "", nil, map[string]string{ + "label-1": "1", + "label-2": "2", + "label-3": "3", + }), + labelValues: nil, + want: []*dto.LabelPair{ + {Name: proto.String("label-1"), Value: proto.String("1")}, + {Name: proto.String("label-2"), Value: proto.String("2")}, + {Name: proto.String("label-3"), Value: proto.String("3")}, + }, + }, + { + name: "only variable labels", + desc: NewDesc("metric-1", "", []string{"var-label-1", "var-label-2", "var-label-3"}, nil), + labelValues: []string{"1", "2", "3"}, + want: []*dto.LabelPair{ + {Name: proto.String("var-label-1"), Value: proto.String("1")}, + {Name: proto.String("var-label-2"), Value: proto.String("2")}, + {Name: proto.String("var-label-3"), Value: proto.String("3")}, + }, + }, + { + name: "variable and const labels", + desc: NewDesc("metric-1", "", []string{"var-label-1", "var-label-2", "var-label-3"}, map[string]string{ + "label-1": "1", + "label-2": "2", + "label-3": "3", + }), + labelValues: []string{"1", "2", "3"}, + want: []*dto.LabelPair{ + {Name: proto.String("label-1"), Value: proto.String("1")}, + {Name: proto.String("label-2"), Value: proto.String("2")}, + {Name: proto.String("label-3"), Value: proto.String("3")}, + {Name: proto.String("var-label-1"), Value: proto.String("1")}, + {Name: proto.String("var-label-2"), Value: proto.String("2")}, + {Name: proto.String("var-label-3"), Value: proto.String("3")}, + }, + }, + { + name: "unsorted variable and const labels are sorted", + desc: NewDesc("metric-1", "", []string{"var-label-3", "var-label-2", "var-label-1"}, map[string]string{ + "label-3": "3", + "label-2": "2", + "label-1": "1", + }), + labelValues: []string{"3", "2", "1"}, + want: []*dto.LabelPair{ + {Name: proto.String("label-1"), Value: proto.String("1")}, + {Name: proto.String("label-2"), Value: proto.String("2")}, + {Name: proto.String("label-3"), Value: proto.String("3")}, + {Name: proto.String("var-label-1"), Value: proto.String("1")}, + {Name: proto.String("var-label-2"), Value: proto.String("2")}, + {Name: proto.String("var-label-3"), Value: proto.String("3")}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := MakeLabelPairs(tt.desc, tt.labelValues); !reflect.DeepEqual(got, tt.want) { + t.Errorf("%v != %v", got, tt.want) + } + }) + } +} + +var new1LabelDescFunc = func() *Desc { + return NewDesc( + "metric", + "help", + []string{"var-label-1"}, + Labels{"const-label-1": "value"}) +} + +var new3LabelsDescFunc = func() *Desc { + return NewDesc( + "metric", + "help", + []string{"var-label-1", "var-label-3", "var-label-2"}, + Labels{"const-label-1": "value", "const-label-3": "value", "const-label-2": "value"}) +} + +var new10LabelsDescFunc = func() *Desc { + return NewDesc( + "metric", + "help", + []string{"var-label-5", "var-label-1", "var-label-3", "var-label-2", "var-label-10", "var-label-4", "var-label-7", "var-label-8", "var-label-9", "var-label-6"}, + Labels{"const-label-4": "value", "const-label-1": "value", "const-label-7": "value", "const-label-2": "value", "const-label-9": "value", "const-label-8": "value", "const-label-10": "value", "const-label-3": "value", "const-label-6": "value", "const-label-5": "value"}) +} + +func BenchmarkMakeLabelPairs(b *testing.B) { + for _, bm := range []struct { + desc *Desc + makeLabelPairValues []string + }{ + { + desc: new1LabelDescFunc(), + makeLabelPairValues: []string{"value"}, + }, + { + desc: new3LabelsDescFunc(), + makeLabelPairValues: []string{"value", "value", "value"}, + }, + { + desc: new10LabelsDescFunc(), + makeLabelPairValues: []string{"value", "value", "value", "value", "value", "value", "value", "value", "value", "value"}, + }, + } { + b.Run(fmt.Sprintf("labels=%v", len(bm.makeLabelPairValues)), func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + MakeLabelPairs(bm.desc, bm.makeLabelPairValues) + } + }) + } +} + +func BenchmarkConstMetricFlow(b *testing.B) { + for _, bm := range []struct { + descFunc func() *Desc + labelValues []string + }{ + { + descFunc: new1LabelDescFunc, + labelValues: []string{"value"}, + }, + { + descFunc: new3LabelsDescFunc, + labelValues: []string{"value", "value", "value"}, + }, + { + descFunc: new10LabelsDescFunc, + labelValues: []string{"value", "value", "value", "value", "value", "value", "value", "value", "value", "value"}, + }, + } { + b.Run(fmt.Sprintf("labels=%v", len(bm.labelValues)), func(b *testing.B) { + for _, metricsToCreate := range []int{1, 2, 3, 5} { + b.Run(fmt.Sprintf("metrics=%v", metricsToCreate), func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + desc := bm.descFunc() + for j := 0; j < metricsToCreate; j++ { + _, err := NewConstMetric(desc, GaugeValue, 1.0, bm.labelValues...) + if err != nil { + b.Fatal(err) + } + } + } + }) + } + }) + } +} diff --git a/prometheus/wrap.go b/prometheus/wrap.go index 25da157f1..43931cdaa 100644 --- a/prometheus/wrap.go +++ b/prometheus/wrap.go @@ -198,6 +198,7 @@ func wrapDesc(desc *Desc, prefix string, labels Labels) *Desc { help: desc.help, variableLabels: desc.variableLabels, constLabelPairs: desc.constLabelPairs, + orderedLabels: desc.orderedLabels, err: fmt.Errorf("attempted wrapping with already existing label name %q", ln), } }