From 5d3b089f0c32701cd05eee546796d377b803574a Mon Sep 17 00:00:00 2001 From: Frederic Branczyk Date: Tue, 3 Jul 2018 13:25:00 +0200 Subject: [PATCH 1/7] Add testutils package Signed-off-by: Frederic Branczyk --- testutils/testutils.go | 182 ++++++++++++++++++++++++++++++++++++ testutils/testutils_test.go | 100 ++++++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 testutils/testutils.go create mode 100644 testutils/testutils_test.go diff --git a/testutils/testutils.go b/testutils/testutils.go new file mode 100644 index 0000000..c6da37b --- /dev/null +++ b/testutils/testutils.go @@ -0,0 +1,182 @@ +/* +Copyright 2018 The Prometheus Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testutils + +import ( + "bytes" + "fmt" + "reflect" + "sort" + "strings" + + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" +) + +// GatherAndCompare retrieves all metrics exposed by a collector and compares it +// to an expected output in the Prometheus text exposition format. +// metricNames allows only comparing the given metrics. All are compared if it's nil. +func GatherAndCompare(c prometheus.Collector, expected string, metricNames ...string) error { + expected = removeUnusedWhitespace(expected) + + reg := prometheus.NewPedanticRegistry() + if err := reg.Register(c); err != nil { + return fmt.Errorf("registering collector failed: %s", err) + } + metrics, err := reg.Gather() + if err != nil { + return fmt.Errorf("gathering metrics failed: %s", err) + } + if metricNames != nil { + metrics = filterMetrics(metrics, metricNames) + } + var tp expfmt.TextParser + expectedMetrics, err := tp.TextToMetricFamilies(bytes.NewReader([]byte(expected))) + if err != nil { + return fmt.Errorf("parsing expected metrics failed: %s", err) + } + + if !reflect.DeepEqual(metrics, normalizeMetricFamilies(expectedMetrics)) { + // Encode the gathered output to the readbale text format for comparison. + var buf1 bytes.Buffer + enc := expfmt.NewEncoder(&buf1, expfmt.FmtText) + for _, mf := range metrics { + if err := enc.Encode(mf); err != nil { + return fmt.Errorf("encoding result failed: %s", err) + } + } + // Encode normalized expected metrics again to generate them in the same ordering + // the registry does to spot differences more easily. + var buf2 bytes.Buffer + enc = expfmt.NewEncoder(&buf2, expfmt.FmtText) + for _, mf := range normalizeMetricFamilies(expectedMetrics) { + if err := enc.Encode(mf); err != nil { + return fmt.Errorf("encoding result failed: %s", err) + } + } + + return fmt.Errorf(` +metric output does not match expectation; want: + +%s + +got: + +%s +`, buf2.String(), buf1.String()) + } + return nil +} + +func filterMetrics(metrics []*dto.MetricFamily, names []string) []*dto.MetricFamily { + var filtered []*dto.MetricFamily + for _, m := range metrics { + drop := true + for _, name := range names { + if m.GetName() == name { + drop = false + break + } + } + if !drop { + filtered = append(filtered, m) + } + } + return filtered +} + +func removeUnusedWhitespace(s string) string { + var ( + trimmedLine string + trimmedLines []string + lines = strings.Split(s, "\n") + ) + + for _, l := range lines { + trimmedLine = strings.TrimSpace(l) + + if len(trimmedLine) > 0 { + trimmedLines = append(trimmedLines, trimmedLine) + } + } + + // The Prometheus metrics representation parser expects an empty line at the + // end otherwise fails with an unexpected EOF error. + return strings.Join(trimmedLines, "\n") + "\n" +} + +// The below sorting code is copied form the Prometheus client library modulo the added +// label pair sorting. +// https://github.com/prometheus/client_golang/blob/ea6e1db4cb8127eeb0b6954f7320363e5451820f/prometheus/registry.go#L642-L684 + +// metricSorter is a sortable slice of *dto.Metric. +type metricSorter []*dto.Metric + +func (s metricSorter) Len() int { + return len(s) +} + +func (s metricSorter) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s metricSorter) Less(i, j int) bool { + sort.Sort(prometheus.LabelPairSorter(s[i].Label)) + sort.Sort(prometheus.LabelPairSorter(s[j].Label)) + + if len(s[i].Label) != len(s[j].Label) { + return len(s[i].Label) < len(s[j].Label) + } + + for n, lp := range s[i].Label { + vi := lp.GetValue() + vj := s[j].Label[n].GetValue() + if vi != vj { + return vi < vj + } + } + + if s[i].TimestampMs == nil { + return false + } + if s[j].TimestampMs == nil { + return true + } + return s[i].GetTimestampMs() < s[j].GetTimestampMs() +} + +// normalizeMetricFamilies returns a MetricFamily slice with empty +// MetricFamilies pruned and the remaining MetricFamilies sorted by name within +// the slice, with the contained Metrics sorted within each MetricFamily. +func normalizeMetricFamilies(metricFamiliesByName map[string]*dto.MetricFamily) []*dto.MetricFamily { + for _, mf := range metricFamiliesByName { + sort.Sort(metricSorter(mf.Metric)) + } + names := make([]string, 0, len(metricFamiliesByName)) + for name, mf := range metricFamiliesByName { + if len(mf.Metric) > 0 { + names = append(names, name) + } + } + sort.Strings(names) + result := make([]*dto.MetricFamily, 0, len(names)) + for _, name := range names { + result = append(result, metricFamiliesByName[name]) + } + return result +} diff --git a/testutils/testutils_test.go b/testutils/testutils_test.go new file mode 100644 index 0000000..96ad971 --- /dev/null +++ b/testutils/testutils_test.go @@ -0,0 +1,100 @@ +package testutils + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" +) + +func TestGatherAndCompare(t *testing.T) { + const metadata = ` + # HELP some_total A value that represents a counter. + # TYPE some_total counter + ` + + c := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "some_total", + Help: "A value that represents a counter.", + ConstLabels: prometheus.Labels{ + "label1": "value1", + }, + }) + c.Inc() + + expected := ` + some_total{label1="value1"} 1 + ` + + if err := GatherAndCompare(c, metadata+expected, "some_total"); err != nil { + t.Errorf("unexpected collecting result:\n%s", err) + } +} + +func TestNoMetricFilter(t *testing.T) { + const metadata = ` + # HELP some_total A value that represents a counter. + # TYPE some_total counter + ` + + c := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "some_total", + Help: "A value that represents a counter.", + ConstLabels: prometheus.Labels{ + "label1": "value1", + }, + }) + c.Inc() + + expected := ` + some_total{label1="value1"} 1 + ` + + if err := GatherAndCompare(c, metadata+expected); err != nil { + t.Errorf("unexpected collecting result:\n%s", err) + } +} + +func TestMetricNotFound(t *testing.T) { + const metadata = ` + # HELP some_other_metric A value that represents a counter. + # TYPE some_other_metric counter + ` + + c := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "some_total", + Help: "A value that represents a counter.", + ConstLabels: prometheus.Labels{ + "label1": "value1", + }, + }) + c.Inc() + + expected := ` + some_other_metric{label1="value1"} 1 + ` + + expectedError := ` +metric output does not match expectation; want: + +# HELP some_other_metric A value that represents a counter. +# TYPE some_other_metric counter +some_other_metric{label1="value1"} 1 + + +got: + +# HELP some_total A value that represents a counter. +# TYPE some_total counter +some_total{label1="value1"} 1 + +` + + err := GatherAndCompare(c, metadata+expected) + if err == nil { + t.Error("Expected error, got no error.") + } + + if err.Error() != expectedError { + t.Errorf("Expected\n%#+v\nGot:\n%#+v\n", expectedError, err.Error()) + } +} From fc4994c93c739aabbf873535ec1951be4aa839bc Mon Sep 17 00:00:00 2001 From: beorn7 Date: Wed, 22 Aug 2018 23:54:26 +0200 Subject: [PATCH 2/7] Remove removeUnusedWhitespace It wasn't needed, as is now proven by the tests Signed-off-by: beorn7 --- testutils/testutils.go | 23 ----------------------- testutils/testutils_test.go | 3 ++- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/testutils/testutils.go b/testutils/testutils.go index c6da37b..c732a03 100644 --- a/testutils/testutils.go +++ b/testutils/testutils.go @@ -21,7 +21,6 @@ import ( "fmt" "reflect" "sort" - "strings" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" @@ -32,8 +31,6 @@ import ( // to an expected output in the Prometheus text exposition format. // metricNames allows only comparing the given metrics. All are compared if it's nil. func GatherAndCompare(c prometheus.Collector, expected string, metricNames ...string) error { - expected = removeUnusedWhitespace(expected) - reg := prometheus.NewPedanticRegistry() if err := reg.Register(c); err != nil { return fmt.Errorf("registering collector failed: %s", err) @@ -100,26 +97,6 @@ func filterMetrics(metrics []*dto.MetricFamily, names []string) []*dto.MetricFam return filtered } -func removeUnusedWhitespace(s string) string { - var ( - trimmedLine string - trimmedLines []string - lines = strings.Split(s, "\n") - ) - - for _, l := range lines { - trimmedLine = strings.TrimSpace(l) - - if len(trimmedLine) > 0 { - trimmedLines = append(trimmedLines, trimmedLine) - } - } - - // The Prometheus metrics representation parser expects an empty line at the - // end otherwise fails with an unexpected EOF error. - return strings.Join(trimmedLines, "\n") + "\n" -} - // The below sorting code is copied form the Prometheus client library modulo the added // label pair sorting. // https://github.com/prometheus/client_golang/blob/ea6e1db4cb8127eeb0b6954f7320363e5451820f/prometheus/registry.go#L642-L684 diff --git a/testutils/testutils_test.go b/testutils/testutils_test.go index 96ad971..54bdf8e 100644 --- a/testutils/testutils_test.go +++ b/testutils/testutils_test.go @@ -22,7 +22,8 @@ func TestGatherAndCompare(t *testing.T) { c.Inc() expected := ` - some_total{label1="value1"} 1 + + some_total{ label1 = "value1" } 1 ` if err := GatherAndCompare(c, metadata+expected, "some_total"); err != nil { From df0210c26c04b26e86ca119eb2fe60f89d8f54f3 Mon Sep 17 00:00:00 2001 From: beorn7 Date: Wed, 22 Aug 2018 23:57:08 +0200 Subject: [PATCH 3/7] Rename testutils to testutil and move below prometheus dir `testutil` is more in line with stdlib naming conventions. The package should be below `prometheus` as it only provides utils to test exposition code, not to test HTTP client code. Signed-off-by: beorn7 --- testutils/testutils.go => prometheus/testutil/testutil.go | 2 +- .../testutils_test.go => prometheus/testutil/testutil_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename testutils/testutils.go => prometheus/testutil/testutil.go (99%) rename testutils/testutils_test.go => prometheus/testutil/testutil_test.go (99%) diff --git a/testutils/testutils.go b/prometheus/testutil/testutil.go similarity index 99% rename from testutils/testutils.go rename to prometheus/testutil/testutil.go index c732a03..562009c 100644 --- a/testutils/testutils.go +++ b/prometheus/testutil/testutil.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package testutils +package testutil import ( "bytes" diff --git a/testutils/testutils_test.go b/prometheus/testutil/testutil_test.go similarity index 99% rename from testutils/testutils_test.go rename to prometheus/testutil/testutil_test.go index 54bdf8e..773b94c 100644 --- a/testutils/testutils_test.go +++ b/prometheus/testutil/testutil_test.go @@ -1,4 +1,4 @@ -package testutils +package testutil import ( "testing" From e60f998e9bf9a38e4e9be5bedfa86bd367cee3fa Mon Sep 17 00:00:00 2001 From: beorn7 Date: Wed, 22 Aug 2018 23:59:40 +0200 Subject: [PATCH 4/7] Make license headers consistent Signed-off-by: beorn7 --- prometheus/testutil/testutil.go | 27 ++++++++++++--------------- prometheus/testutil/testutil_test.go | 13 +++++++++++++ 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/prometheus/testutil/testutil.go b/prometheus/testutil/testutil.go index 562009c..3acb8ef 100644 --- a/prometheus/testutil/testutil.go +++ b/prometheus/testutil/testutil.go @@ -1,18 +1,15 @@ -/* -Copyright 2018 The Prometheus Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package testutil diff --git a/prometheus/testutil/testutil_test.go b/prometheus/testutil/testutil_test.go index 773b94c..a05eda7 100644 --- a/prometheus/testutil/testutil_test.go +++ b/prometheus/testutil/testutil_test.go @@ -1,3 +1,16 @@ +// Copyright 2018 he Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package testutil import ( From 154f28a3168edf383139f13aff9fd3df3d7e858b Mon Sep 17 00:00:00 2001 From: beorn7 Date: Thu, 23 Aug 2018 00:05:02 +0200 Subject: [PATCH 5/7] Fix import grouping Signed-off-by: beorn7 --- prometheus/testutil/testutil.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/prometheus/testutil/testutil.go b/prometheus/testutil/testutil.go index 3acb8ef..6145833 100644 --- a/prometheus/testutil/testutil.go +++ b/prometheus/testutil/testutil.go @@ -19,9 +19,11 @@ import ( "reflect" "sort" - "github.com/prometheus/client_golang/prometheus" - dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/expfmt" + + dto "github.com/prometheus/client_model/go" + + "github.com/prometheus/client_golang/prometheus" ) // GatherAndCompare retrieves all metrics exposed by a collector and compares it From 5ba0993f6f91a7be093f0460572ee88d435cc94c Mon Sep 17 00:00:00 2001 From: beorn7 Date: Thu, 23 Aug 2018 00:21:38 +0200 Subject: [PATCH 6/7] Improved interface - Expected text format is now read from an io.Reader. - Metrics are gathered from a Gatherer. - Added a convenience wrapper to collect from a Collector. Signed-off-by: beorn7 --- prometheus/testutil/testutil.go | 29 ++++++++++++++++------------ prometheus/testutil/testutil_test.go | 11 ++++++----- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/prometheus/testutil/testutil.go b/prometheus/testutil/testutil.go index 6145833..5d731ae 100644 --- a/prometheus/testutil/testutil.go +++ b/prometheus/testutil/testutil.go @@ -16,6 +16,7 @@ package testutil import ( "bytes" "fmt" + "io" "reflect" "sort" @@ -26,15 +27,23 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -// GatherAndCompare retrieves all metrics exposed by a collector and compares it -// to an expected output in the Prometheus text exposition format. -// metricNames allows only comparing the given metrics. All are compared if it's nil. -func GatherAndCompare(c prometheus.Collector, expected string, metricNames ...string) error { +// CollectAndCompare registers the provided Collector with a newly created +// pedantic Registry. It then does the same as GatherAndCompare, gathering the +// metrics from the pedantic Registry. +func CollectAndCompare(c prometheus.Collector, expected io.Reader, metricNames ...string) error { reg := prometheus.NewPedanticRegistry() if err := reg.Register(c); err != nil { return fmt.Errorf("registering collector failed: %s", err) } - metrics, err := reg.Gather() + return GatherAndCompare(reg, expected, metricNames...) +} + +// GatherAndCompare gathers all metrics from the provided Gatherer and compares +// it to an expected output read from the provided Reader in the Prometheus text +// exposition format. If any metricNames are provided, only metrics with those +// names are compared. +func GatherAndCompare(g prometheus.Gatherer, expected io.Reader, metricNames ...string) error { + metrics, err := g.Gather() if err != nil { return fmt.Errorf("gathering metrics failed: %s", err) } @@ -42,13 +51,13 @@ func GatherAndCompare(c prometheus.Collector, expected string, metricNames ...st metrics = filterMetrics(metrics, metricNames) } var tp expfmt.TextParser - expectedMetrics, err := tp.TextToMetricFamilies(bytes.NewReader([]byte(expected))) + expectedMetrics, err := tp.TextToMetricFamilies(expected) if err != nil { return fmt.Errorf("parsing expected metrics failed: %s", err) } if !reflect.DeepEqual(metrics, normalizeMetricFamilies(expectedMetrics)) { - // Encode the gathered output to the readbale text format for comparison. + // Encode the gathered output to the readable text format for comparison. var buf1 bytes.Buffer enc := expfmt.NewEncoder(&buf1, expfmt.FmtText) for _, mf := range metrics { @@ -82,16 +91,12 @@ got: func filterMetrics(metrics []*dto.MetricFamily, names []string) []*dto.MetricFamily { var filtered []*dto.MetricFamily for _, m := range metrics { - drop := true for _, name := range names { if m.GetName() == name { - drop = false + filtered = append(filtered, m) break } } - if !drop { - filtered = append(filtered, m) - } } return filtered } diff --git a/prometheus/testutil/testutil_test.go b/prometheus/testutil/testutil_test.go index a05eda7..b4e7b01 100644 --- a/prometheus/testutil/testutil_test.go +++ b/prometheus/testutil/testutil_test.go @@ -1,4 +1,4 @@ -// Copyright 2018 he Prometheus Authors +// Copyright 2018 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -14,12 +14,13 @@ package testutil import ( + "strings" "testing" "github.com/prometheus/client_golang/prometheus" ) -func TestGatherAndCompare(t *testing.T) { +func TestCollectAndCompare(t *testing.T) { const metadata = ` # HELP some_total A value that represents a counter. # TYPE some_total counter @@ -39,7 +40,7 @@ func TestGatherAndCompare(t *testing.T) { some_total{ label1 = "value1" } 1 ` - if err := GatherAndCompare(c, metadata+expected, "some_total"); err != nil { + if err := CollectAndCompare(c, strings.NewReader(metadata+expected), "some_total"); err != nil { t.Errorf("unexpected collecting result:\n%s", err) } } @@ -63,7 +64,7 @@ func TestNoMetricFilter(t *testing.T) { some_total{label1="value1"} 1 ` - if err := GatherAndCompare(c, metadata+expected); err != nil { + if err := CollectAndCompare(c, strings.NewReader(metadata+expected)); err != nil { t.Errorf("unexpected collecting result:\n%s", err) } } @@ -103,7 +104,7 @@ some_total{label1="value1"} 1 ` - err := GatherAndCompare(c, metadata+expected) + err := CollectAndCompare(c, strings.NewReader(metadata+expected)) if err == nil { t.Error("Expected error, got no error.") } From 7be86f93c1d6fd608a6d29493fbdacc630f10e13 Mon Sep 17 00:00:00 2001 From: beorn7 Date: Sun, 2 Sep 2018 23:36:34 +0200 Subject: [PATCH 7/7] Create an internal package This is for types we don't want to export but which are used in different packages within client_golang. Currently, that's only NormalizeMetricFamilies (used in the prometheus package and in the testutil package). More to be added as needed. Signed-off-by: beorn7 --- prometheus/internal/metric.go | 85 +++++++++++++++++++++++++++++++++ prometheus/registry.go | 71 ++------------------------- prometheus/testutil/testutil.go | 67 ++------------------------ 3 files changed, 92 insertions(+), 131 deletions(-) create mode 100644 prometheus/internal/metric.go diff --git a/prometheus/internal/metric.go b/prometheus/internal/metric.go new file mode 100644 index 0000000..351c26e --- /dev/null +++ b/prometheus/internal/metric.go @@ -0,0 +1,85 @@ +// Copyright 2018 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "sort" + + dto "github.com/prometheus/client_model/go" +) + +// metricSorter is a sortable slice of *dto.Metric. +type metricSorter []*dto.Metric + +func (s metricSorter) Len() int { + return len(s) +} + +func (s metricSorter) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s metricSorter) Less(i, j int) bool { + if len(s[i].Label) != len(s[j].Label) { + // This should not happen. The metrics are + // inconsistent. However, we have to deal with the fact, as + // people might use custom collectors or metric family injection + // to create inconsistent metrics. So let's simply compare the + // number of labels in this case. That will still yield + // reproducible sorting. + return len(s[i].Label) < len(s[j].Label) + } + for n, lp := range s[i].Label { + vi := lp.GetValue() + vj := s[j].Label[n].GetValue() + if vi != vj { + return vi < vj + } + } + + // We should never arrive here. Multiple metrics with the same + // label set in the same scrape will lead to undefined ingestion + // behavior. However, as above, we have to provide stable sorting + // here, even for inconsistent metrics. So sort equal metrics + // by their timestamp, with missing timestamps (implying "now") + // coming last. + if s[i].TimestampMs == nil { + return false + } + if s[j].TimestampMs == nil { + return true + } + return s[i].GetTimestampMs() < s[j].GetTimestampMs() +} + +// NormalizeMetricFamilies returns a MetricFamily slice with empty +// MetricFamilies pruned and the remaining MetricFamilies sorted by name within +// the slice, with the contained Metrics sorted within each MetricFamily. +func NormalizeMetricFamilies(metricFamiliesByName map[string]*dto.MetricFamily) []*dto.MetricFamily { + for _, mf := range metricFamiliesByName { + sort.Sort(metricSorter(mf.Metric)) + } + names := make([]string, 0, len(metricFamiliesByName)) + for name, mf := range metricFamiliesByName { + if len(mf.Metric) > 0 { + names = append(names, name) + } + } + sort.Strings(names) + result := make([]*dto.MetricFamily, 0, len(names)) + for _, name := range names { + result = append(result, metricFamiliesByName[name]) + } + return result +} diff --git a/prometheus/registry.go b/prometheus/registry.go index 896838f..79c3dd6 100644 --- a/prometheus/registry.go +++ b/prometheus/registry.go @@ -26,6 +26,8 @@ import ( "github.com/golang/protobuf/proto" dto "github.com/prometheus/client_model/go" + + "github.com/prometheus/client_golang/prometheus/internal" ) const ( @@ -527,7 +529,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { break } } - return normalizeMetricFamilies(metricFamiliesByName), errs.MaybeUnwrap() + return internal.NormalizeMetricFamilies(metricFamiliesByName), errs.MaybeUnwrap() } // processMetric is an internal helper method only used by the Gather method. @@ -707,72 +709,7 @@ func (gs Gatherers) Gather() ([]*dto.MetricFamily, error) { } } } - return normalizeMetricFamilies(metricFamiliesByName), errs.MaybeUnwrap() -} - -// metricSorter is a sortable slice of *dto.Metric. -type metricSorter []*dto.Metric - -func (s metricSorter) Len() int { - return len(s) -} - -func (s metricSorter) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -func (s metricSorter) Less(i, j int) bool { - if len(s[i].Label) != len(s[j].Label) { - // This should not happen. The metrics are - // inconsistent. However, we have to deal with the fact, as - // people might use custom collectors or metric family injection - // to create inconsistent metrics. So let's simply compare the - // number of labels in this case. That will still yield - // reproducible sorting. - return len(s[i].Label) < len(s[j].Label) - } - for n, lp := range s[i].Label { - vi := lp.GetValue() - vj := s[j].Label[n].GetValue() - if vi != vj { - return vi < vj - } - } - - // We should never arrive here. Multiple metrics with the same - // label set in the same scrape will lead to undefined ingestion - // behavior. However, as above, we have to provide stable sorting - // here, even for inconsistent metrics. So sort equal metrics - // by their timestamp, with missing timestamps (implying "now") - // coming last. - if s[i].TimestampMs == nil { - return false - } - if s[j].TimestampMs == nil { - return true - } - return s[i].GetTimestampMs() < s[j].GetTimestampMs() -} - -// normalizeMetricFamilies returns a MetricFamily slice with empty -// MetricFamilies pruned and the remaining MetricFamilies sorted by name within -// the slice, with the contained Metrics sorted within each MetricFamily. -func normalizeMetricFamilies(metricFamiliesByName map[string]*dto.MetricFamily) []*dto.MetricFamily { - for _, mf := range metricFamiliesByName { - sort.Sort(metricSorter(mf.Metric)) - } - names := make([]string, 0, len(metricFamiliesByName)) - for name, mf := range metricFamiliesByName { - if len(mf.Metric) > 0 { - names = append(names, name) - } - } - sort.Strings(names) - result := make([]*dto.MetricFamily, 0, len(names)) - for _, name := range names { - result = append(result, metricFamiliesByName[name]) - } - return result + return internal.NormalizeMetricFamilies(metricFamiliesByName), errs.MaybeUnwrap() } // checkSuffixCollisions checks for collisions with the “magic” suffixes the diff --git a/prometheus/testutil/testutil.go b/prometheus/testutil/testutil.go index 5d731ae..dc2ce22 100644 --- a/prometheus/testutil/testutil.go +++ b/prometheus/testutil/testutil.go @@ -18,13 +18,13 @@ import ( "fmt" "io" "reflect" - "sort" "github.com/prometheus/common/expfmt" dto "github.com/prometheus/client_model/go" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/internal" ) // CollectAndCompare registers the provided Collector with a newly created @@ -56,7 +56,7 @@ func GatherAndCompare(g prometheus.Gatherer, expected io.Reader, metricNames ... return fmt.Errorf("parsing expected metrics failed: %s", err) } - if !reflect.DeepEqual(metrics, normalizeMetricFamilies(expectedMetrics)) { + if !reflect.DeepEqual(metrics, internal.NormalizeMetricFamilies(expectedMetrics)) { // Encode the gathered output to the readable text format for comparison. var buf1 bytes.Buffer enc := expfmt.NewEncoder(&buf1, expfmt.FmtText) @@ -69,7 +69,7 @@ func GatherAndCompare(g prometheus.Gatherer, expected io.Reader, metricNames ... // the registry does to spot differences more easily. var buf2 bytes.Buffer enc = expfmt.NewEncoder(&buf2, expfmt.FmtText) - for _, mf := range normalizeMetricFamilies(expectedMetrics) { + for _, mf := range internal.NormalizeMetricFamilies(expectedMetrics) { if err := enc.Encode(mf); err != nil { return fmt.Errorf("encoding result failed: %s", err) } @@ -100,64 +100,3 @@ func filterMetrics(metrics []*dto.MetricFamily, names []string) []*dto.MetricFam } return filtered } - -// The below sorting code is copied form the Prometheus client library modulo the added -// label pair sorting. -// https://github.com/prometheus/client_golang/blob/ea6e1db4cb8127eeb0b6954f7320363e5451820f/prometheus/registry.go#L642-L684 - -// metricSorter is a sortable slice of *dto.Metric. -type metricSorter []*dto.Metric - -func (s metricSorter) Len() int { - return len(s) -} - -func (s metricSorter) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -func (s metricSorter) Less(i, j int) bool { - sort.Sort(prometheus.LabelPairSorter(s[i].Label)) - sort.Sort(prometheus.LabelPairSorter(s[j].Label)) - - if len(s[i].Label) != len(s[j].Label) { - return len(s[i].Label) < len(s[j].Label) - } - - for n, lp := range s[i].Label { - vi := lp.GetValue() - vj := s[j].Label[n].GetValue() - if vi != vj { - return vi < vj - } - } - - if s[i].TimestampMs == nil { - return false - } - if s[j].TimestampMs == nil { - return true - } - return s[i].GetTimestampMs() < s[j].GetTimestampMs() -} - -// normalizeMetricFamilies returns a MetricFamily slice with empty -// MetricFamilies pruned and the remaining MetricFamilies sorted by name within -// the slice, with the contained Metrics sorted within each MetricFamily. -func normalizeMetricFamilies(metricFamiliesByName map[string]*dto.MetricFamily) []*dto.MetricFamily { - for _, mf := range metricFamiliesByName { - sort.Sort(metricSorter(mf.Metric)) - } - names := make([]string, 0, len(metricFamiliesByName)) - for name, mf := range metricFamiliesByName { - if len(mf.Metric) > 0 { - names = append(names, name) - } - } - sort.Strings(names) - result := make([]*dto.MetricFamily, 0, len(names)) - for _, name := range names { - result = append(result, metricFamiliesByName[name]) - } - return result -}