Path: blob/main/pkg/util/testappender/testappender.go
4096 views
// Package testappender exposes utilities to test code which writes to1// Prometheus storage.Appenders.2package testappender34import (5"fmt"67"github.com/grafana/agent/pkg/util/testappender/internal/dtobuilder"8dto "github.com/prometheus/client_model/go"9"github.com/prometheus/common/model"10"github.com/prometheus/prometheus/model/exemplar"11"github.com/prometheus/prometheus/model/histogram"12"github.com/prometheus/prometheus/model/labels"13"github.com/prometheus/prometheus/model/metadata"14"github.com/prometheus/prometheus/storage"15"github.com/prometheus/prometheus/tsdb"16)1718// Appender implements storage.Appender. It keeps track of samples, metadata,19// and exemplars written to it.20//21// When Commit is called, the written data will be converted into a slice of22// *dto.MetricFamily, when can then be used for asserting against expectations23// in tests.24//25// The zero value of Appender is ready for use. Appender is only intended for26// test code, and is not optimized for production.27//28// Appender is not safe for concurrent use.29type Appender struct {30// HideTimestamps, when true, will omit timestamps from results.31HideTimestamps bool3233commitCalled, rollbackCalled bool3435samples map[string]dtobuilder.Sample // metric labels -> sample36exemplars map[string]dtobuilder.SeriesExemplar // metric labels -> series exemplar37metadata map[string]metadata.Metadata // metric family name -> metadata3839families []*dto.MetricFamily40}4142var _ storage.Appender = (*Appender)(nil)4344func (app *Appender) init() {45if app.samples == nil {46app.samples = make(map[string]dtobuilder.Sample)47}48if app.exemplars == nil {49app.exemplars = make(map[string]dtobuilder.SeriesExemplar)50}51if app.metadata == nil {52app.metadata = make(map[string]metadata.Metadata)53}54}5556// Append adds or updates a sample for a given metric, identified by labels. l57// must not be empty. If Append is called twice for the same metric, older58// samples are discarded.59//60// Upon calling Commit, a MetricFamily is created for each unique `__name__`61// label. If UpdateMetadata is not called for the named series, the series will62// be treated as untyped. The timestamp of the metric will be reported with the63// value denoted by t.64//65// The ref field is ignored, and Append always returns 0 for the resulting66// storage.SeriesRef.67func (app *Appender) Append(ref storage.SeriesRef, l labels.Labels, t int64, v float64) (storage.SeriesRef, error) {68if app.commitCalled || app.rollbackCalled {69return 0, fmt.Errorf("appender is closed")70}71app.init()7273l = l.WithoutEmpty()74if len(l) == 0 {75return 0, fmt.Errorf("empty labelset: %w", tsdb.ErrInvalidSample)76}77if lbl, dup := l.HasDuplicateLabelNames(); dup {78return 0, fmt.Errorf("label name %q is not unique: %w", lbl, tsdb.ErrInvalidSample)79}8081app.samples[l.String()] = dtobuilder.Sample{82Labels: l,83Timestamp: t,84Value: v,85PrintTimestamp: !app.HideTimestamps,86}87return 0, nil88}8990// AppendExemplar adds an exemplar for a given metric, identified by labels. l91// must not be empty.92//93// Upon calling Commit, exemplars are injected into the resulting Metrics for94// any Counter or Histogram.95//96// The ref field is ignored, and AppendExemplar always returns 0 for the97// resulting storage.SeriesRef.98func (app *Appender) AppendExemplar(ref storage.SeriesRef, l labels.Labels, e exemplar.Exemplar) (storage.SeriesRef, error) {99if app.commitCalled || app.rollbackCalled {100return 0, fmt.Errorf("appender is closed")101}102app.init()103104l = l.WithoutEmpty()105if len(l) == 0 {106return 0, fmt.Errorf("empty labelset: %w", tsdb.ErrInvalidSample)107}108if lbl, dup := l.HasDuplicateLabelNames(); dup {109return 0, fmt.Errorf("label name %q is not unique: %w", lbl, tsdb.ErrInvalidSample)110}111112app.exemplars[l.String()] = dtobuilder.SeriesExemplar{113Labels: l,114Exemplar: e,115}116return 0, nil117}118119// UpdateMetadata associates metadata for a given named metric. l must not be120// empty. Only the `__name__` label is used from the label set; other labels121// are ignored.122//123// Upon calling Commit, the metadata will be injected into the associated124// MetricFamily. If m represents a histogram, metrics suffixed with `_bucket`,125// `_sum`, and `_count` will be brought into the same MetricFamily.126func (app *Appender) UpdateMetadata(ref storage.SeriesRef, l labels.Labels, m metadata.Metadata) (storage.SeriesRef, error) {127if app.commitCalled || app.rollbackCalled {128return 0, fmt.Errorf("appender is closed")129}130app.init()131132l = l.WithoutEmpty()133if len(l) == 0 {134return 0, fmt.Errorf("empty labelset: %w", tsdb.ErrInvalidSample)135}136if lbl, dup := l.HasDuplicateLabelNames(); dup {137return 0, fmt.Errorf("label name %q is not unique: %w", lbl, tsdb.ErrInvalidSample)138}139140// Metadata is associated with just the metric family, retrieved by the141// __name__ label. All metrics for that same name always have the same142// metadata.143familyName := l.Get(model.MetricNameLabel)144if familyName == "" {145return 0, fmt.Errorf("__name__ label missing: %w", tsdb.ErrInvalidSample)146}147app.metadata[familyName] = m148return 0, nil149}150151// AppendHistogram implements storage.Appendable, but always returns an error152// as native histograms are not supported.153func (app *Appender) AppendHistogram(ref storage.SeriesRef, l labels.Labels, t int64, h *histogram.Histogram, fh *histogram.FloatHistogram) (storage.SeriesRef, error) {154return 0, fmt.Errorf("native histograms are not supported")155}156157// Commit commits pending samples, exemplars, and metadata, converting them158// into a slice of *dto.MetricsFamily. Call MetricFamilies to get the resulting159// data.160//161// After calling Commit, no other methods except MetricFamilies may be called.162func (app *Appender) Commit() error {163if app.commitCalled || app.rollbackCalled {164return fmt.Errorf("appender is closed")165}166167app.commitCalled = true168app.families = dtobuilder.Build(169app.samples,170app.exemplars,171app.metadata,172)173return nil174}175176// Rollback discards pending samples, exemplars, and metadata.177//178// After calling Rollback, no other methods may be called and the Appender must179// be discarded.180func (app *Appender) Rollback() error {181if app.commitCalled || app.rollbackCalled {182return fmt.Errorf("appender is closed")183}184185app.rollbackCalled = true186return nil187}188189// MetricFamilies returns the generated slice of *dto.MetricsFamily.190// MetricFamilies returns an error unless Commit was called.191//192// MetricFamilies always returns a non-nil slice. If no data was appended, the193// resulting slice has a length of zero.194func (app *Appender) MetricFamilies() ([]*dto.MetricFamily, error) {195if !app.commitCalled {196return nil, fmt.Errorf("MetricFamilies is not ready")197} else if app.rollbackCalled {198return nil, fmt.Errorf("appender is closed")199}200201return app.families, nil202}203204205