Extend Counters, Summaries and Histograms with creation timestamp

Signed-off-by: Arthur Silva Sens <arthur.sens@coralogix.com>
This commit is contained in:
Arthur Silva Sens 2023-08-11 18:02:28 -03:00
parent 51d24f8680
commit 3c7e78cf3e
No known key found for this signature in database
GPG Key ID: 7B844F1CE139BA7E
12 changed files with 315 additions and 19 deletions

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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
}),
}

View File

@ -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())
}
}

View File

@ -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

View File

@ -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 {
@ -750,6 +758,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,
}
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))

View File

@ -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())
}
}

View File

@ -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

View File

@ -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 {
@ -492,6 +506,7 @@ func (s *noObjectivesSummary) Write(out *dto.Metric) error {
sum := &dto.Summary{
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)

View File

@ -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())
}
})
}
}

View File

@ -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:

View File

@ -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)
}
}
})
}
}