From 59086cc318b165bf80a2ffa07654a9142d8ef9dd Mon Sep 17 00:00:00 2001 From: Arthur Silva Sens Date: Fri, 11 Aug 2023 18:02:28 -0300 Subject: [PATCH] Extend Counters, Summaries and Histograms with creation timestamp Signed-off-by: Arthur Silva Sens --- go.mod | 2 +- go.sum | 4 +- prometheus/counter.go | 15 +++++++- prometheus/counter_test.go | 37 ++++++++++++++++++ prometheus/gauge.go | 2 +- prometheus/histogram.go | 20 ++++++++-- prometheus/histogram_test.go | 41 ++++++++++++++++++++ prometheus/metric.go | 2 + prometheus/summary.go | 26 +++++++++++-- prometheus/summary_test.go | 75 ++++++++++++++++++++++++++++++++++++ prometheus/value.go | 50 ++++++++++++++++++++++-- prometheus/value_test.go | 57 +++++++++++++++++++++++++++ 12 files changed, 314 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 9c354dfaf..16c411139 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.42.0 github.com/prometheus/procfs v0.11.1 golang.org/x/sys v0.10.0 diff --git a/go.sum b/go.sum index 022e77354..80c2f84e7 100644 --- a/go.sum +++ b/go.sum @@ -152,8 +152,8 @@ github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQg github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -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.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= diff --git a/prometheus/counter.go b/prometheus/counter.go index d8ade9ba7..fba4f212c 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,6 +111,7 @@ type counter struct { selfCollector desc *Desc + createdTs *timestamppb.Timestamp labelPairs []*dto.LabelPair exemplar atomic.Value // Containing nil or a *dto.Exemplar. @@ -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 a6ec4de46..93042e5c0 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 // To mock out time.Now() for testing. } // HistogramVecOpts bundles the options to create a HistogramVec metric. @@ -492,6 +495,9 @@ type HistogramVecOpts struct { // perform the corresponding type assertion. Exemplars are tracked separately // for each bucket. func NewHistogram(opts HistogramOpts) Histogram { + if opts.now == nil { + opts.now = time.Now + } return newHistogram( NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), @@ -569,6 +575,7 @@ func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogr h.exemplars = make([]atomic.Value, len(h.upperBounds)+1) h.init(h) // Init self-collection. + h.createdTs = timestamppb.New(opts.now()) return h } @@ -707,6 +714,7 @@ 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. } @@ -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..50dd9c752 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 // To mock out time.Now() for testing. } // BuildFQName joins the given three name components by "_". Empty name diff --git a/prometheus/summary.go b/prometheus/summary.go index 440f29efd..2d096f66c 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 // To mock out time.Now() for testing. } // 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..f5665b794 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,17 @@ func populateMetric( labelPairs []*dto.LabelPair, e *dto.Exemplar, m *dto.Metric, + createdTimestamp *time.Time, ) error { + var ct *timestamppb.Timestamp + if createdTimestamp != nil { + ct = timestamppb.New(*createdTimestamp) + } + m.Label = labelPairs switch t { case CounterValue: - m.Counter = &dto.Counter{Value: proto.Float64(v), Exemplar: e} + 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) + } + } + }) + } +}