From df7fa494179f42a8c08cf21d95da9ae09564f907 Mon Sep 17 00:00:00 2001 From: Arthur Silva Sens Date: Thu, 21 Sep 2023 06:46:54 -0300 Subject: [PATCH] Extend Counters, Summaries and Histograms with creation timestamp (#1313) * Extend Counters, Summaries and Histograms with creation timestamp Signed-off-by: Arthur Silva Sens * Backport created timestamp to existing tests Signed-off-by: Arthur Silva Sens * 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 * Update prometheus/counter_test.go Co-authored-by: Arthur Silva Sens Signed-off-by: Bartlomiej Plotka --------- Signed-off-by: Arthur Silva Sens Signed-off-by: bwplotka Signed-off-by: Bartlomiej Plotka Co-authored-by: bwplotka --- go.mod | 2 +- go.sum | 4 +- prometheus/counter.go | 20 ++- prometheus/counter_test.go | 98 +++++++++++++- prometheus/example_metricvec_test.go | 4 +- prometheus/examples_test.go | 38 ++++-- prometheus/expvar_collector_test.go | 2 +- prometheus/gauge.go | 2 +- prometheus/histogram.go | 30 +++-- prometheus/histogram_test.go | 183 +++++++++++++++++++++------ prometheus/metric.go | 3 + prometheus/registry_test.go | 7 +- prometheus/summary.go | 29 ++++- prometheus/summary_test.go | 54 ++++++++ prometheus/utils_test.go | 37 ++++-- prometheus/value.go | 45 ++++++- prometheus/value_test.go | 55 ++++++++ prometheus/wrap_test.go | 9 ++ 18 files changed, 531 insertions(+), 91 deletions(-) diff --git a/go.mod b/go.mod index a0afbd4..97e627e 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 bbabe42..d07d422 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 d8ade9b..5f72deb 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,12 @@ 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 is for testing purposes, by default it's time.Now. + now func() time.Time } func (c *counter) Desc() *Desc { @@ -159,8 +166,7 @@ func (c *counter) Write(out *dto.Metric) error { exemplar = e.(*dto.Exemplar) } val := c.get() - - return populateMetric(CounterValue, val, c.labelPairs, exemplar, out) + return populateMetric(CounterValue, val, c.labelPairs, exemplar, out, c.createdTs) } func (c *counter) updateExemplar(v float64, l Labels) { @@ -200,13 +206,17 @@ 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) { 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 cf0fd54..3686199 100644 --- a/prometheus/counter_test.go +++ b/prometheus/counter_test.go @@ -26,10 +26,13 @@ import ( ) func TestCounterAdd(t *testing.T) { + now := time.Now() + counter := NewCounter(CounterOpts{ Name: "test", Help: "test help", ConstLabels: Labels{"a": "1", "b": "2"}, + now: func() time.Time { return now }, }).(*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(now), + }, } 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() + counter := NewCounter(CounterOpts{ Name: "test", Help: "test help", + now: func() time.Time { return now }, }).(*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(now), }, } @@ -183,9 +193,12 @@ func TestCounterAddInf(t *testing.T) { } func TestCounterAddLarge(t *testing.T) { + now := time.Now() + counter := NewCounter(CounterOpts{ Name: "test", Help: "test help", + now: func() time.Time { return now }, }).(*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(now), }, } @@ -213,10 +227,14 @@ func TestCounterAddLarge(t *testing.T) { } func TestCounterAddSmall(t *testing.T) { + now := time.Now() + counter := NewCounter(CounterOpts{ Name: "test", Help: "test help", + now: func() time.Time { return now }, }).(*counter) + small := 0.000000000001 counter.Add(small) if expected, got := small, math.Float64frombits(counter.valBits); expected != got { @@ -231,7 +249,8 @@ func TestCounterAddSmall(t *testing.T) { expected := &dto.Metric{ Counter: &dto.Counter{ - Value: proto.Float64(small), + Value: proto.Float64(small), + CreatedTimestamp: timestamppb.New(now), }, } @@ -246,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 { @@ -298,3 +317,72 @@ func TestCounterExemplar(t *testing.T) { t.Error("adding exemplar with oversized labels succeeded") } } + +func TestCounterVecCreatedTimestampWithDeletes(t *testing.T) { + now := time.Now() + + counterVec := NewCounterVec(CounterOpts{ + Name: "test", + Help: "test help", + now: func() time.Time { return now }, + }, []string{"label"}) + + // First use of "With" should populate CT. + counterVec.WithLabelValues("1") + expected := map[string]time.Time{"1": now} + + 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 expectCTsForMetricVecValues(t testing.TB, vec *MetricVec, typ dto.MetricType, ctsPerLabelValue map[string]time.Time) { + t.Helper() + + for val, ct := range ctsPerLabelValue { + var metric dto.Metric + m, err := vec.GetMetricWithLabelValues(val) + if err != nil { + t.Fatal(err) + } + + 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 %s with label value %q: %s, got %s", typ, val, ct, gotTs) + } + } +} diff --git a/prometheus/example_metricvec_test.go b/prometheus/example_metricvec_test.go index a9f29d4..59e43f8 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 9d918e1..89232bc 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() { @@ -320,10 +336,10 @@ func ExampleSummary() { metric := &dto.Metric{} temps.Write(metric) - 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() { @@ -355,10 +371,11 @@ func ExampleSummaryVec() { if err != nil || len(metricFamilies) != 1 { panic("unexpected behavior of custom test registry") } - 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() { @@ -382,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}]}} @@ -405,10 +422,11 @@ func ExampleHistogram() { // internally). metric := &dto.Metric{} temps.Write(metric) - 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() { @@ -432,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}]}} @@ -470,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"}}]}} @@ -632,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 9b1202e..a8d0ed2 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/gauge.go b/prometheus/gauge.go index d2bce21..8e94883 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 6e9b44f..ab758db 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,9 @@ type HistogramOpts struct { NativeHistogramMaxBucketNumber uint32 NativeHistogramMinResetDuration time.Duration NativeHistogramMaxZeroThreshold float64 + + // now is for testing purposes, by default it's time.Now. + now func() time.Time } // HistogramVecOpts bundles the options to create a HistogramVec metric. @@ -519,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, @@ -526,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 @@ -706,9 +714,11 @@ type histogram struct { nativeHistogramMaxZeroThreshold float64 nativeHistogramMaxBuckets uint32 nativeHistogramMinResetDuration time.Duration - lastResetTime time.Time // Protected by mtx. + // lastResetTime is protected by mtx. It is also used as created timestamp. + lastResetTime time.Time - now func() time.Time // To mock out time.Now() for testing. + // now is for testing purposes, by default it's time.Now. + now func() time.Time } func (h *histogram) Desc() *Desc { @@ -747,9 +757,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: timestamppb.New(h.lastResetTime), } out.Histogram = his out.Label = h.labelPairs @@ -1194,6 +1205,7 @@ type constHistogram struct { sum float64 buckets map[float64]uint64 labelPairs []*dto.LabelPair + createdTs *timestamppb.Timestamp } func (h *constHistogram) Desc() *Desc { @@ -1201,7 +1213,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 e2bc162..60b43fd 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 { @@ -469,6 +469,8 @@ func TestHistogramExemplar(t *testing.T) { } func TestNativeHistogram(t *testing.T) { + now := time.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(now), }, }, { 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(now), }, }, { @@ -525,6 +529,7 @@ func TestNativeHistogram(t *testing.T) { PositiveSpan: []*dto.BucketSpan{ {Offset: proto.Int32(0), Length: proto.Uint32(0)}, }, + CreatedTimestamp: timestamppb.New(now), }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now.Add(8 * time.Minute)), // We expect reset to happen after 8 observations. }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now), }, }, { @@ -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(now.Add(8 * time.Minute)), // We expect reset to happen after 8 observations. }, }, { @@ -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(now.Add(10 * time.Minute)), // We expect reset to happen after 9 minutes. }, }, { @@ -927,13 +953,16 @@ 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(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", @@ -942,12 +971,10 @@ func TestNativeHistogram(t *testing.T) { NativeHistogramMaxBucketNumber: s.maxBuckets, NativeHistogramMinResetDuration: s.minResetDuration, NativeHistogramMaxZeroThreshold: s.maxZeroThreshold, + 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) @@ -972,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 @@ -988,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++ { @@ -1152,3 +1178,82 @@ 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()) + } +} + +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 07bbc9d..f018e57 100644 --- a/prometheus/metric.go +++ b/prometheus/metric.go @@ -92,6 +92,9 @@ type Opts struct { // machine_role metric). See also // https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels-not-static-scraped-labels ConstLabels Labels + + // 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/registry_test.go b/prometheus/registry_test.go index d1b7a19..654778b 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/summary.go b/prometheus/summary.go index 440f29e..e2c2fbd 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,9 @@ type SummaryOpts struct { // is the internal buffer size of the underlying package // "github.com/bmizerany/perks/quantile"). BufCap uint32 + + // now is for testing purposes, by default it's time.Now. + now func() time.Time } // SummaryVecOpts bundles the options to create a SummaryVec metric. @@ -222,6 +226,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 +237,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 } @@ -245,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++ { @@ -259,6 +267,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 +295,8 @@ type summary struct { headStream *quantile.Stream headStreamIdx int headStreamExpTime, hotBufExpTime time.Time + + createdTs *timestamppb.Timestamp } func (s *summary) Desc() *Desc { @@ -307,7 +318,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 +453,8 @@ type noObjectivesSummary struct { counts [2]*summaryCounts labelPairs []*dto.LabelPair + + createdTs *timestamppb.Timestamp } func (s *noObjectivesSummary) Desc() *Desc { @@ -490,8 +505,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 +697,7 @@ type constSummary struct { sum float64 quantiles map[float64]float64 labelPairs []*dto.LabelPair + createdTs *timestamppb.Timestamp } func (s *constSummary) Desc() *Desc { @@ -688,7 +705,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 eff733a..2a0d1f3 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) { @@ -420,3 +427,50 @@ func getBounds(vars []float64, q, ε float64) (min, max float64) { } return } + +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() + t.Run(tcase.desc, func(t *testing.T) { + summaryVec := NewSummaryVec(SummaryOpts{ + Name: "test", + Help: "test help", + Objectives: tcase.objectives, + now: func() time.Time { return now }, + }, []string{"label"}) + + // First use of "With" should populate CT. + summaryVec.WithLabelValues("1") + expected := map[string]time.Time{"1": now} + + now = now.Add(1 * time.Hour) + expectCTsForMetricVecValues(t, summaryVec.MetricVec, dto.MetricType_SUMMARY, expected) + + // Two more labels at different times. + summaryVec.WithLabelValues("2") + expected["2"] = now + + 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 a8ab001..81d0820 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 4bf5727..cc23011 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, timestamppb.New(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,12 @@ func populateMetric( labelPairs []*dto.LabelPair, e *dto.Exemplar, m *dto.Metric, + ct *timestamppb.Timestamp, ) error { 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 51867b5..004c3bb 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,54 @@ func TestNewConstMetricInvalidLabelValues(t *testing.T) { } } } + +func TestNewConstMetricWithCreatedTimestamp(t *testing.T) { + now := time.Now() + + for _, tcase := range []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), + }, + } { + t.Run(tcase.desc, func(t *testing.T) { + metricDesc := NewDesc( + "sample_value", + "sample value", + nil, + nil, + ) + 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 !tcase.expecErr && err != nil { + t.Errorf("Didn't expect error in test %s, got %s", tcase.desc, err.Error()) + } + + if tcase.expectedCt != nil { + var metric dto.Metric + m.Write(&metric) + if metric.Counter.CreatedTimestamp.AsTime() != tcase.expectedCt.AsTime() { + t.Errorf("Expected timestamp %v, got %v", tcase.expectedCt, &metric.Counter.CreatedTimestamp) + } + } + }) + } +} diff --git a/prometheus/wrap_test.go b/prometheus/wrap_test.go index 7e5bba1..d2b4e4c 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()