Compare commits

...

4 Commits

Author SHA1 Message Date
Not Rob Pike d8688a65ed
Merge 9f18e2fc42 into 769f6f63ec 2024-11-01 18:29:33 +01:00
Not Rob Pike 9f18e2fc42 remove unused now field 2024-03-12 10:56:43 -07:00
Not Rob Pike afd6f33dcd remove incorrect comment about atomic safety
indeed the ival and fval can get updated separately, but such update
is always one or the other, not both.
2024-03-11 16:31:49 -07:00
Not Rob Pike f55a4e3265 Integer-only Counters
Signed-off-by: Not Rob Pike <closeup_oblique.0o@icloud.com>
2024-03-10 12:38:10 -07:00
3 changed files with 95 additions and 8 deletions

View File

@ -23,6 +23,9 @@ import (
"google.golang.org/protobuf/types/known/timestamppb" "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
// Counter is a Metric that represents a single numerical value that only ever // 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 // 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 // also go down, e.g. the number of currently running goroutines. Those
@ -57,8 +60,52 @@ type ExemplarAdder interface {
AddWithExemplar(value float64, exemplar Labels) AddWithExemplar(value float64, exemplar Labels)
} }
// CounterOpts is an alias for Opts. See there for doc comments. // CounterOpts bundles the options for creating a Histogram metric. It is
type CounterOpts Opts // 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. // CounterVecOpts bundles the options to create a CounterVec metric.
// It is mandatory to set CounterOpts, see there for mandatory fields. VariableLabels // It is mandatory to set CounterOpts, see there for mandatory fields. VariableLabels
@ -94,7 +141,7 @@ func NewCounter(opts CounterOpts) Counter {
if opts.now == nil { if opts.now == nil {
opts.now = time.Now 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.init(result) // Init self-collection.
result.createdTs = timestamppb.New(opts.now()) result.createdTs = timestamppb.New(opts.now())
return result return result
@ -115,6 +162,8 @@ type counter struct {
labelPairs []*dto.LabelPair labelPairs []*dto.LabelPair
exemplar atomic.Value // Containing nil or a *dto.Exemplar. exemplar atomic.Value // Containing nil or a *dto.Exemplar.
integerExposition bool
// now is for testing purposes, by default it's time.Now. // now is for testing purposes, by default it's time.Now.
now func() time.Time now func() time.Time
} }
@ -132,6 +181,9 @@ func (c *counter) Add(v float64) {
if float64(ival) == v { if float64(ival) == v {
atomic.AddUint64(&c.valInt, ival) atomic.AddUint64(&c.valInt, ival)
return 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 { for {
@ -153,8 +205,11 @@ func (c *counter) Inc() {
} }
func (c *counter) get() float64 { func (c *counter) get() float64 {
fval := math.Float64frombits(atomic.LoadUint64(&c.valBits))
ival := atomic.LoadUint64(&c.valInt) ival := atomic.LoadUint64(&c.valInt)
if c.integerExposition {
return float64(ival % float64Mantissa)
}
fval := math.Float64frombits(atomic.LoadUint64(&c.valBits))
return fval + float64(ival) return fval + float64(ival)
} }
@ -214,7 +269,7 @@ func (v2) NewCounterVec(opts CounterVecOpts) *CounterVec {
if len(lvs) != len(desc.variableLabels.names) { if len(lvs) != len(desc.variableLabels.names) {
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.names, lvs)) 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.init(result) // Init self-collection.
result.createdTs = timestamppb.New(opts.now()) result.createdTs = timestamppb.New(opts.now())
return result return result

View File

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

View File

@ -92,9 +92,6 @@ type Opts struct {
// machine_role metric). See also // machine_role metric). See also
// https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels-not-static-scraped-labels // https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels-not-static-scraped-labels
ConstLabels 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 // BuildFQName joins the given three name components by "_". Empty name