build: move e2e dependencies into e2e/go.mod

Several packages are only used while running the e2e suite. These
packages are less important to update, as the they can not influence the
final executable that is part of the Ceph-CSI container-image.

By moving these dependencies out of the main Ceph-CSI go.mod, it is
easier to identify if a reported CVE affects Ceph-CSI, or only the
testing (like most of the Kubernetes CVEs).

Signed-off-by: Niels de Vos <ndevos@ibm.com>
This commit is contained in:
Niels de Vos
2025-03-04 08:57:28 +01:00
committed by mergify[bot]
parent 15da101b1b
commit bec6090996
8047 changed files with 1407827 additions and 3453 deletions

View File

@ -1,46 +0,0 @@
// Copyright 2020 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
import (
"fmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil/promlint"
)
// CollectAndLint registers the provided Collector with a newly created pedantic
// Registry. It then calls GatherAndLint with that Registry and with the
// provided metricNames.
func CollectAndLint(c prometheus.Collector, metricNames ...string) ([]promlint.Problem, error) {
reg := prometheus.NewPedanticRegistry()
if err := reg.Register(c); err != nil {
return nil, fmt.Errorf("registering collector failed: %w", err)
}
return GatherAndLint(reg, metricNames...)
}
// GatherAndLint gathers all metrics from the provided Gatherer and checks them
// with the linter in the promlint package. If any metricNames are provided,
// only metrics with those names are checked.
func GatherAndLint(g prometheus.Gatherer, metricNames ...string) ([]promlint.Problem, error) {
got, err := g.Gather()
if err != nil {
return nil, fmt.Errorf("gathering metrics failed: %w", err)
}
if metricNames != nil {
got = filterMetrics(got, metricNames)
}
return promlint.NewWithMetricFamilies(got).Lint()
}

View File

@ -1,33 +0,0 @@
// Copyright 2020 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 promlint
import dto "github.com/prometheus/client_model/go"
// A Problem is an issue detected by a linter.
type Problem struct {
// The name of the metric indicated by this Problem.
Metric string
// A description of the issue for this Problem.
Text string
}
// newProblem is helper function to create a Problem.
func newProblem(mf *dto.MetricFamily, text string) Problem {
return Problem{
Metric: mf.GetName(),
Text: text,
}
}

View File

@ -1,123 +0,0 @@
// Copyright 2020 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 promlint provides a linter for Prometheus metrics.
package promlint
import (
"errors"
"io"
"sort"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
)
// A Linter is a Prometheus metrics linter. It identifies issues with metric
// names, types, and metadata, and reports them to the caller.
type Linter struct {
// The linter will read metrics in the Prometheus text format from r and
// then lint it, _and_ it will lint the metrics provided directly as
// MetricFamily proto messages in mfs. Note, however, that the current
// constructor functions New and NewWithMetricFamilies only ever set one
// of them.
r io.Reader
mfs []*dto.MetricFamily
customValidations []Validation
}
// New creates a new Linter that reads an input stream of Prometheus metrics in
// the Prometheus text exposition format.
func New(r io.Reader) *Linter {
return &Linter{
r: r,
}
}
// NewWithMetricFamilies creates a new Linter that reads from a slice of
// MetricFamily protobuf messages.
func NewWithMetricFamilies(mfs []*dto.MetricFamily) *Linter {
return &Linter{
mfs: mfs,
}
}
// AddCustomValidations adds custom validations to the linter.
func (l *Linter) AddCustomValidations(vs ...Validation) {
if l.customValidations == nil {
l.customValidations = make([]Validation, 0, len(vs))
}
l.customValidations = append(l.customValidations, vs...)
}
// Lint performs a linting pass, returning a slice of Problems indicating any
// issues found in the metrics stream. The slice is sorted by metric name
// and issue description.
func (l *Linter) Lint() ([]Problem, error) {
var problems []Problem
if l.r != nil {
d := expfmt.NewDecoder(l.r, expfmt.NewFormat(expfmt.TypeTextPlain))
mf := &dto.MetricFamily{}
for {
if err := d.Decode(mf); err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, err
}
problems = append(problems, l.lint(mf)...)
}
}
for _, mf := range l.mfs {
problems = append(problems, l.lint(mf)...)
}
// Ensure deterministic output.
sort.SliceStable(problems, func(i, j int) bool {
if problems[i].Metric == problems[j].Metric {
return problems[i].Text < problems[j].Text
}
return problems[i].Metric < problems[j].Metric
})
return problems, nil
}
// lint is the entry point for linting a single metric.
func (l *Linter) lint(mf *dto.MetricFamily) []Problem {
var problems []Problem
for _, fn := range defaultValidations {
errs := fn(mf)
for _, err := range errs {
problems = append(problems, newProblem(mf, err.Error()))
}
}
if l.customValidations != nil {
for _, fn := range l.customValidations {
errs := fn(mf)
for _, err := range errs {
problems = append(problems, newProblem(mf, err.Error()))
}
}
}
// TODO(mdlayher): lint rules for specific metrics types.
return problems
}

View File

@ -1,34 +0,0 @@
// Copyright 2020 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 promlint
import (
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/client_golang/prometheus/testutil/promlint/validations"
)
type Validation = func(mf *dto.MetricFamily) []error
var defaultValidations = []Validation{
validations.LintHelp,
validations.LintMetricUnits,
validations.LintCounter,
validations.LintHistogramSummaryReserved,
validations.LintMetricTypeInName,
validations.LintReservedChars,
validations.LintCamelCase,
validations.LintUnitAbbreviations,
validations.LintDuplicateMetric,
}

View File

@ -1,40 +0,0 @@
// Copyright 2020 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 validations
import (
"errors"
"strings"
dto "github.com/prometheus/client_model/go"
)
// LintCounter detects issues specific to counters, as well as patterns that should
// only be used with counters.
func LintCounter(mf *dto.MetricFamily) []error {
var problems []error
isCounter := mf.GetType() == dto.MetricType_COUNTER
isUntyped := mf.GetType() == dto.MetricType_UNTYPED
hasTotalSuffix := strings.HasSuffix(mf.GetName(), "_total")
switch {
case isCounter && !hasTotalSuffix:
problems = append(problems, errors.New(`counter metrics should have "_total" suffix`))
case !isUntyped && !isCounter && hasTotalSuffix:
problems = append(problems, errors.New(`non-counter metrics should not have "_total" suffix`))
}
return problems
}

View File

@ -1,37 +0,0 @@
// Copyright 2024 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 validations
import (
"errors"
"reflect"
dto "github.com/prometheus/client_model/go"
)
// LintDuplicateMetric detects duplicate metric.
func LintDuplicateMetric(mf *dto.MetricFamily) []error {
var problems []error
for i, m := range mf.Metric {
for _, k := range mf.Metric[i+1:] {
if reflect.DeepEqual(m.Label, k.Label) {
problems = append(problems, errors.New("metric not unique"))
break
}
}
}
return problems
}

View File

@ -1,101 +0,0 @@
// Copyright 2020 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 validations
import (
"errors"
"fmt"
"regexp"
"strings"
dto "github.com/prometheus/client_model/go"
)
var camelCase = regexp.MustCompile(`[a-z][A-Z]`)
// LintMetricUnits detects issues with metric unit names.
func LintMetricUnits(mf *dto.MetricFamily) []error {
var problems []error
unit, base, ok := metricUnits(*mf.Name)
if !ok {
// No known units detected.
return nil
}
// Unit is already a base unit.
if unit == base {
return nil
}
problems = append(problems, fmt.Errorf("use base unit %q instead of %q", base, unit))
return problems
}
// LintMetricTypeInName detects when the metric type is included in the metric name.
func LintMetricTypeInName(mf *dto.MetricFamily) []error {
if mf.GetType() == dto.MetricType_UNTYPED {
return nil
}
var problems []error
n := strings.ToLower(mf.GetName())
typename := strings.ToLower(mf.GetType().String())
if strings.Contains(n, "_"+typename+"_") || strings.HasSuffix(n, "_"+typename) {
problems = append(problems, fmt.Errorf(`metric name should not include type '%s'`, typename))
}
return problems
}
// LintReservedChars detects colons in metric names.
func LintReservedChars(mf *dto.MetricFamily) []error {
var problems []error
if strings.Contains(mf.GetName(), ":") {
problems = append(problems, errors.New("metric names should not contain ':'"))
}
return problems
}
// LintCamelCase detects metric names and label names written in camelCase.
func LintCamelCase(mf *dto.MetricFamily) []error {
var problems []error
if camelCase.FindString(mf.GetName()) != "" {
problems = append(problems, errors.New("metric names should be written in 'snake_case' not 'camelCase'"))
}
for _, m := range mf.GetMetric() {
for _, l := range m.GetLabel() {
if camelCase.FindString(l.GetName()) != "" {
problems = append(problems, errors.New("label names should be written in 'snake_case' not 'camelCase'"))
}
}
}
return problems
}
// LintUnitAbbreviations detects abbreviated units in the metric name.
func LintUnitAbbreviations(mf *dto.MetricFamily) []error {
var problems []error
n := strings.ToLower(mf.GetName())
for _, s := range unitAbbreviations {
if strings.Contains(n, "_"+s+"_") || strings.HasSuffix(n, "_"+s) {
problems = append(problems, errors.New("metric names should not contain abbreviated units"))
}
}
return problems
}

View File

@ -1,32 +0,0 @@
// Copyright 2020 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 validations
import (
"errors"
dto "github.com/prometheus/client_model/go"
)
// LintHelp detects issues related to the help text for a metric.
func LintHelp(mf *dto.MetricFamily) []error {
var problems []error
// Expect all metrics to have help text available.
if mf.Help == nil {
problems = append(problems, errors.New("no help text"))
}
return problems
}

View File

@ -1,63 +0,0 @@
// Copyright 2020 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 validations
import (
"errors"
"strings"
dto "github.com/prometheus/client_model/go"
)
// LintHistogramSummaryReserved detects when other types of metrics use names or labels
// reserved for use by histograms and/or summaries.
func LintHistogramSummaryReserved(mf *dto.MetricFamily) []error {
// These rules do not apply to untyped metrics.
t := mf.GetType()
if t == dto.MetricType_UNTYPED {
return nil
}
var problems []error
isHistogram := t == dto.MetricType_HISTOGRAM
isSummary := t == dto.MetricType_SUMMARY
n := mf.GetName()
if !isHistogram && strings.HasSuffix(n, "_bucket") {
problems = append(problems, errors.New(`non-histogram metrics should not have "_bucket" suffix`))
}
if !isHistogram && !isSummary && strings.HasSuffix(n, "_count") {
problems = append(problems, errors.New(`non-histogram and non-summary metrics should not have "_count" suffix`))
}
if !isHistogram && !isSummary && strings.HasSuffix(n, "_sum") {
problems = append(problems, errors.New(`non-histogram and non-summary metrics should not have "_sum" suffix`))
}
for _, m := range mf.GetMetric() {
for _, l := range m.GetLabel() {
ln := l.GetName()
if !isHistogram && ln == "le" {
problems = append(problems, errors.New(`non-histogram metrics should not have "le" label`))
}
if !isSummary && ln == "quantile" {
problems = append(problems, errors.New(`non-summary metrics should not have "quantile" label`))
}
}
}
return problems
}

View File

@ -1,118 +0,0 @@
// Copyright 2020 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 validations
import "strings"
// Units and their possible prefixes recognized by this library. More can be
// added over time as needed.
var (
// map a unit to the appropriate base unit.
units = map[string]string{
// Base units.
"amperes": "amperes",
"bytes": "bytes",
"celsius": "celsius", // Also allow Celsius because it is common in typical Prometheus use cases.
"grams": "grams",
"joules": "joules",
"kelvin": "kelvin", // SI base unit, used in special cases (e.g. color temperature, scientific measurements).
"meters": "meters", // Both American and international spelling permitted.
"metres": "metres",
"seconds": "seconds",
"volts": "volts",
// Non base units.
// Time.
"minutes": "seconds",
"hours": "seconds",
"days": "seconds",
"weeks": "seconds",
// Temperature.
"kelvins": "kelvin",
"fahrenheit": "celsius",
"rankine": "celsius",
// Length.
"inches": "meters",
"yards": "meters",
"miles": "meters",
// Bytes.
"bits": "bytes",
// Energy.
"calories": "joules",
// Mass.
"pounds": "grams",
"ounces": "grams",
}
unitPrefixes = []string{
"pico",
"nano",
"micro",
"milli",
"centi",
"deci",
"deca",
"hecto",
"kilo",
"kibi",
"mega",
"mibi",
"giga",
"gibi",
"tera",
"tebi",
"peta",
"pebi",
}
// Common abbreviations that we'd like to discourage.
unitAbbreviations = []string{
"s",
"ms",
"us",
"ns",
"sec",
"b",
"kb",
"mb",
"gb",
"tb",
"pb",
"m",
"h",
"d",
}
)
// metricUnits attempts to detect known unit types used as part of a metric name,
// e.g. "foo_bytes_total" or "bar_baz_milligrams".
func metricUnits(m string) (unit, base string, ok bool) {
ss := strings.Split(m, "_")
for _, s := range ss {
if base, found := units[s]; found {
return s, base, true
}
for _, p := range unitPrefixes {
if strings.HasPrefix(s, p) {
if base, found := units[s[len(p):]]; found {
return s, base, true
}
}
}
}
return "", "", false
}

View File

@ -1,334 +0,0 @@
// 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 provides helpers to test code using the prometheus package
// of client_golang.
//
// While writing unit tests to verify correct instrumentation of your code, it's
// a common mistake to mostly test the instrumentation library instead of your
// own code. Rather than verifying that a prometheus.Counter's value has changed
// as expected or that it shows up in the exposition after registration, it is
// in general more robust and more faithful to the concept of unit tests to use
// mock implementations of the prometheus.Counter and prometheus.Registerer
// interfaces that simply assert that the Add or Register methods have been
// called with the expected arguments. However, this might be overkill in simple
// scenarios. The ToFloat64 function is provided for simple inspection of a
// single-value metric, but it has to be used with caution.
//
// End-to-end tests to verify all or larger parts of the metrics exposition can
// be implemented with the CollectAndCompare or GatherAndCompare functions. The
// most appropriate use is not so much testing instrumentation of your code, but
// testing custom prometheus.Collector implementations and in particular whole
// exporters, i.e. programs that retrieve telemetry data from a 3rd party source
// and convert it into Prometheus metrics.
//
// In a similar pattern, CollectAndLint and GatherAndLint can be used to detect
// metrics that have issues with their name, type, or metadata without being
// necessarily invalid, e.g. a counter with a name missing the “_total” suffix.
package testutil
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"github.com/kylelemons/godebug/diff"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
"github.com/prometheus/common/model"
"google.golang.org/protobuf/proto"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/internal"
)
// ToFloat64 collects all Metrics from the provided Collector. It expects that
// this results in exactly one Metric being collected, which must be a Gauge,
// Counter, or Untyped. In all other cases, ToFloat64 panics. ToFloat64 returns
// the value of the collected Metric.
//
// The Collector provided is typically a simple instance of Gauge or Counter, or
// less commonly a GaugeVec or CounterVec with exactly one element. But any
// Collector fulfilling the prerequisites described above will do.
//
// Use this function with caution. It is computationally very expensive and thus
// not suited at all to read values from Metrics in regular code. This is really
// only for testing purposes, and even for testing, other approaches are often
// more appropriate (see this package's documentation).
//
// A clear anti-pattern would be to use a metric type from the prometheus
// package to track values that are also needed for something else than the
// exposition of Prometheus metrics. For example, you would like to track the
// number of items in a queue because your code should reject queuing further
// items if a certain limit is reached. It is tempting to track the number of
// items in a prometheus.Gauge, as it is then easily available as a metric for
// exposition, too. However, then you would need to call ToFloat64 in your
// regular code, potentially quite often. The recommended way is to track the
// number of items conventionally (in the way you would have done it without
// considering Prometheus metrics) and then expose the number with a
// prometheus.GaugeFunc.
func ToFloat64(c prometheus.Collector) float64 {
var (
m prometheus.Metric
mCount int
mChan = make(chan prometheus.Metric)
done = make(chan struct{})
)
go func() {
for m = range mChan {
mCount++
}
close(done)
}()
c.Collect(mChan)
close(mChan)
<-done
if mCount != 1 {
panic(fmt.Errorf("collected %d metrics instead of exactly 1", mCount))
}
pb := &dto.Metric{}
if err := m.Write(pb); err != nil {
panic(fmt.Errorf("error happened while collecting metrics: %w", err))
}
if pb.Gauge != nil {
return pb.Gauge.GetValue()
}
if pb.Counter != nil {
return pb.Counter.GetValue()
}
if pb.Untyped != nil {
return pb.Untyped.GetValue()
}
panic(fmt.Errorf("collected a non-gauge/counter/untyped metric: %s", pb))
}
// CollectAndCount registers the provided Collector with a newly created
// pedantic Registry. It then calls GatherAndCount with that Registry and with
// the provided metricNames. In the unlikely case that the registration or the
// gathering fails, this function panics. (This is inconsistent with the other
// CollectAnd… functions in this package and has historical reasons. Changing
// the function signature would be a breaking change and will therefore only
// happen with the next major version bump.)
func CollectAndCount(c prometheus.Collector, metricNames ...string) int {
reg := prometheus.NewPedanticRegistry()
if err := reg.Register(c); err != nil {
panic(fmt.Errorf("registering collector failed: %w", err))
}
result, err := GatherAndCount(reg, metricNames...)
if err != nil {
panic(err)
}
return result
}
// GatherAndCount gathers all metrics from the provided Gatherer and counts
// them. It returns the number of metric children in all gathered metric
// families together. If any metricNames are provided, only metrics with those
// names are counted.
func GatherAndCount(g prometheus.Gatherer, metricNames ...string) (int, error) {
got, err := g.Gather()
if err != nil {
return 0, fmt.Errorf("gathering metrics failed: %w", err)
}
if metricNames != nil {
got = filterMetrics(got, metricNames)
}
result := 0
for _, mf := range got {
result += len(mf.GetMetric())
}
return result, nil
}
// ScrapeAndCompare calls a remote exporter's endpoint which is expected to return some metrics in
// plain text format. Then it compares it with the results that the `expected` would return.
// If the `metricNames` is not empty it would filter the comparison only to the given metric names.
//
// NOTE: Be mindful of accidental discrepancies between expected and metricNames; metricNames filter
// both expected and scraped metrics. See https://github.com/prometheus/client_golang/issues/1351.
func ScrapeAndCompare(url string, expected io.Reader, metricNames ...string) error {
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("scraping metrics failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("the scraping target returned a status code other than 200: %d",
resp.StatusCode)
}
scraped, err := convertReaderToMetricFamily(resp.Body)
if err != nil {
return err
}
wanted, err := convertReaderToMetricFamily(expected)
if err != nil {
return err
}
return compareMetricFamilies(scraped, wanted, metricNames...)
}
// CollectAndCompare collects the metrics identified by `metricNames` and compares them in the Prometheus text
// exposition format to the data read from expected.
//
// NOTE: Be mindful of accidental discrepancies between expected and metricNames; metricNames filter
// both expected and collected metrics. See https://github.com/prometheus/client_golang/issues/1351.
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: %w", err)
}
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.
//
// NOTE: Be mindful of accidental discrepancies between expected and metricNames; metricNames filter
// both expected and gathered metrics. See https://github.com/prometheus/client_golang/issues/1351.
func GatherAndCompare(g prometheus.Gatherer, expected io.Reader, metricNames ...string) error {
return TransactionalGatherAndCompare(prometheus.ToTransactionalGatherer(g), expected, metricNames...)
}
// TransactionalGatherAndCompare 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.
//
// NOTE: Be mindful of accidental discrepancies between expected and metricNames; metricNames filter
// both expected and gathered metrics. See https://github.com/prometheus/client_golang/issues/1351.
func TransactionalGatherAndCompare(g prometheus.TransactionalGatherer, expected io.Reader, metricNames ...string) error {
got, done, err := g.Gather()
defer done()
if err != nil {
return fmt.Errorf("gathering metrics failed: %w", err)
}
wanted, err := convertReaderToMetricFamily(expected)
if err != nil {
return err
}
return compareMetricFamilies(got, wanted, metricNames...)
}
// CollectAndFormat collects the metrics identified by `metricNames` and returns them in the given format.
func CollectAndFormat(c prometheus.Collector, format expfmt.FormatType, metricNames ...string) ([]byte, error) {
reg := prometheus.NewPedanticRegistry()
if err := reg.Register(c); err != nil {
return nil, fmt.Errorf("registering collector failed: %w", err)
}
gotFiltered, err := reg.Gather()
if err != nil {
return nil, fmt.Errorf("gathering metrics failed: %w", err)
}
gotFiltered = filterMetrics(gotFiltered, metricNames)
var gotFormatted bytes.Buffer
enc := expfmt.NewEncoder(&gotFormatted, expfmt.NewFormat(format))
for _, mf := range gotFiltered {
if err := enc.Encode(mf); err != nil {
return nil, fmt.Errorf("encoding gathered metrics failed: %w", err)
}
}
return gotFormatted.Bytes(), nil
}
// convertReaderToMetricFamily would read from a io.Reader object and convert it to a slice of
// dto.MetricFamily.
func convertReaderToMetricFamily(reader io.Reader) ([]*dto.MetricFamily, error) {
var tp expfmt.TextParser
notNormalized, err := tp.TextToMetricFamilies(reader)
if err != nil {
return nil, fmt.Errorf("converting reader to metric families failed: %w", err)
}
// The text protocol handles empty help fields inconsistently. When
// encoding, any non-nil value, include the empty string, produces a
// "# HELP" line. But when decoding, the help field is only set to a
// non-nil value if the "# HELP" line contains a non-empty value.
//
// Because metrics in a registry always have non-nil help fields, populate
// any nil help fields in the parsed metrics with the empty string so that
// when we compare text encodings, the results are consistent.
for _, metric := range notNormalized {
if metric.Help == nil {
metric.Help = proto.String("")
}
}
return internal.NormalizeMetricFamilies(notNormalized), nil
}
// compareMetricFamilies would compare 2 slices of metric families, and optionally filters both of
// them to the `metricNames` provided.
func compareMetricFamilies(got, expected []*dto.MetricFamily, metricNames ...string) error {
if metricNames != nil {
got = filterMetrics(got, metricNames)
expected = filterMetrics(expected, metricNames)
}
return compare(got, expected)
}
// compare encodes both provided slices of metric families into the text format,
// compares their string message, and returns an error if they do not match.
// The error contains the encoded text of both the desired and the actual
// result.
func compare(got, want []*dto.MetricFamily) error {
var gotBuf, wantBuf bytes.Buffer
enc := expfmt.NewEncoder(&gotBuf, expfmt.NewFormat(expfmt.TypeTextPlain).WithEscapingScheme(model.NoEscaping))
for _, mf := range got {
if err := enc.Encode(mf); err != nil {
return fmt.Errorf("encoding gathered metrics failed: %w", err)
}
}
enc = expfmt.NewEncoder(&wantBuf, expfmt.NewFormat(expfmt.TypeTextPlain).WithEscapingScheme(model.NoEscaping))
for _, mf := range want {
if err := enc.Encode(mf); err != nil {
return fmt.Errorf("encoding expected metrics failed: %w", err)
}
}
if diffErr := diff.Diff(gotBuf.String(), wantBuf.String()); diffErr != "" {
return errors.New(diffErr)
}
return nil
}
func filterMetrics(metrics []*dto.MetricFamily, names []string) []*dto.MetricFamily {
var filtered []*dto.MetricFamily
for _, m := range metrics {
for _, name := range names {
if m.GetName() == name {
filtered = append(filtered, m)
break
}
}
}
return filtered
}