Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integer-only Counters #1464

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 60 additions & 5 deletions prometheus/counter.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
)

// 64-bit float mantissa: https://en.wikipedia.org/wiki/Double-precision_floating-point_format
var float64Mantissa uint64 = 9007199254740992
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this really be a const instead of a var? IMHO it would also be clearer and more legible if it were defined as 1 << 53 rather than a long decimal number.


// Counter is a Metric that represents a single numerical value that only ever
// goes up. That implies that it cannot be used to count items whose number can
// also go down, e.g. the number of currently running goroutines. Those
Expand Down Expand Up @@ -57,8 +60,52 @@ type ExemplarAdder interface {
AddWithExemplar(value float64, exemplar Labels)
}

// CounterOpts is an alias for Opts. See there for doc comments.
type CounterOpts Opts
// CounterOpts bundles the options for creating a Histogram metric. It is
// mandatory to set Name to a non-empty string. All other fields are optional
// and can safely be left at their zero value, although it is strongly
// encouraged to set a Help string.
type CounterOpts struct {
// Namespace, Subsystem, and Name are components of the fully-qualified
// name of the Metric (created by joining these components with
// "_"). Only Name is mandatory, the others merely help structuring the
// name. Note that the fully-qualified name of the metric must be a
// valid Prometheus metric name.
Namespace string
Subsystem string
Name string

// Help provides information about this metric.
//
// Metrics with the same fully-qualified name must have the same Help
// string.
Help string

// ConstLabels are used to attach fixed labels to this metric. Metrics
// with the same fully-qualified name must have the same label names in
// their ConstLabels.
//
// ConstLabels are only used rarely. In particular, do not use them to
// attach the same labels to all your metrics. Those use cases are
// better covered by target labels set by the scraping Prometheus
// server, or by one specific metric (e.g. a build_info or a
// 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

// Counters are double (float64) values. At values above 2^53, double loses
// the ability to represent discrete integer values precisely. At 2^53 the
// error is just +/-1 and is likely of little consequence. At 2^64 the error
// is +/-1024 (the next smallest number that can be represented is 2^64-2048).
// This may be significant error for long-running counters that reach the
// upper range of uint64. To present large Counter values as integer-only,
// set the IntegerExposition option. This will wrap the Counter twice, once at the
// largest safe integer value, and again when the Counter's uint64 value
// becomes 0. Prometheus will handle this rollover gracefully.
IntegerExposition bool
}

// CounterVecOpts bundles the options to create a CounterVec metric.
// It is mandatory to set CounterOpts, see there for mandatory fields. VariableLabels
Expand Down Expand Up @@ -94,7 +141,7 @@ func NewCounter(opts CounterOpts) Counter {
if opts.now == nil {
opts.now = time.Now
}
result := &counter{desc: desc, labelPairs: desc.constLabelPairs, now: opts.now}
result := &counter{desc: desc, labelPairs: desc.constLabelPairs, now: opts.now, integerExposition: opts.IntegerExposition}
result.init(result) // Init self-collection.
result.createdTs = timestamppb.New(opts.now())
return result
Expand All @@ -115,6 +162,8 @@ type counter struct {
labelPairs []*dto.LabelPair
exemplar atomic.Value // Containing nil or a *dto.Exemplar.

integerExposition bool

// now is for testing purposes, by default it's time.Now.
now func() time.Time
}
Expand All @@ -132,6 +181,9 @@ func (c *counter) Add(v float64) {
if float64(ival) == v {
atomic.AddUint64(&c.valInt, ival)
return
} else if c.integerExposition {
// perhaps there should be an AddInt() method? this is a footgun for callers.
panic(errors.New("cannot add large value with rounding error to integer counter"))
}

for {
Expand All @@ -153,8 +205,11 @@ func (c *counter) Inc() {
}

func (c *counter) get() float64 {
fval := math.Float64frombits(atomic.LoadUint64(&c.valBits))
ival := atomic.LoadUint64(&c.valInt)
if c.integerExposition {
return float64(ival % float64Mantissa)
}
fval := math.Float64frombits(atomic.LoadUint64(&c.valBits))
return fval + float64(ival)
}

Expand Down Expand Up @@ -214,7 +269,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: opts.now}
result := &counter{desc: desc, labelPairs: MakeLabelPairs(desc, lvs), now: opts.now, integerExposition: opts.IntegerExposition}
result.init(result) // Init self-collection.
result.createdTs = timestamppb.New(opts.now())
return result
Expand Down
35 changes: 35 additions & 0 deletions prometheus/counter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,3 +386,38 @@ func expectCTsForMetricVecValues(t testing.TB, vec *MetricVec, typ dto.MetricTyp
}
}
}

func TestCounterInt(t *testing.T) {
now := time.Now()

counter := NewCounter(CounterOpts{
Name: "test",
Help: "test help",
now: func() time.Time { return now },
IntegerExposition: true,
}).(*counter)

// large is greater than the max safe integer value, but has no rounding error itself and ergo is integer-safe
large := math.Nextafter(float64(float64Mantissa), math.MaxUint64)
counter.Add(large)
if expected, got := 0.0, math.Float64frombits(counter.valBits); expected != got {
t.Errorf("valBits expected %f, got %f.", expected, got)
}
if expected, got := uint64(large), counter.valInt; expected != got {
t.Errorf("valInts expected %d, got %d.", expected, got)
}

m := &dto.Metric{}
counter.Write(m)

expected := &dto.Metric{
Counter: &dto.Counter{
Value: proto.Float64(float64(uint64(large) % float64Mantissa)), // wrapped value!
CreatedTimestamp: timestamppb.New(now),
},
}

if !proto.Equal(expected, m) {
t.Errorf("expected %q, got %q", expected, m)
}
}
3 changes: 0 additions & 3 deletions prometheus/metric.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,6 @@ 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
Expand Down
Loading