From c0994f7532f3329f843b42362c8e47e1f13e254a Mon Sep 17 00:00:00 2001 From: Arthur Silva Sens Date: Fri, 11 Aug 2023 18:02:28 -0300 Subject: [PATCH 1/4] Extend Counters, Summaries and Histograms with creation timestamp Signed-off-by: Arthur Silva Sens --- go.mod | 2 +- go.sum | 4 +- prometheus/counter.go | 17 ++++++-- prometheus/counter_test.go | 37 ++++++++++++++++++ prometheus/gauge.go | 2 +- prometheus/histogram.go | 22 ++++++++--- prometheus/histogram_test.go | 41 ++++++++++++++++++++ prometheus/metric.go | 2 + prometheus/summary.go | 26 +++++++++++-- prometheus/summary_test.go | 75 ++++++++++++++++++++++++++++++++++++ prometheus/value.go | 49 +++++++++++++++++++++-- prometheus/value_test.go | 57 +++++++++++++++++++++++++++ 12 files changed, 315 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index a0afbd4ff..97e627e7d 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 github.com/davecgh/go-spew v1.1.1 github.com/json-iterator/go v1.1.12 - github.com/prometheus/client_model v0.4.0 + github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 github.com/prometheus/common v0.44.0 github.com/prometheus/procfs v0.11.1 golang.org/x/sys v0.11.0 diff --git a/go.sum b/go.sum index bbabe429b..d07d4220d 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= -github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= diff --git a/prometheus/counter.go b/prometheus/counter.go index d8ade9ba7..a3139ffa3 100644 --- a/prometheus/counter.go +++ b/prometheus/counter.go @@ -20,6 +20,7 @@ import ( "time" dto "github.com/prometheus/client_model/go" + "google.golang.org/protobuf/types/known/timestamppb" ) // Counter is a Metric that represents a single numerical value that only ever @@ -90,8 +91,12 @@ func NewCounter(opts CounterOpts) Counter { nil, opts.ConstLabels, ) - result := &counter{desc: desc, labelPairs: desc.constLabelPairs, now: time.Now} + if opts.now == nil { + opts.now = time.Now + } + result := &counter{desc: desc, labelPairs: desc.constLabelPairs, now: opts.now} result.init(result) // Init self-collection. + result.createdTs = timestamppb.New(opts.now()) return result } @@ -106,10 +111,11 @@ type counter struct { selfCollector desc *Desc + createdTs *timestamppb.Timestamp labelPairs []*dto.LabelPair exemplar atomic.Value // Containing nil or a *dto.Exemplar. - now func() time.Time // To mock out time.Now() for testing. + now func() time.Time // For testing, all constructors put time.Now() here. } func (c *counter) Desc() *Desc { @@ -160,7 +166,8 @@ func (c *counter) Write(out *dto.Metric) error { } val := c.get() - return populateMetric(CounterValue, val, c.labelPairs, exemplar, out) + ct := c.createdTs.AsTime() + return populateMetric(CounterValue, val, c.labelPairs, exemplar, out, &ct) } func (c *counter) updateExemplar(v float64, l Labels) { @@ -200,6 +207,9 @@ func (v2) NewCounterVec(opts CounterVecOpts) *CounterVec { opts.VariableLabels, opts.ConstLabels, ) + if opts.now == nil { + opts.now = time.Now + } return &CounterVec{ MetricVec: NewMetricVec(desc, func(lvs ...string) Metric { if len(lvs) != len(desc.variableLabels.names) { @@ -207,6 +217,7 @@ func (v2) NewCounterVec(opts CounterVecOpts) *CounterVec { } result := &counter{desc: desc, labelPairs: MakeLabelPairs(desc, lvs), now: time.Now} result.init(result) // Init self-collection. + result.createdTs = timestamppb.New(opts.now()) return result }), } diff --git a/prometheus/counter_test.go b/prometheus/counter_test.go index cf0fd54d7..6b850b6c1 100644 --- a/prometheus/counter_test.go +++ b/prometheus/counter_test.go @@ -298,3 +298,40 @@ func TestCounterExemplar(t *testing.T) { t.Error("adding exemplar with oversized labels succeeded") } } + +func TestCounterCreatedTimestamp(t *testing.T) { + now := time.Now() + counter := NewCounter(CounterOpts{ + Name: "test", + Help: "test help", + now: func() time.Time { return now }, + }) + + var metric dto.Metric + if err := counter.Write(&metric); err != nil { + t.Fatal(err) + } + + if metric.Counter.CreatedTimestamp.AsTime().Unix() != now.Unix() { + t.Errorf("expected created timestamp %d, got %d", now.Unix(), metric.Counter.CreatedTimestamp.AsTime().Unix()) + } +} + +func TestCounterVecCreatedTimestamp(t *testing.T) { + now := time.Now() + counterVec := NewCounterVec(CounterOpts{ + Name: "test", + Help: "test help", + now: func() time.Time { return now }, + }, []string{"label"}) + counter := counterVec.WithLabelValues("value") + + var metric dto.Metric + if err := counter.Write(&metric); err != nil { + t.Fatal(err) + } + + if metric.Counter.CreatedTimestamp.AsTime().Unix() != now.Unix() { + t.Errorf("expected created timestamp %d, got %d", now.Unix(), metric.Counter.CreatedTimestamp.AsTime().Unix()) + } +} diff --git a/prometheus/gauge.go b/prometheus/gauge.go index d2bce2168..8e94883e2 100644 --- a/prometheus/gauge.go +++ b/prometheus/gauge.go @@ -135,7 +135,7 @@ func (g *gauge) Sub(val float64) { func (g *gauge) Write(out *dto.Metric) error { val := math.Float64frombits(atomic.LoadUint64(&g.valBits)) - return populateMetric(GaugeValue, val, g.labelPairs, nil, out) + return populateMetric(GaugeValue, val, g.labelPairs, nil, out, nil) } // GaugeVec is a Collector that bundles a set of Gauges that all share the same diff --git a/prometheus/histogram.go b/prometheus/histogram.go index 6e9b44f1a..94c79624f 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -25,6 +25,7 @@ import ( dto "github.com/prometheus/client_model/go" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) // nativeHistogramBounds for the frac of observed values. Only relevant for @@ -471,6 +472,8 @@ type HistogramOpts struct { NativeHistogramMaxBucketNumber uint32 NativeHistogramMinResetDuration time.Duration NativeHistogramMaxZeroThreshold float64 + + now func() time.Time // For testing, all constructors put time.Now() here. } // HistogramVecOpts bundles the options to create a HistogramVec metric. @@ -568,7 +571,11 @@ func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogr atomic.StoreInt32(&h.counts[1].nativeHistogramSchema, h.nativeHistogramSchema) h.exemplars = make([]atomic.Value, len(h.upperBounds)+1) + if opts.now == nil { + opts.now = time.Now + } h.init(h) // Init self-collection. + h.createdTs = timestamppb.New(opts.now()) return h } @@ -707,8 +714,9 @@ type histogram struct { nativeHistogramMaxBuckets uint32 nativeHistogramMinResetDuration time.Duration lastResetTime time.Time // Protected by mtx. + createdTs *timestamppb.Timestamp - now func() time.Time // To mock out time.Now() for testing. + now func() time.Time // For testing, all constructors put time.Now() here. } func (h *histogram) Desc() *Desc { @@ -747,9 +755,10 @@ func (h *histogram) Write(out *dto.Metric) error { waitForCooldown(count, coldCounts) his := &dto.Histogram{ - Bucket: make([]*dto.Bucket, len(h.upperBounds)), - SampleCount: proto.Uint64(count), - SampleSum: proto.Float64(math.Float64frombits(atomic.LoadUint64(&coldCounts.sumBits))), + Bucket: make([]*dto.Bucket, len(h.upperBounds)), + SampleCount: proto.Uint64(count), + SampleSum: proto.Float64(math.Float64frombits(atomic.LoadUint64(&coldCounts.sumBits))), + CreatedTimestamp: h.createdTs, } out.Histogram = his out.Label = h.labelPairs @@ -1194,6 +1203,7 @@ type constHistogram struct { sum float64 buckets map[float64]uint64 labelPairs []*dto.LabelPair + createdTs *timestamppb.Timestamp } func (h *constHistogram) Desc() *Desc { @@ -1201,7 +1211,9 @@ func (h *constHistogram) Desc() *Desc { } func (h *constHistogram) Write(out *dto.Metric) error { - his := &dto.Histogram{} + his := &dto.Histogram{ + CreatedTimestamp: h.createdTs, + } buckets := make([]*dto.Bucket, 0, len(h.buckets)) diff --git a/prometheus/histogram_test.go b/prometheus/histogram_test.go index e2bc16240..379fcb029 100644 --- a/prometheus/histogram_test.go +++ b/prometheus/histogram_test.go @@ -1152,3 +1152,44 @@ func TestGetLe(t *testing.T) { } } } + +func TestHistogramCreatedTimestamp(t *testing.T) { + now := time.Now() + + histogram := NewHistogram(HistogramOpts{ + Name: "test", + Help: "test help", + Buckets: []float64{1, 2, 3, 4}, + now: func() time.Time { return now }, + }) + + var metric dto.Metric + if err := histogram.Write(&metric); err != nil { + t.Fatal(err) + } + + if metric.Histogram.CreatedTimestamp.AsTime().Unix() != now.Unix() { + t.Errorf("expected created timestamp %d, got %d", now.Unix(), metric.Histogram.CreatedTimestamp.AsTime().Unix()) + } +} + +func TestHistogramVecCreatedTimestamp(t *testing.T) { + now := time.Now() + + histogramVec := NewHistogramVec(HistogramOpts{ + Name: "test", + Help: "test help", + Buckets: []float64{1, 2, 3, 4}, + now: func() time.Time { return now }, + }, []string{"label"}) + histogram := histogramVec.WithLabelValues("value").(Histogram) + + var metric dto.Metric + if err := histogram.Write(&metric); err != nil { + t.Fatal(err) + } + + if metric.Histogram.CreatedTimestamp.AsTime().Unix() != now.Unix() { + t.Errorf("expected created timestamp %d, got %d", now.Unix(), metric.Histogram.CreatedTimestamp.AsTime().Unix()) + } +} diff --git a/prometheus/metric.go b/prometheus/metric.go index 07bbc9d76..aad02eead 100644 --- a/prometheus/metric.go +++ b/prometheus/metric.go @@ -92,6 +92,8 @@ type Opts struct { // machine_role metric). See also // https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels-not-static-scraped-labels ConstLabels Labels + + now func() time.Time // For testing, all constructors put time.Now() here. } // BuildFQName joins the given three name components by "_". Empty name diff --git a/prometheus/summary.go b/prometheus/summary.go index 440f29efd..2b7e9d19d 100644 --- a/prometheus/summary.go +++ b/prometheus/summary.go @@ -26,6 +26,7 @@ import ( "github.com/beorn7/perks/quantile" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) // quantileLabel is used for the label that defines the quantile in a @@ -145,6 +146,8 @@ type SummaryOpts struct { // is the internal buffer size of the underlying package // "github.com/bmizerany/perks/quantile"). BufCap uint32 + + now func() time.Time // For testing, all constructors put time.Now() here. } // SummaryVecOpts bundles the options to create a SummaryVec metric. @@ -222,6 +225,9 @@ func newSummary(desc *Desc, opts SummaryOpts, labelValues ...string) Summary { opts.BufCap = DefBufCap } + if opts.now == nil { + opts.now = time.Now + } if len(opts.Objectives) == 0 { // Use the lock-free implementation of a Summary without objectives. s := &noObjectivesSummary{ @@ -230,6 +236,7 @@ func newSummary(desc *Desc, opts SummaryOpts, labelValues ...string) Summary { counts: [2]*summaryCounts{{}, {}}, } s.init(s) // Init self-collection. + s.createdTs = timestamppb.New(opts.now()) return s } @@ -259,6 +266,7 @@ func newSummary(desc *Desc, opts SummaryOpts, labelValues ...string) Summary { sort.Float64s(s.sortedObjectives) s.init(s) // Init self-collection. + s.createdTs = timestamppb.New(opts.now()) return s } @@ -286,6 +294,8 @@ type summary struct { headStream *quantile.Stream headStreamIdx int headStreamExpTime, hotBufExpTime time.Time + + createdTs *timestamppb.Timestamp } func (s *summary) Desc() *Desc { @@ -307,7 +317,9 @@ func (s *summary) Observe(v float64) { } func (s *summary) Write(out *dto.Metric) error { - sum := &dto.Summary{} + sum := &dto.Summary{ + CreatedTimestamp: s.createdTs, + } qs := make([]*dto.Quantile, 0, len(s.objectives)) s.bufMtx.Lock() @@ -440,6 +452,8 @@ type noObjectivesSummary struct { counts [2]*summaryCounts labelPairs []*dto.LabelPair + + createdTs *timestamppb.Timestamp } func (s *noObjectivesSummary) Desc() *Desc { @@ -490,8 +504,9 @@ func (s *noObjectivesSummary) Write(out *dto.Metric) error { } sum := &dto.Summary{ - SampleCount: proto.Uint64(count), - SampleSum: proto.Float64(math.Float64frombits(atomic.LoadUint64(&coldCounts.sumBits))), + SampleCount: proto.Uint64(count), + SampleSum: proto.Float64(math.Float64frombits(atomic.LoadUint64(&coldCounts.sumBits))), + CreatedTimestamp: s.createdTs, } out.Summary = sum @@ -681,6 +696,7 @@ type constSummary struct { sum float64 quantiles map[float64]float64 labelPairs []*dto.LabelPair + createdTs *timestamppb.Timestamp } func (s *constSummary) Desc() *Desc { @@ -688,7 +704,9 @@ func (s *constSummary) Desc() *Desc { } func (s *constSummary) Write(out *dto.Metric) error { - sum := &dto.Summary{} + sum := &dto.Summary{ + CreatedTimestamp: s.createdTs, + } qs := make([]*dto.Quantile, 0, len(s.quantiles)) sum.SampleCount = proto.Uint64(s.count) diff --git a/prometheus/summary_test.go b/prometheus/summary_test.go index eff733a73..1213bb11a 100644 --- a/prometheus/summary_test.go +++ b/prometheus/summary_test.go @@ -420,3 +420,78 @@ func getBounds(vars []float64, q, ε float64) (min, max float64) { } return } + +func TestSummaryCreatedTimestamp(t *testing.T) { + testCases := []struct { + desc string + objectives map[float64]float64 + }{ + { + desc: "summary with objectives", + objectives: map[float64]float64{ + 1.0: 1.0, + }, + }, + { + desc: "no objectives summary", + objectives: nil, + }, + } + now := time.Now() + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + summary := NewSummary(SummaryOpts{ + Name: "test", + Help: "test help", + Objectives: test.objectives, + now: func() time.Time { return now }, + }) + + var metric dto.Metric + summary.Write(&metric) + + if metric.Summary.CreatedTimestamp.AsTime().Unix() != now.Unix() { + t.Errorf("expected created timestamp %d, got %d", now.Unix(), metric.Summary.CreatedTimestamp.AsTime().Unix()) + } + }) + } +} + +func TestSummaryVecCreatedTimestamp(t *testing.T) { + testCases := []struct { + desc string + objectives map[float64]float64 + }{ + { + desc: "summary with objectives", + objectives: map[float64]float64{ + 1.0: 1.0, + }, + }, + { + desc: "no objectives summary", + objectives: nil, + }, + } + now := time.Now() + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + summaryVec := NewSummaryVec(SummaryOpts{ + Name: "test", + Help: "test help", + Objectives: test.objectives, + now: func() time.Time { return now }, + }, + []string{"label"}) + summary := summaryVec.WithLabelValues("value").(Summary) + var metric dto.Metric + summary.Write(&metric) + + if metric.Summary.CreatedTimestamp.AsTime().Unix() != now.Unix() { + t.Errorf("expected created timestamp %d, got %d", now.Unix(), metric.Summary.CreatedTimestamp.AsTime().Unix()) + } + }) + } +} diff --git a/prometheus/value.go b/prometheus/value.go index 4bf5727b5..e1a5f57ca 100644 --- a/prometheus/value.go +++ b/prometheus/value.go @@ -14,6 +14,7 @@ package prometheus import ( + "errors" "fmt" "sort" "time" @@ -91,7 +92,7 @@ func (v *valueFunc) Desc() *Desc { } func (v *valueFunc) Write(out *dto.Metric) error { - return populateMetric(v.valType, v.function(), v.labelPairs, nil, out) + return populateMetric(v.valType, v.function(), v.labelPairs, nil, out, nil) } // NewConstMetric returns a metric with one fixed value that cannot be @@ -110,7 +111,7 @@ func NewConstMetric(desc *Desc, valueType ValueType, value float64, labelValues } metric := &dto.Metric{} - if err := populateMetric(valueType, value, MakeLabelPairs(desc, labelValues), nil, metric); err != nil { + if err := populateMetric(valueType, value, MakeLabelPairs(desc, labelValues), nil, metric, nil); err != nil { return nil, err } @@ -130,6 +131,43 @@ func MustNewConstMetric(desc *Desc, valueType ValueType, value float64, labelVal return m } +// NewConstMetricWithCreatedTimestamp does the same thing as NewConstMetric, but generates Counters +// with created timestamp set and returns an error for other metric types. +func NewConstMetricWithCreatedTimestamp(desc *Desc, valueType ValueType, value float64, ct time.Time, labelValues ...string) (Metric, error) { + if desc.err != nil { + return nil, desc.err + } + if err := validateLabelValues(labelValues, len(desc.variableLabels.names)); err != nil { + return nil, err + } + switch valueType { + case CounterValue: + break + default: + return nil, errors.New("Created timestamps are only supported for counters") + } + + metric := &dto.Metric{} + if err := populateMetric(valueType, value, MakeLabelPairs(desc, labelValues), nil, metric, &ct); err != nil { + return nil, err + } + + return &constMetric{ + desc: desc, + metric: metric, + }, nil +} + +// MustNewConstMetricWithCreatedTimestamp is a version of NewConstMetricWithCreatedTimestamp that panics where +// NewConstMetricWithCreatedTimestamp would have returned an error. +func MustNewConstMetricWithCreatedTimestamp(desc *Desc, valueType ValueType, value float64, ct time.Time, labelValues ...string) Metric { + m, err := NewConstMetricWithCreatedTimestamp(desc, valueType, value, ct, labelValues...) + if err != nil { + panic(err) + } + return m +} + type constMetric struct { desc *Desc metric *dto.Metric @@ -153,11 +191,16 @@ func populateMetric( labelPairs []*dto.LabelPair, e *dto.Exemplar, m *dto.Metric, + createdTimestamp *time.Time, ) error { m.Label = labelPairs switch t { case CounterValue: - m.Counter = &dto.Counter{Value: proto.Float64(v), Exemplar: e} + var ct *timestamppb.Timestamp + if createdTimestamp != nil { + ct = timestamppb.New(*createdTimestamp) + } + m.Counter = &dto.Counter{Value: proto.Float64(v), Exemplar: e, CreatedTimestamp: ct} case GaugeValue: m.Gauge = &dto.Gauge{Value: proto.Float64(v)} case UntypedValue: diff --git a/prometheus/value_test.go b/prometheus/value_test.go index 51867b5e4..3e06ea8f7 100644 --- a/prometheus/value_test.go +++ b/prometheus/value_test.go @@ -16,6 +16,10 @@ package prometheus import ( "fmt" "testing" + "time" + + dto "github.com/prometheus/client_model/go" + "google.golang.org/protobuf/types/known/timestamppb" ) func TestNewConstMetricInvalidLabelValues(t *testing.T) { @@ -54,3 +58,56 @@ func TestNewConstMetricInvalidLabelValues(t *testing.T) { } } } + +func TestNewConstMetricWithCreatedTimestamp(t *testing.T) { + now := time.Now() + testCases := []struct { + desc string + metricType ValueType + createdTimestamp time.Time + expecErr bool + expectedCt *timestamppb.Timestamp + }{ + { + desc: "gauge with CT", + metricType: GaugeValue, + createdTimestamp: now, + expecErr: true, + expectedCt: nil, + }, + { + desc: "counter with CT", + metricType: CounterValue, + createdTimestamp: now, + expecErr: false, + expectedCt: timestamppb.New(now), + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + metricDesc := NewDesc( + "sample_value", + "sample value", + nil, + nil, + ) + m, err := NewConstMetricWithCreatedTimestamp(metricDesc, test.metricType, float64(1), test.createdTimestamp) + if test.expecErr && err == nil { + t.Errorf("Expected error is test %s, got no err", test.desc) + } + if !test.expecErr && err != nil { + t.Errorf("Didn't expect error in test %s, got %s", test.desc, err.Error()) + } + + if test.expectedCt != nil { + var metric dto.Metric + m.Write(&metric) + if metric.Counter.CreatedTimestamp.AsTime() != test.expectedCt.AsTime() { + t.Errorf("Expected timestamp %v, got %v", test.expectedCt, &metric.Counter.CreatedTimestamp) + } + } + }) + } +} From 04270233927870c762f0f51627c142743fa3906e Mon Sep 17 00:00:00 2001 From: Arthur Silva Sens Date: Fri, 11 Aug 2023 18:43:28 -0300 Subject: [PATCH 2/4] Backport created timestamp to existing tests Signed-off-by: Arthur Silva Sens --- prometheus/counter_test.go | 26 ++++++++++-- prometheus/examples_test.go | 10 +++++ prometheus/histogram_test.go | 82 ++++++++++++++++++++++++------------ prometheus/registry_test.go | 7 ++- prometheus/wrap_test.go | 9 ++++ 5 files changed, 101 insertions(+), 33 deletions(-) diff --git a/prometheus/counter_test.go b/prometheus/counter_test.go index 6b850b6c1..ac047c495 100644 --- a/prometheus/counter_test.go +++ b/prometheus/counter_test.go @@ -26,10 +26,13 @@ import ( ) func TestCounterAdd(t *testing.T) { + now := time.Now() + nowFn := func() time.Time { return now } counter := NewCounter(CounterOpts{ Name: "test", Help: "test help", ConstLabels: Labels{"a": "1", "b": "2"}, + now: nowFn, }).(*counter) counter.Inc() if expected, got := 0.0, math.Float64frombits(counter.valBits); expected != got { @@ -66,7 +69,10 @@ func TestCounterAdd(t *testing.T) { {Name: proto.String("a"), Value: proto.String("1")}, {Name: proto.String("b"), Value: proto.String("2")}, }, - Counter: &dto.Counter{Value: proto.Float64(67.42)}, + Counter: &dto.Counter{ + Value: proto.Float64(67.42), + CreatedTimestamp: timestamppb.New(nowFn()), + }, } if !proto.Equal(expected, m) { t.Errorf("expected %q, got %q", expected, m) @@ -139,9 +145,12 @@ func expectPanic(t *testing.T, op func(), errorMsg string) { } func TestCounterAddInf(t *testing.T) { + now := time.Now() + nowFn := func() time.Time { return now } counter := NewCounter(CounterOpts{ Name: "test", Help: "test help", + now: nowFn, }).(*counter) counter.Inc() @@ -173,7 +182,8 @@ func TestCounterAddInf(t *testing.T) { expected := &dto.Metric{ Counter: &dto.Counter{ - Value: proto.Float64(math.Inf(1)), + Value: proto.Float64(math.Inf(1)), + CreatedTimestamp: timestamppb.New(nowFn()), }, } @@ -183,9 +193,12 @@ func TestCounterAddInf(t *testing.T) { } func TestCounterAddLarge(t *testing.T) { + now := time.Now() + nowFn := func() time.Time { return now } counter := NewCounter(CounterOpts{ Name: "test", Help: "test help", + now: nowFn, }).(*counter) // large overflows the underlying type and should therefore be stored in valBits. @@ -203,7 +216,8 @@ func TestCounterAddLarge(t *testing.T) { expected := &dto.Metric{ Counter: &dto.Counter{ - Value: proto.Float64(large), + Value: proto.Float64(large), + CreatedTimestamp: timestamppb.New(nowFn()), }, } @@ -213,9 +227,12 @@ func TestCounterAddLarge(t *testing.T) { } func TestCounterAddSmall(t *testing.T) { + now := time.Now() + nowFn := func() time.Time { return now } counter := NewCounter(CounterOpts{ Name: "test", Help: "test help", + now: nowFn, }).(*counter) small := 0.000000000001 counter.Add(small) @@ -231,7 +248,8 @@ func TestCounterAddSmall(t *testing.T) { expected := &dto.Metric{ Counter: &dto.Counter{ - Value: proto.Float64(small), + Value: proto.Float64(small), + CreatedTimestamp: timestamppb.New(nowFn()), }, } diff --git a/prometheus/examples_test.go b/prometheus/examples_test.go index 9d918e1fa..bca9bf62f 100644 --- a/prometheus/examples_test.go +++ b/prometheus/examples_test.go @@ -319,6 +319,8 @@ func ExampleSummary() { // internally). metric := &dto.Metric{} temps.Write(metric) + // We remove CreatedTimestamp just to make sure the assert below works. + metric.Summary.CreatedTimestamp = nil printlnNormalized(metric) @@ -355,6 +357,11 @@ func ExampleSummaryVec() { if err != nil || len(metricFamilies) != 1 { panic("unexpected behavior of custom test registry") } + // We remove CreatedTimestamp just to make sure the assert below works. + for _, m := range metricFamilies[0].Metric { + m.Summary.CreatedTimestamp = nil + } + printlnNormalized(metricFamilies[0]) // Output: @@ -405,6 +412,9 @@ func ExampleHistogram() { // internally). metric := &dto.Metric{} temps.Write(metric) + // We remove CreatedTimestamp just to make sure the assert below works. + metric.Histogram.CreatedTimestamp = nil + printlnNormalized(metric) // Output: diff --git a/prometheus/histogram_test.go b/prometheus/histogram_test.go index 379fcb029..fee75b352 100644 --- a/prometheus/histogram_test.go +++ b/prometheus/histogram_test.go @@ -469,6 +469,8 @@ func TestHistogramExemplar(t *testing.T) { } func TestNativeHistogram(t *testing.T) { + now := time.Now() + nowFn := func() time.Time { return now } scenarios := []struct { name string observations []float64 // With simulated interval of 1m. @@ -499,17 +501,19 @@ func TestNativeHistogram(t *testing.T) { {CumulativeCount: proto.Uint64(3), UpperBound: proto.Float64(5)}, {CumulativeCount: proto.Uint64(3), UpperBound: proto.Float64(10)}, }, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { name: "no observations", factor: 1.1, want: &dto.Histogram{ - SampleCount: proto.Uint64(0), - SampleSum: proto.Float64(0), - Schema: proto.Int32(3), - ZeroThreshold: proto.Float64(2.938735877055719e-39), - ZeroCount: proto.Uint64(0), + SampleCount: proto.Uint64(0), + SampleSum: proto.Float64(0), + Schema: proto.Int32(3), + ZeroThreshold: proto.Float64(2.938735877055719e-39), + ZeroCount: proto.Uint64(0), + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -525,6 +529,7 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(0), Length: proto.Uint32(0)}, }, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -542,7 +547,8 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(7), Length: proto.Uint32(1)}, {Offset: proto.Int32(4), Length: proto.Uint32(1)}, }, - PositiveDelta: []int64{1, 0, 0}, + PositiveDelta: []int64{1, 0, 0}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -558,7 +564,8 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, - PositiveDelta: []int64{1, -1, 2, -2, 2}, + PositiveDelta: []int64{1, -1, 2, -2, 2}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -581,7 +588,8 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(-2), Length: proto.Uint32(6)}, }, - PositiveDelta: []int64{2, 0, 0, 2, -1, -2}, + PositiveDelta: []int64{2, 0, 0, 2, -1, -2}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -602,7 +610,8 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(-1), Length: proto.Uint32(4)}, }, - PositiveDelta: []int64{2, 2, 3, -6}, + PositiveDelta: []int64{2, 2, 3, -6}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -618,7 +627,8 @@ func TestNativeHistogram(t *testing.T) { NegativeSpan: []*dto.BucketSpan{ {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, - NegativeDelta: []int64{1, -1, 2, -2, 2}, + NegativeDelta: []int64{1, -1, 2, -2, 2}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -638,7 +648,8 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, - PositiveDelta: []int64{1, -1, 2, -2, 2}, + PositiveDelta: []int64{1, -1, 2, -2, 2}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -659,7 +670,8 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(4), Length: proto.Uint32(1)}, }, - PositiveDelta: []int64{2}, + PositiveDelta: []int64{2}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -675,7 +687,8 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, - PositiveDelta: []int64{1, -1, 2, -2, 2}, + PositiveDelta: []int64{1, -1, 2, -2, 2}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -692,7 +705,8 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(0), Length: proto.Uint32(5)}, {Offset: proto.Int32(4092), Length: proto.Uint32(1)}, }, - PositiveDelta: []int64{1, -1, 2, -2, 2, -1}, + PositiveDelta: []int64{1, -1, 2, -2, 2, -1}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -712,7 +726,8 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, - PositiveDelta: []int64{1, -1, 2, -2, 2}, + PositiveDelta: []int64{1, -1, 2, -2, 2}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -729,7 +744,8 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, - PositiveDelta: []int64{1, -1, 2, -2, 2}, + PositiveDelta: []int64{1, -1, 2, -2, 2}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -746,7 +762,8 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, - PositiveDelta: []int64{1, 2, -1, -2, 1}, + PositiveDelta: []int64{1, 2, -1, -2, 1}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -764,7 +781,8 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(1), Length: proto.Uint32(7)}, }, - PositiveDelta: []int64{1, 1, -2, 2, -2, 0, 1}, + PositiveDelta: []int64{1, 1, -2, 2, -2, 0, 1}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -782,7 +800,8 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(2), Length: proto.Uint32(7)}, }, - PositiveDelta: []int64{2, -2, 2, -2, 0, 1, 0}, + PositiveDelta: []int64{2, -2, 2, -2, 0, 1, 0}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -801,7 +820,8 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(7), Length: proto.Uint32(2)}, }, - PositiveDelta: []int64{1, 0}, + PositiveDelta: []int64{1, 0}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -818,7 +838,8 @@ func TestNativeHistogram(t *testing.T) { NegativeSpan: []*dto.BucketSpan{ {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, - NegativeDelta: []int64{1, -1, 2, -2, 2}, + NegativeDelta: []int64{1, -1, 2, -2, 2}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -835,7 +856,8 @@ func TestNativeHistogram(t *testing.T) { NegativeSpan: []*dto.BucketSpan{ {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, - NegativeDelta: []int64{1, 2, -1, -2, 1}, + NegativeDelta: []int64{1, 2, -1, -2, 1}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -853,7 +875,8 @@ func TestNativeHistogram(t *testing.T) { NegativeSpan: []*dto.BucketSpan{ {Offset: proto.Int32(1), Length: proto.Uint32(7)}, }, - NegativeDelta: []int64{1, 1, -2, 2, -2, 0, 1}, + NegativeDelta: []int64{1, 1, -2, 2, -2, 0, 1}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -871,7 +894,8 @@ func TestNativeHistogram(t *testing.T) { NegativeSpan: []*dto.BucketSpan{ {Offset: proto.Int32(2), Length: proto.Uint32(7)}, }, - NegativeDelta: []int64{2, -2, 2, -2, 0, 1, 0}, + NegativeDelta: []int64{2, -2, 2, -2, 0, 1, 0}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -890,7 +914,8 @@ func TestNativeHistogram(t *testing.T) { NegativeSpan: []*dto.BucketSpan{ {Offset: proto.Int32(7), Length: proto.Uint32(2)}, }, - NegativeDelta: []int64{1, 0}, + NegativeDelta: []int64{1, 0}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -908,7 +933,8 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(7), Length: proto.Uint32(2)}, }, - PositiveDelta: []int64{1, 0}, + PositiveDelta: []int64{1, 0}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, { @@ -927,7 +953,8 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(7), Length: proto.Uint32(2)}, }, - PositiveDelta: []int64{1, 0}, + PositiveDelta: []int64{1, 0}, + CreatedTimestamp: timestamppb.New(nowFn()), }, }, } @@ -942,6 +969,7 @@ func TestNativeHistogram(t *testing.T) { NativeHistogramMaxBucketNumber: s.maxBuckets, NativeHistogramMinResetDuration: s.minResetDuration, NativeHistogramMaxZeroThreshold: s.maxZeroThreshold, + now: nowFn, }) ts := time.Now().Add(30 * time.Second) now := func() time.Time { diff --git a/prometheus/registry_test.go b/prometheus/registry_test.go index d1b7a192c..654778bb7 100644 --- a/prometheus/registry_test.go +++ b/prometheus/registry_test.go @@ -37,6 +37,7 @@ import ( dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/expfmt" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) // uncheckedCollector wraps a Collector but its Describe method yields no Desc. @@ -138,7 +139,8 @@ metric: < }, }, Counter: &dto.Counter{ - Value: proto.Float64(1), + Value: proto.Float64(1), + CreatedTimestamp: timestamppb.New(time.Now()), }, }, { @@ -153,7 +155,8 @@ metric: < }, }, Counter: &dto.Counter{ - Value: proto.Float64(1), + Value: proto.Float64(1), + CreatedTimestamp: timestamppb.New(time.Now()), }, }, }, diff --git a/prometheus/wrap_test.go b/prometheus/wrap_test.go index 7e5bba15a..d2b4e4c6d 100644 --- a/prometheus/wrap_test.go +++ b/prometheus/wrap_test.go @@ -17,6 +17,7 @@ import ( "fmt" "strings" "testing" + "time" dto "github.com/prometheus/client_model/go" "google.golang.org/protobuf/proto" @@ -43,9 +44,12 @@ func toMetricFamilies(cs ...Collector) []*dto.MetricFamily { } func TestWrap(t *testing.T) { + now := time.Now() + nowFn := func() time.Time { return now } simpleCnt := NewCounter(CounterOpts{ Name: "simpleCnt", Help: "helpSimpleCnt", + now: nowFn, }) simpleCnt.Inc() @@ -58,6 +62,7 @@ func TestWrap(t *testing.T) { preCnt := NewCounter(CounterOpts{ Name: "pre_simpleCnt", Help: "helpSimpleCnt", + now: nowFn, }) preCnt.Inc() @@ -65,6 +70,7 @@ func TestWrap(t *testing.T) { Name: "simpleCnt", Help: "helpSimpleCnt", ConstLabels: Labels{"foo": "bar"}, + now: nowFn, }) barLabeledCnt.Inc() @@ -72,6 +78,7 @@ func TestWrap(t *testing.T) { Name: "simpleCnt", Help: "helpSimpleCnt", ConstLabels: Labels{"foo": "baz"}, + now: nowFn, }) bazLabeledCnt.Inc() @@ -79,6 +86,7 @@ func TestWrap(t *testing.T) { Name: "pre_simpleCnt", Help: "helpSimpleCnt", ConstLabels: Labels{"foo": "bar"}, + now: nowFn, }) labeledPreCnt.Inc() @@ -86,6 +94,7 @@ func TestWrap(t *testing.T) { Name: "pre_simpleCnt", Help: "helpSimpleCnt", ConstLabels: Labels{"foo": "bar", "dings": "bums"}, + now: nowFn, }) twiceLabeledPreCnt.Inc() From 6f8bab8950ccd5dade1e61f712a14e451da0d31f Mon Sep 17 00:00:00 2001 From: bwplotka Date: Tue, 19 Sep 2023 18:43:25 +0100 Subject: [PATCH 3/4] Last touches (readability and consistency) Changes: * Comments for "now" are more explicit and not inlined. * populateMetrics is simpler and bit more efficient without timestamp to time to timestamp conversionts for more common code. * Test consistency and simplicity - the fewer variables the better. * Fixed inconsistency for v2 and MetricVec - let's pass opt.now consistently. * We don't need TestCounterXXXTimestamp - we test CT in many other places already. * Added more involved test for counter vectors with created timestamp. * Refactored normalization for simplicity. * Make histogram, summaries now consistent. * Simplified histograms CT flow and implemented proper CT on reset. TODO for next PRs: * NewConstSummary and NewConstHistogram - ability to specify CTs there. Signed-off-by: bwplotka --- prometheus/counter.go | 9 +-- prometheus/counter_test.go | 107 ++++++++++++++++--------- prometheus/example_metricvec_test.go | 4 +- prometheus/examples_test.go | 44 ++++++----- prometheus/expvar_collector_test.go | 2 +- prometheus/histogram.go | 24 +++--- prometheus/histogram_test.go | 114 ++++++++++++++++++--------- prometheus/metric.go | 3 +- prometheus/summary.go | 5 +- prometheus/summary_test.go | 101 ++++++++++-------------- prometheus/utils_test.go | 37 +++++++-- prometheus/value.go | 10 +-- prometheus/value_test.go | 26 +++--- 13 files changed, 281 insertions(+), 205 deletions(-) diff --git a/prometheus/counter.go b/prometheus/counter.go index a3139ffa3..5f72deba3 100644 --- a/prometheus/counter.go +++ b/prometheus/counter.go @@ -115,7 +115,8 @@ type counter struct { labelPairs []*dto.LabelPair exemplar atomic.Value // Containing nil or a *dto.Exemplar. - now func() time.Time // For testing, all constructors put time.Now() here. + // now is for testing purposes, by default it's time.Now. + now func() time.Time } func (c *counter) Desc() *Desc { @@ -165,9 +166,7 @@ func (c *counter) Write(out *dto.Metric) error { exemplar = e.(*dto.Exemplar) } val := c.get() - - ct := c.createdTs.AsTime() - return populateMetric(CounterValue, val, c.labelPairs, exemplar, out, &ct) + return populateMetric(CounterValue, val, c.labelPairs, exemplar, out, c.createdTs) } func (c *counter) updateExemplar(v float64, l Labels) { @@ -215,7 +214,7 @@ func (v2) NewCounterVec(opts CounterVecOpts) *CounterVec { if len(lvs) != len(desc.variableLabels.names) { panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.names, lvs)) } - result := &counter{desc: desc, labelPairs: MakeLabelPairs(desc, lvs), now: time.Now} + result := &counter{desc: desc, labelPairs: MakeLabelPairs(desc, lvs), now: opts.now} result.init(result) // Init self-collection. result.createdTs = timestamppb.New(opts.now()) return result diff --git a/prometheus/counter_test.go b/prometheus/counter_test.go index ac047c495..67c273b65 100644 --- a/prometheus/counter_test.go +++ b/prometheus/counter_test.go @@ -27,12 +27,12 @@ import ( func TestCounterAdd(t *testing.T) { now := time.Now() - nowFn := func() time.Time { return now } + counter := NewCounter(CounterOpts{ Name: "test", Help: "test help", ConstLabels: Labels{"a": "1", "b": "2"}, - now: nowFn, + now: func() time.Time { return now }, }).(*counter) counter.Inc() if expected, got := 0.0, math.Float64frombits(counter.valBits); expected != got { @@ -71,7 +71,7 @@ func TestCounterAdd(t *testing.T) { }, Counter: &dto.Counter{ Value: proto.Float64(67.42), - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, } if !proto.Equal(expected, m) { @@ -146,11 +146,11 @@ func expectPanic(t *testing.T, op func(), errorMsg string) { func TestCounterAddInf(t *testing.T) { now := time.Now() - nowFn := func() time.Time { return now } + counter := NewCounter(CounterOpts{ Name: "test", Help: "test help", - now: nowFn, + now: func() time.Time { return now }, }).(*counter) counter.Inc() @@ -183,7 +183,7 @@ func TestCounterAddInf(t *testing.T) { expected := &dto.Metric{ Counter: &dto.Counter{ Value: proto.Float64(math.Inf(1)), - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, } @@ -194,11 +194,11 @@ func TestCounterAddInf(t *testing.T) { func TestCounterAddLarge(t *testing.T) { now := time.Now() - nowFn := func() time.Time { return now } + counter := NewCounter(CounterOpts{ Name: "test", Help: "test help", - now: nowFn, + now: func() time.Time { return now }, }).(*counter) // large overflows the underlying type and should therefore be stored in valBits. @@ -217,7 +217,7 @@ func TestCounterAddLarge(t *testing.T) { expected := &dto.Metric{ Counter: &dto.Counter{ Value: proto.Float64(large), - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, } @@ -228,12 +228,13 @@ func TestCounterAddLarge(t *testing.T) { func TestCounterAddSmall(t *testing.T) { now := time.Now() - nowFn := func() time.Time { return now } + counter := NewCounter(CounterOpts{ Name: "test", Help: "test help", - now: nowFn, + now: func() time.Time { return now }, }).(*counter) + small := 0.000000000001 counter.Add(small) if expected, got := small, math.Float64frombits(counter.valBits); expected != got { @@ -249,7 +250,7 @@ func TestCounterAddSmall(t *testing.T) { expected := &dto.Metric{ Counter: &dto.Counter{ Value: proto.Float64(small), - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, } @@ -264,8 +265,8 @@ func TestCounterExemplar(t *testing.T) { counter := NewCounter(CounterOpts{ Name: "test", Help: "test help", + now: func() time.Time { return now }, }).(*counter) - counter.now = func() time.Time { return now } ts := timestamppb.New(now) if err := ts.CheckValid(); err != nil { @@ -317,39 +318,71 @@ func TestCounterExemplar(t *testing.T) { } } -func TestCounterCreatedTimestamp(t *testing.T) { +func TestCounterVecCreatedTimestampWithDeletes(t *testing.T) { now := time.Now() - counter := NewCounter(CounterOpts{ + + counterVec := NewCounterVec(CounterOpts{ Name: "test", Help: "test help", now: func() time.Time { return now }, - }) + }, []string{"label"}) - var metric dto.Metric - if err := counter.Write(&metric); err != nil { - t.Fatal(err) - } + // First use of "With" should populate CT. + counterVec.WithLabelValues("1") + expected := map[string]time.Time{"1": now} - if metric.Counter.CreatedTimestamp.AsTime().Unix() != now.Unix() { - t.Errorf("expected created timestamp %d, got %d", now.Unix(), metric.Counter.CreatedTimestamp.AsTime().Unix()) - } + now = now.Add(1 * time.Hour) + expectCTsForMetricVecValues(t, counterVec.MetricVec, dto.MetricType_COUNTER, expected) + + // Two more labels at different times. + counterVec.WithLabelValues("2") + expected["2"] = now + + now = now.Add(1 * time.Hour) + + counterVec.WithLabelValues("3") + expected["3"] = now + + now = now.Add(1 * time.Hour) + expectCTsForMetricVecValues(t, counterVec.MetricVec, dto.MetricType_COUNTER, expected) + + // Recreate metric instance should reset created timestamp to now. + counterVec.DeleteLabelValues("1") + counterVec.WithLabelValues("1") + expected["1"] = now + + now = now.Add(1 * time.Hour) + expectCTsForMetricVecValues(t, counterVec.MetricVec, dto.MetricType_COUNTER, expected) } -func TestCounterVecCreatedTimestamp(t *testing.T) { - now := time.Now() - counterVec := NewCounterVec(CounterOpts{ - Name: "test", - Help: "test help", - now: func() time.Time { return now }, - }, []string{"label"}) - counter := counterVec.WithLabelValues("value") +func expectCTsForMetricVecValues(t testing.TB, vec *MetricVec, typ dto.MetricType, ctsPerLabelValue map[string]time.Time) { + t.Helper() - var metric dto.Metric - if err := counter.Write(&metric); err != nil { - t.Fatal(err) - } + for val, ct := range ctsPerLabelValue { + var metric dto.Metric + m, err := vec.GetMetricWithLabelValues(val) + if err != nil { + t.Fatal(err) + } - if metric.Counter.CreatedTimestamp.AsTime().Unix() != now.Unix() { - t.Errorf("expected created timestamp %d, got %d", now.Unix(), metric.Counter.CreatedTimestamp.AsTime().Unix()) + if err := m.Write(&metric); err != nil { + t.Fatal(err) + } + + var gotTs time.Time + switch typ { + case dto.MetricType_COUNTER: + gotTs = metric.Counter.CreatedTimestamp.AsTime() + case dto.MetricType_HISTOGRAM: + gotTs = metric.Histogram.CreatedTimestamp.AsTime() + case dto.MetricType_SUMMARY: + gotTs = metric.Summary.CreatedTimestamp.AsTime() + default: + t.Fatalf("unknown metric type %v", typ) + } + + if !gotTs.Equal(ct) { + t.Errorf("expected created timestamp for counter with label value %q: %s, got %s", val, ct, gotTs) + } } } diff --git a/prometheus/example_metricvec_test.go b/prometheus/example_metricvec_test.go index a9f29d48f..59e43f8f8 100644 --- a/prometheus/example_metricvec_test.go +++ b/prometheus/example_metricvec_test.go @@ -14,6 +14,8 @@ package prometheus_test import ( + "fmt" + "google.golang.org/protobuf/proto" dto "github.com/prometheus/client_model/go" @@ -124,7 +126,7 @@ func ExampleMetricVec() { if err != nil || len(metricFamilies) != 1 { panic("unexpected behavior of custom test registry") } - printlnNormalized(metricFamilies[0]) + fmt.Println(toNormalizedJSON(metricFamilies[0])) // Output: // {"name":"library_version_info","help":"Versions of the libraries used in this binary.","type":"GAUGE","metric":[{"label":[{"name":"library","value":"k8s.io/client-go"},{"name":"version","value":"0.18.8"}],"gauge":{"value":1}},{"label":[{"name":"library","value":"prometheus/client_golang"},{"name":"version","value":"1.7.1"}],"gauge":{"value":1}}]} diff --git a/prometheus/examples_test.go b/prometheus/examples_test.go index bca9bf62f..89232bc80 100644 --- a/prometheus/examples_test.go +++ b/prometheus/examples_test.go @@ -153,6 +153,22 @@ func ExampleCounterVec() { httpReqs.DeleteLabelValues("200", "GET") // Same thing with the more verbose Labels syntax. httpReqs.Delete(prometheus.Labels{"method": "GET", "code": "200"}) + + // Just for demonstration, let's check the state of the counter vector + // by registering it with a custom registry and then let it collect the + // metrics. + reg := prometheus.NewRegistry() + reg.MustRegister(httpReqs) + + metricFamilies, err := reg.Gather() + if err != nil || len(metricFamilies) != 1 { + panic("unexpected behavior of custom test registry") + } + + fmt.Println(toNormalizedJSON(sanitizeMetricFamily(metricFamilies[0]))) + + // Output: + // {"name":"http_requests_total","help":"How many HTTP requests processed, partitioned by status code and HTTP method.","type":"COUNTER","metric":[{"label":[{"name":"code","value":"404"},{"name":"method","value":"POST"}],"counter":{"value":42,"createdTimestamp":"1970-01-01T00:00:10Z"}}]} } func ExampleRegister() { @@ -319,13 +335,11 @@ func ExampleSummary() { // internally). metric := &dto.Metric{} temps.Write(metric) - // We remove CreatedTimestamp just to make sure the assert below works. - metric.Summary.CreatedTimestamp = nil - printlnNormalized(metric) + fmt.Println(toNormalizedJSON(sanitizeMetric(metric))) // Output: - // {"summary":{"sampleCount":"1000","sampleSum":29969.50000000001,"quantile":[{"quantile":0.5,"value":31.1},{"quantile":0.9,"value":41.3},{"quantile":0.99,"value":41.9}]}} + // {"summary":{"sampleCount":"1000","sampleSum":29969.50000000001,"quantile":[{"quantile":0.5,"value":31.1},{"quantile":0.9,"value":41.3},{"quantile":0.99,"value":41.9}],"createdTimestamp":"1970-01-01T00:00:10Z"}} } func ExampleSummaryVec() { @@ -357,15 +371,11 @@ func ExampleSummaryVec() { if err != nil || len(metricFamilies) != 1 { panic("unexpected behavior of custom test registry") } - // We remove CreatedTimestamp just to make sure the assert below works. - for _, m := range metricFamilies[0].Metric { - m.Summary.CreatedTimestamp = nil - } - printlnNormalized(metricFamilies[0]) + fmt.Println(toNormalizedJSON(sanitizeMetricFamily(metricFamilies[0]))) // Output: - // {"name":"pond_temperature_celsius","help":"The temperature of the frog pond.","type":"SUMMARY","metric":[{"label":[{"name":"species","value":"leiopelma-hochstetteri"}],"summary":{"sampleCount":"0","sampleSum":0,"quantile":[{"quantile":0.5,"value":"NaN"},{"quantile":0.9,"value":"NaN"},{"quantile":0.99,"value":"NaN"}]}},{"label":[{"name":"species","value":"lithobates-catesbeianus"}],"summary":{"sampleCount":"1000","sampleSum":31956.100000000017,"quantile":[{"quantile":0.5,"value":32.4},{"quantile":0.9,"value":41.4},{"quantile":0.99,"value":41.9}]}},{"label":[{"name":"species","value":"litoria-caerulea"}],"summary":{"sampleCount":"1000","sampleSum":29969.50000000001,"quantile":[{"quantile":0.5,"value":31.1},{"quantile":0.9,"value":41.3},{"quantile":0.99,"value":41.9}]}}]} + // {"name":"pond_temperature_celsius","help":"The temperature of the frog pond.","type":"SUMMARY","metric":[{"label":[{"name":"species","value":"leiopelma-hochstetteri"}],"summary":{"sampleCount":"0","sampleSum":0,"quantile":[{"quantile":0.5,"value":"NaN"},{"quantile":0.9,"value":"NaN"},{"quantile":0.99,"value":"NaN"}],"createdTimestamp":"1970-01-01T00:00:10Z"}},{"label":[{"name":"species","value":"lithobates-catesbeianus"}],"summary":{"sampleCount":"1000","sampleSum":31956.100000000017,"quantile":[{"quantile":0.5,"value":32.4},{"quantile":0.9,"value":41.4},{"quantile":0.99,"value":41.9}],"createdTimestamp":"1970-01-01T00:00:10Z"}},{"label":[{"name":"species","value":"litoria-caerulea"}],"summary":{"sampleCount":"1000","sampleSum":29969.50000000001,"quantile":[{"quantile":0.5,"value":31.1},{"quantile":0.9,"value":41.3},{"quantile":0.99,"value":41.9}],"createdTimestamp":"1970-01-01T00:00:10Z"}}]} } func ExampleNewConstSummary() { @@ -389,7 +399,7 @@ func ExampleNewConstSummary() { // internally). metric := &dto.Metric{} s.Write(metric) - printlnNormalized(metric) + fmt.Println(toNormalizedJSON(metric)) // Output: // {"label":[{"name":"code","value":"200"},{"name":"method","value":"get"},{"name":"owner","value":"example"}],"summary":{"sampleCount":"4711","sampleSum":403.34,"quantile":[{"quantile":0.5,"value":42.3},{"quantile":0.9,"value":323.3}]}} @@ -412,13 +422,11 @@ func ExampleHistogram() { // internally). metric := &dto.Metric{} temps.Write(metric) - // We remove CreatedTimestamp just to make sure the assert below works. - metric.Histogram.CreatedTimestamp = nil - printlnNormalized(metric) + fmt.Println(toNormalizedJSON(sanitizeMetric(metric))) // Output: - // {"histogram":{"sampleCount":"1000","sampleSum":29969.50000000001,"bucket":[{"cumulativeCount":"192","upperBound":20},{"cumulativeCount":"366","upperBound":25},{"cumulativeCount":"501","upperBound":30},{"cumulativeCount":"638","upperBound":35},{"cumulativeCount":"816","upperBound":40}]}} + // {"histogram":{"sampleCount":"1000","sampleSum":29969.50000000001,"bucket":[{"cumulativeCount":"192","upperBound":20},{"cumulativeCount":"366","upperBound":25},{"cumulativeCount":"501","upperBound":30},{"cumulativeCount":"638","upperBound":35},{"cumulativeCount":"816","upperBound":40}],"createdTimestamp":"1970-01-01T00:00:10Z"}} } func ExampleNewConstHistogram() { @@ -442,7 +450,7 @@ func ExampleNewConstHistogram() { // internally). metric := &dto.Metric{} h.Write(metric) - printlnNormalized(metric) + fmt.Println(toNormalizedJSON(metric)) // Output: // {"label":[{"name":"code","value":"200"},{"name":"method","value":"get"},{"name":"owner","value":"example"}],"histogram":{"sampleCount":"4711","sampleSum":403.34,"bucket":[{"cumulativeCount":"121","upperBound":25},{"cumulativeCount":"2403","upperBound":50},{"cumulativeCount":"3221","upperBound":100},{"cumulativeCount":"4233","upperBound":200}]}} @@ -480,7 +488,7 @@ func ExampleNewConstHistogram_WithExemplar() { // internally). metric := &dto.Metric{} h.Write(metric) - printlnNormalized(metric) + fmt.Println(toNormalizedJSON(metric)) // Output: // {"label":[{"name":"code","value":"200"},{"name":"method","value":"get"},{"name":"owner","value":"example"}],"histogram":{"sampleCount":"4711","sampleSum":403.34,"bucket":[{"cumulativeCount":"121","upperBound":25,"exemplar":{"label":[{"name":"testName","value":"testVal"}],"value":24,"timestamp":"2006-01-02T15:04:05Z"}},{"cumulativeCount":"2403","upperBound":50,"exemplar":{"label":[{"name":"testName","value":"testVal"}],"value":42,"timestamp":"2006-01-02T15:04:05Z"}},{"cumulativeCount":"3221","upperBound":100,"exemplar":{"label":[{"name":"testName","value":"testVal"}],"value":89,"timestamp":"2006-01-02T15:04:05Z"}},{"cumulativeCount":"4233","upperBound":200,"exemplar":{"label":[{"name":"testName","value":"testVal"}],"value":157,"timestamp":"2006-01-02T15:04:05Z"}}]}} @@ -642,7 +650,7 @@ func ExampleNewMetricWithTimestamp() { // internally). metric := &dto.Metric{} s.Write(metric) - printlnNormalized(metric) + fmt.Println(toNormalizedJSON(metric)) // Output: // {"gauge":{"value":298.15},"timestampMs":"1257894000012"} diff --git a/prometheus/expvar_collector_test.go b/prometheus/expvar_collector_test.go index 9b1202e1e..a8d0ed294 100644 --- a/prometheus/expvar_collector_test.go +++ b/prometheus/expvar_collector_test.go @@ -81,7 +81,7 @@ func ExampleNewExpvarCollector() { if !strings.Contains(m.Desc().String(), "expvar_memstats") { metric.Reset() m.Write(&metric) - metricStrings = append(metricStrings, protoToNormalizedJSON(&metric)) + metricStrings = append(metricStrings, toNormalizedJSON(&metric)) } } sort.Strings(metricStrings) diff --git a/prometheus/histogram.go b/prometheus/histogram.go index 94c79624f..ab758db28 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -473,7 +473,8 @@ type HistogramOpts struct { NativeHistogramMinResetDuration time.Duration NativeHistogramMaxZeroThreshold float64 - now func() time.Time // For testing, all constructors put time.Now() here. + // now is for testing purposes, by default it's time.Now. + now func() time.Time } // HistogramVecOpts bundles the options to create a HistogramVec metric. @@ -522,6 +523,10 @@ func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogr } } + if opts.now == nil { + opts.now = time.Now + } + h := &histogram{ desc: desc, upperBounds: opts.Buckets, @@ -529,8 +534,8 @@ func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogr nativeHistogramMaxBuckets: opts.NativeHistogramMaxBucketNumber, nativeHistogramMaxZeroThreshold: opts.NativeHistogramMaxZeroThreshold, nativeHistogramMinResetDuration: opts.NativeHistogramMinResetDuration, - lastResetTime: time.Now(), - now: time.Now, + lastResetTime: opts.now(), + now: opts.now, } if len(h.upperBounds) == 0 && opts.NativeHistogramBucketFactor <= 1 { h.upperBounds = DefBuckets @@ -571,11 +576,7 @@ func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogr atomic.StoreInt32(&h.counts[1].nativeHistogramSchema, h.nativeHistogramSchema) h.exemplars = make([]atomic.Value, len(h.upperBounds)+1) - if opts.now == nil { - opts.now = time.Now - } h.init(h) // Init self-collection. - h.createdTs = timestamppb.New(opts.now()) return h } @@ -713,10 +714,11 @@ type histogram struct { nativeHistogramMaxZeroThreshold float64 nativeHistogramMaxBuckets uint32 nativeHistogramMinResetDuration time.Duration - lastResetTime time.Time // Protected by mtx. - createdTs *timestamppb.Timestamp + // lastResetTime is protected by mtx. It is also used as created timestamp. + lastResetTime time.Time - now func() time.Time // For testing, all constructors put time.Now() here. + // now is for testing purposes, by default it's time.Now. + now func() time.Time } func (h *histogram) Desc() *Desc { @@ -758,7 +760,7 @@ func (h *histogram) Write(out *dto.Metric) error { Bucket: make([]*dto.Bucket, len(h.upperBounds)), SampleCount: proto.Uint64(count), SampleSum: proto.Float64(math.Float64frombits(atomic.LoadUint64(&coldCounts.sumBits))), - CreatedTimestamp: h.createdTs, + CreatedTimestamp: timestamppb.New(h.lastResetTime), } out.Histogram = his out.Label = h.labelPairs diff --git a/prometheus/histogram_test.go b/prometheus/histogram_test.go index fee75b352..60b43fd61 100644 --- a/prometheus/histogram_test.go +++ b/prometheus/histogram_test.go @@ -416,8 +416,8 @@ func TestHistogramExemplar(t *testing.T) { Name: "test", Help: "test help", Buckets: []float64{1, 2, 3, 4}, + now: func() time.Time { return now }, }).(*histogram) - histogram.now = func() time.Time { return now } ts := timestamppb.New(now) if err := ts.CheckValid(); err != nil { @@ -470,7 +470,7 @@ func TestHistogramExemplar(t *testing.T) { func TestNativeHistogram(t *testing.T) { now := time.Now() - nowFn := func() time.Time { return now } + scenarios := []struct { name string observations []float64 // With simulated interval of 1m. @@ -501,7 +501,7 @@ func TestNativeHistogram(t *testing.T) { {CumulativeCount: proto.Uint64(3), UpperBound: proto.Float64(5)}, {CumulativeCount: proto.Uint64(3), UpperBound: proto.Float64(10)}, }, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -513,7 +513,7 @@ func TestNativeHistogram(t *testing.T) { Schema: proto.Int32(3), ZeroThreshold: proto.Float64(2.938735877055719e-39), ZeroCount: proto.Uint64(0), - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -529,7 +529,7 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(0), Length: proto.Uint32(0)}, }, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -548,7 +548,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(4), Length: proto.Uint32(1)}, }, PositiveDelta: []int64{1, 0, 0}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -565,7 +565,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, PositiveDelta: []int64{1, -1, 2, -2, 2}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -589,7 +589,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(-2), Length: proto.Uint32(6)}, }, PositiveDelta: []int64{2, 0, 0, 2, -1, -2}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -611,7 +611,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(-1), Length: proto.Uint32(4)}, }, PositiveDelta: []int64{2, 2, 3, -6}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -628,7 +628,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, NegativeDelta: []int64{1, -1, 2, -2, 2}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -649,7 +649,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, PositiveDelta: []int64{1, -1, 2, -2, 2}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -671,7 +671,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(4), Length: proto.Uint32(1)}, }, PositiveDelta: []int64{2}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -688,7 +688,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, PositiveDelta: []int64{1, -1, 2, -2, 2}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -706,7 +706,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(4092), Length: proto.Uint32(1)}, }, PositiveDelta: []int64{1, -1, 2, -2, 2, -1}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -727,7 +727,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, PositiveDelta: []int64{1, -1, 2, -2, 2}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -745,7 +745,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, PositiveDelta: []int64{1, -1, 2, -2, 2}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -763,7 +763,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, PositiveDelta: []int64{1, 2, -1, -2, 1}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -782,7 +782,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(1), Length: proto.Uint32(7)}, }, PositiveDelta: []int64{1, 1, -2, 2, -2, 0, 1}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -801,7 +801,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(2), Length: proto.Uint32(7)}, }, PositiveDelta: []int64{2, -2, 2, -2, 0, 1, 0}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -821,7 +821,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(7), Length: proto.Uint32(2)}, }, PositiveDelta: []int64{1, 0}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now.Add(8 * time.Minute)), // We expect reset to happen after 8 observations. }, }, { @@ -839,7 +839,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, NegativeDelta: []int64{1, -1, 2, -2, 2}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -857,7 +857,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(0), Length: proto.Uint32(5)}, }, NegativeDelta: []int64{1, 2, -1, -2, 1}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -876,7 +876,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(1), Length: proto.Uint32(7)}, }, NegativeDelta: []int64{1, 1, -2, 2, -2, 0, 1}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -895,7 +895,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(2), Length: proto.Uint32(7)}, }, NegativeDelta: []int64{2, -2, 2, -2, 0, 1, 0}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -915,7 +915,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(7), Length: proto.Uint32(2)}, }, NegativeDelta: []int64{1, 0}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now.Add(8 * time.Minute)), // We expect reset to happen after 8 observations. }, }, { @@ -934,7 +934,7 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(7), Length: proto.Uint32(2)}, }, PositiveDelta: []int64{1, 0}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now.Add(10 * time.Minute)), // We expect reset to happen after 9 minutes. }, }, { @@ -954,13 +954,15 @@ func TestNativeHistogram(t *testing.T) { {Offset: proto.Int32(7), Length: proto.Uint32(2)}, }, PositiveDelta: []int64{1, 0}, - CreatedTimestamp: timestamppb.New(nowFn()), + CreatedTimestamp: timestamppb.New(now.Add(10 * time.Minute)), // We expect reset to happen after 9 minutes. }, }, } for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { + ts := now + his := NewHistogram(HistogramOpts{ Name: "name", Help: "help", @@ -969,13 +971,10 @@ func TestNativeHistogram(t *testing.T) { NativeHistogramMaxBucketNumber: s.maxBuckets, NativeHistogramMinResetDuration: s.minResetDuration, NativeHistogramMaxZeroThreshold: s.maxZeroThreshold, - now: nowFn, + now: func() time.Time { return ts }, }) - ts := time.Now().Add(30 * time.Second) - now := func() time.Time { - return ts - } - his.(*histogram).now = now + + ts = ts.Add(time.Minute) for _, o := range s.observations { his.Observe(o) ts = ts.Add(time.Minute) @@ -1000,6 +999,8 @@ func TestNativeHistogramConcurrency(t *testing.T) { rand.Seed(42) it := func(n uint32) bool { + ts := time.Now().Add(30 * time.Second).Unix() + mutations := int(n%1e4 + 1e4) concLevel := int(n%5 + 1) total := mutations * concLevel @@ -1016,14 +1017,11 @@ func TestNativeHistogramConcurrency(t *testing.T) { NativeHistogramMaxBucketNumber: 50, NativeHistogramMinResetDuration: time.Hour, // Comment out to test for totals below. NativeHistogramMaxZeroThreshold: 0.001, + now: func() time.Time { + return time.Unix(atomic.LoadInt64(&ts), 0) + }, }) - ts := time.Now().Add(30 * time.Second).Unix() - now := func() time.Time { - return time.Unix(atomic.LoadInt64(&ts), 0) - } - his.(*histogram).now = now - allVars := make([]float64, total) var sampleSum float64 for i := 0; i < concLevel; i++ { @@ -1221,3 +1219,41 @@ func TestHistogramVecCreatedTimestamp(t *testing.T) { t.Errorf("expected created timestamp %d, got %d", now.Unix(), metric.Histogram.CreatedTimestamp.AsTime().Unix()) } } + +func TestHistogramVecCreatedTimestampWithDeletes(t *testing.T) { + now := time.Now() + + histogramVec := NewHistogramVec(HistogramOpts{ + Name: "test", + Help: "test help", + Buckets: []float64{1, 2, 3, 4}, + now: func() time.Time { return now }, + }, []string{"label"}) + + // First use of "With" should populate CT. + histogramVec.WithLabelValues("1") + expected := map[string]time.Time{"1": now} + + now = now.Add(1 * time.Hour) + expectCTsForMetricVecValues(t, histogramVec.MetricVec, dto.MetricType_HISTOGRAM, expected) + + // Two more labels at different times. + histogramVec.WithLabelValues("2") + expected["2"] = now + + now = now.Add(1 * time.Hour) + + histogramVec.WithLabelValues("3") + expected["3"] = now + + now = now.Add(1 * time.Hour) + expectCTsForMetricVecValues(t, histogramVec.MetricVec, dto.MetricType_HISTOGRAM, expected) + + // Recreate metric instance should reset created timestamp to now. + histogramVec.DeleteLabelValues("1") + histogramVec.WithLabelValues("1") + expected["1"] = now + + now = now.Add(1 * time.Hour) + expectCTsForMetricVecValues(t, histogramVec.MetricVec, dto.MetricType_HISTOGRAM, expected) +} diff --git a/prometheus/metric.go b/prometheus/metric.go index aad02eead..f018e5723 100644 --- a/prometheus/metric.go +++ b/prometheus/metric.go @@ -93,7 +93,8 @@ type Opts struct { // https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels-not-static-scraped-labels ConstLabels Labels - now func() time.Time // For testing, all constructors put time.Now() here. + // now is for testing purposes, by default it's time.Now. + now func() time.Time } // BuildFQName joins the given three name components by "_". Empty name diff --git a/prometheus/summary.go b/prometheus/summary.go index 2b7e9d19d..e2c2fbde0 100644 --- a/prometheus/summary.go +++ b/prometheus/summary.go @@ -147,7 +147,8 @@ type SummaryOpts struct { // "github.com/bmizerany/perks/quantile"). BufCap uint32 - now func() time.Time // For testing, all constructors put time.Now() here. + // now is for testing purposes, by default it's time.Now. + now func() time.Time } // SummaryVecOpts bundles the options to create a SummaryVec metric. @@ -252,7 +253,7 @@ func newSummary(desc *Desc, opts SummaryOpts, labelValues ...string) Summary { coldBuf: make([]float64, 0, opts.BufCap), streamDuration: opts.MaxAge / time.Duration(opts.AgeBuckets), } - s.headStreamExpTime = time.Now().Add(s.streamDuration) + s.headStreamExpTime = opts.now().Add(s.streamDuration) s.hotBufExpTime = s.headStreamExpTime for i := uint32(0); i < opts.AgeBuckets; i++ { diff --git a/prometheus/summary_test.go b/prometheus/summary_test.go index 1213bb11a..2a0d1f3ce 100644 --- a/prometheus/summary_test.go +++ b/prometheus/summary_test.go @@ -26,10 +26,13 @@ import ( ) func TestSummaryWithDefaultObjectives(t *testing.T) { + now := time.Now() + reg := NewRegistry() summaryWithDefaultObjectives := NewSummary(SummaryOpts{ Name: "default_objectives", Help: "Test help.", + now: func() time.Time { return now }, }) if err := reg.Register(summaryWithDefaultObjectives); err != nil { t.Error(err) @@ -42,6 +45,10 @@ func TestSummaryWithDefaultObjectives(t *testing.T) { if len(m.GetSummary().Quantile) != 0 { t.Error("expected no objectives in summary") } + + if !m.Summary.CreatedTimestamp.AsTime().Equal(now) { + t.Errorf("expected created timestamp %s, got %s", now, m.Summary.CreatedTimestamp.AsTime()) + } } func TestSummaryWithoutObjectives(t *testing.T) { @@ -421,77 +428,49 @@ func getBounds(vars []float64, q, ε float64) (min, max float64) { return } -func TestSummaryCreatedTimestamp(t *testing.T) { - testCases := []struct { +func TestSummaryVecCreatedTimestampWithDeletes(t *testing.T) { + for _, tcase := range []struct { desc string objectives map[float64]float64 }{ - { - desc: "summary with objectives", - objectives: map[float64]float64{ - 1.0: 1.0, - }, - }, - { - desc: "no objectives summary", - objectives: nil, - }, - } - now := time.Now() - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - summary := NewSummary(SummaryOpts{ + {desc: "summary with objectives", objectives: map[float64]float64{1.0: 1.0}}, + {desc: "no objectives summary", objectives: nil}, + } { + now := time.Now() + t.Run(tcase.desc, func(t *testing.T) { + summaryVec := NewSummaryVec(SummaryOpts{ Name: "test", Help: "test help", - Objectives: test.objectives, + Objectives: tcase.objectives, now: func() time.Time { return now }, - }) + }, []string{"label"}) - var metric dto.Metric - summary.Write(&metric) + // First use of "With" should populate CT. + summaryVec.WithLabelValues("1") + expected := map[string]time.Time{"1": now} - if metric.Summary.CreatedTimestamp.AsTime().Unix() != now.Unix() { - t.Errorf("expected created timestamp %d, got %d", now.Unix(), metric.Summary.CreatedTimestamp.AsTime().Unix()) - } - }) - } -} + now = now.Add(1 * time.Hour) + expectCTsForMetricVecValues(t, summaryVec.MetricVec, dto.MetricType_SUMMARY, expected) -func TestSummaryVecCreatedTimestamp(t *testing.T) { - testCases := []struct { - desc string - objectives map[float64]float64 - }{ - { - desc: "summary with objectives", - objectives: map[float64]float64{ - 1.0: 1.0, - }, - }, - { - desc: "no objectives summary", - objectives: nil, - }, - } - now := time.Now() - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - summaryVec := NewSummaryVec(SummaryOpts{ - Name: "test", - Help: "test help", - Objectives: test.objectives, - now: func() time.Time { return now }, - }, - []string{"label"}) - summary := summaryVec.WithLabelValues("value").(Summary) - var metric dto.Metric - summary.Write(&metric) + // Two more labels at different times. + summaryVec.WithLabelValues("2") + expected["2"] = now - if metric.Summary.CreatedTimestamp.AsTime().Unix() != now.Unix() { - t.Errorf("expected created timestamp %d, got %d", now.Unix(), metric.Summary.CreatedTimestamp.AsTime().Unix()) - } + now = now.Add(1 * time.Hour) + + summaryVec.WithLabelValues("3") + expected["3"] = now + + now = now.Add(1 * time.Hour) + expectCTsForMetricVecValues(t, summaryVec.MetricVec, dto.MetricType_SUMMARY, expected) + + // Recreate metric instance should reset created timestamp to now. + summaryVec.DeleteLabelValues("1") + summaryVec.WithLabelValues("1") + expected["1"] = now + + now = now.Add(1 * time.Hour) + expectCTsForMetricVecValues(t, summaryVec.MetricVec, dto.MetricType_SUMMARY, expected) }) } } diff --git a/prometheus/utils_test.go b/prometheus/utils_test.go index a8ab001cd..81d0820ed 100644 --- a/prometheus/utils_test.go +++ b/prometheus/utils_test.go @@ -15,21 +15,42 @@ package prometheus_test import ( "bytes" "encoding/json" - "fmt" + "time" + dto "github.com/prometheus/client_model/go" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) -// printlnNormalized is a helper function to compare proto messages in json format. -// Without removing brittle, we can't assert that two proto messages in json/text format are equal. -// Read more in https://github.com/golang/protobuf/issues/1121 -func printlnNormalized(m proto.Message) { - fmt.Println(protoToNormalizedJSON(m)) +// sanitizeMetric injects expected fake created timestamp value "1970-01-01T00:00:10Z", +// so we can compare it in examples. It modifies metric in-place, the returned pointer +// is for convenience. +func sanitizeMetric(metric *dto.Metric) *dto.Metric { + if metric.Counter != nil && metric.Counter.CreatedTimestamp != nil { + metric.Counter.CreatedTimestamp = timestamppb.New(time.Unix(10, 0)) + } + if metric.Summary != nil && metric.Summary.CreatedTimestamp != nil { + metric.Summary.CreatedTimestamp = timestamppb.New(time.Unix(10, 0)) + } + if metric.Histogram != nil && metric.Histogram.CreatedTimestamp != nil { + metric.Histogram.CreatedTimestamp = timestamppb.New(time.Unix(10, 0)) + } + return metric } -// protoToNormalizedJSON works as printlnNormalized, but returns the string instead of printing. -func protoToNormalizedJSON(m proto.Message) string { +// sanitizeMetricFamily is like sanitizeMetric, but for multiple metrics. +func sanitizeMetricFamily(f *dto.MetricFamily) *dto.MetricFamily { + for _, m := range f.Metric { + sanitizeMetric(m) + } + return f +} + +// toNormalizedJSON removes fake random space from proto JSON original marshaller. +// It is required, so we can compare proto messages in json format. +// Read more in https://github.com/golang/protobuf/issues/1121 +func toNormalizedJSON(m proto.Message) string { mAsJSON, err := protojson.Marshal(m) if err != nil { panic(err) diff --git a/prometheus/value.go b/prometheus/value.go index e1a5f57ca..cc23011fa 100644 --- a/prometheus/value.go +++ b/prometheus/value.go @@ -144,11 +144,11 @@ func NewConstMetricWithCreatedTimestamp(desc *Desc, valueType ValueType, value f case CounterValue: break default: - return nil, errors.New("Created timestamps are only supported for counters") + return nil, errors.New("created timestamps are only supported for counters") } metric := &dto.Metric{} - if err := populateMetric(valueType, value, MakeLabelPairs(desc, labelValues), nil, metric, &ct); err != nil { + if err := populateMetric(valueType, value, MakeLabelPairs(desc, labelValues), nil, metric, timestamppb.New(ct)); err != nil { return nil, err } @@ -191,15 +191,11 @@ func populateMetric( labelPairs []*dto.LabelPair, e *dto.Exemplar, m *dto.Metric, - createdTimestamp *time.Time, + ct *timestamppb.Timestamp, ) error { m.Label = labelPairs switch t { case CounterValue: - var ct *timestamppb.Timestamp - if createdTimestamp != nil { - ct = timestamppb.New(*createdTimestamp) - } m.Counter = &dto.Counter{Value: proto.Float64(v), Exemplar: e, CreatedTimestamp: ct} case GaugeValue: m.Gauge = &dto.Gauge{Value: proto.Float64(v)} diff --git a/prometheus/value_test.go b/prometheus/value_test.go index 3e06ea8f7..004c3bb35 100644 --- a/prometheus/value_test.go +++ b/prometheus/value_test.go @@ -61,7 +61,8 @@ func TestNewConstMetricInvalidLabelValues(t *testing.T) { func TestNewConstMetricWithCreatedTimestamp(t *testing.T) { now := time.Now() - testCases := []struct { + + for _, tcase := range []struct { desc string metricType ValueType createdTimestamp time.Time @@ -82,30 +83,27 @@ func TestNewConstMetricWithCreatedTimestamp(t *testing.T) { expecErr: false, expectedCt: timestamppb.New(now), }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { + } { + t.Run(tcase.desc, func(t *testing.T) { metricDesc := NewDesc( "sample_value", "sample value", nil, nil, ) - m, err := NewConstMetricWithCreatedTimestamp(metricDesc, test.metricType, float64(1), test.createdTimestamp) - if test.expecErr && err == nil { - t.Errorf("Expected error is test %s, got no err", test.desc) + m, err := NewConstMetricWithCreatedTimestamp(metricDesc, tcase.metricType, float64(1), tcase.createdTimestamp) + if tcase.expecErr && err == nil { + t.Errorf("Expected error is test %s, got no err", tcase.desc) } - if !test.expecErr && err != nil { - t.Errorf("Didn't expect error in test %s, got %s", test.desc, err.Error()) + if !tcase.expecErr && err != nil { + t.Errorf("Didn't expect error in test %s, got %s", tcase.desc, err.Error()) } - if test.expectedCt != nil { + if tcase.expectedCt != nil { var metric dto.Metric m.Write(&metric) - if metric.Counter.CreatedTimestamp.AsTime() != test.expectedCt.AsTime() { - t.Errorf("Expected timestamp %v, got %v", test.expectedCt, &metric.Counter.CreatedTimestamp) + if metric.Counter.CreatedTimestamp.AsTime() != tcase.expectedCt.AsTime() { + t.Errorf("Expected timestamp %v, got %v", tcase.expectedCt, &metric.Counter.CreatedTimestamp) } } }) From e5639d1dcb98e3f1027e110d11ab9eda54e36f34 Mon Sep 17 00:00:00 2001 From: Bartlomiej Plotka Date: Wed, 20 Sep 2023 21:12:41 +0100 Subject: [PATCH 4/4] Update prometheus/counter_test.go Co-authored-by: Arthur Silva Sens Signed-off-by: Bartlomiej Plotka --- prometheus/counter_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prometheus/counter_test.go b/prometheus/counter_test.go index 67c273b65..3686199b5 100644 --- a/prometheus/counter_test.go +++ b/prometheus/counter_test.go @@ -382,7 +382,7 @@ func expectCTsForMetricVecValues(t testing.TB, vec *MetricVec, typ dto.MetricTyp } if !gotTs.Equal(ct) { - t.Errorf("expected created timestamp for counter with label value %q: %s, got %s", val, ct, gotTs) + t.Errorf("expected created timestamp for %s with label value %q: %s, got %s", typ, val, ct, gotTs) } } }