Proposal: Add new type of counter: FloatCounter (#5)

* Add new type of counter: FloatCounter
* sometimes you need to count things with more precision than uint64
* FloatCounter also usefull if you need setable Gauge w/o callback func

* Fix PR review:
* sync.RWMutex -> sync.Mutex
* more idiomatic add/sub
This commit is contained in:
Ivan G 2020-01-23 13:48:00 +03:00 committed by Aliaksandr Valialkin
parent 21c3ffd10e
commit e6d6f46b5d
4 changed files with 257 additions and 0 deletions

82
floatcounter.go Normal file
View File

@ -0,0 +1,82 @@
package metrics
import (
"fmt"
"io"
"sync"
)
// NewFloatCounter registers and returns new counter of float64 type with the given name.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned counter is safe to use from concurrent goroutines.
func NewFloatCounter(name string) *FloatCounter {
return defaultSet.NewFloatCounter(name)
}
// FloatCounter is a float64 counter guarded by RWmutex.
//
// It may be used as a gauge if Add and Sub are called.
type FloatCounter struct {
mu sync.Mutex
n float64
}
// Add adds n to fc.
func (fc *FloatCounter) Add(n float64) {
fc.mu.Lock()
fc.n += n
fc.mu.Unlock()
}
// Sub substracts n from fc.
func (fc *FloatCounter) Sub(n float64) {
fc.mu.Lock()
fc.n -= n
fc.mu.Unlock()
}
// Get returns the current value for fc.
func (fc *FloatCounter) Get() float64 {
fc.mu.Lock()
n := fc.n
fc.mu.Unlock()
return n
}
// Set sets fc value to n.
func (fc *FloatCounter) Set(n float64) {
fc.mu.Lock()
fc.n = n
fc.mu.Unlock()
}
// marshalTo marshals fc with the given prefix to w.
func (fc *FloatCounter) marshalTo(prefix string, w io.Writer) {
v := fc.Get()
fmt.Fprintf(w, "%s %g\n", prefix, v)
}
// GetOrCreateFloatCounter returns registered FloatCounter with the given name
// or creates new FloatCounter if the registry doesn't contain FloatCounter with
// the given name.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned FloatCounter is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewFloatCounter instead of GetOrCreateFloatCounter.
func GetOrCreateFloatCounter(name string) *FloatCounter {
return defaultSet.GetOrCreateFloatCounter(name)
}

View File

@ -0,0 +1,41 @@
package metrics_test
import (
"fmt"
"github.com/VictoriaMetrics/metrics"
)
func ExampleFloatCounter() {
// Define a float64 counter in global scope.
var fc = metrics.NewFloatCounter(`float_metric_total{label1="value1", label2="value2"}`)
// Add to the counter when needed.
for i := 0; i < 10; i++ {
fc.Add(1.01)
}
n := fc.Get()
fmt.Println(n)
// Output:
// 10.1
}
func ExampleFloatCounter_vec() {
for i := 0; i < 3; i++ {
// Dynamically construct metric name and pass it to GetOrCreateFloatCounter.
name := fmt.Sprintf(`float_metric_total{label1=%q, label2="%d"}`, "value1", i)
metrics.GetOrCreateFloatCounter(name).Add(float64(i) + 1.01)
}
// Read counter values.
for i := 0; i < 3; i++ {
name := fmt.Sprintf(`float_metric_total{label1=%q, label2="%d"}`, "value1", i)
n := metrics.GetOrCreateFloatCounter(name).Get()
fmt.Println(n)
}
// Output:
// 1.01
// 2.01
// 3.01
}

76
floatcounter_test.go Normal file
View File

@ -0,0 +1,76 @@
package metrics
import (
"fmt"
"testing"
)
func TestFloatCounterSerial(t *testing.T) {
name := "FloatCounterSerial"
c := NewFloatCounter(name)
c.Add(0.1)
if n := c.Get(); n != 0.1 {
t.Fatalf("unexpected counter value; got %f; want 0.1", n)
}
c.Set(123.00001)
if n := c.Get(); n != 123.00001 {
t.Fatalf("unexpected counter value; got %f; want 123.00001", n)
}
c.Sub(0.00001)
if n := c.Get(); n != 123 {
t.Fatalf("unexpected counter value; got %f; want 123", n)
}
c.Add(2.002)
if n := c.Get(); n != 125.002 {
t.Fatalf("unexpected counter value; got %f; want 125.002", n)
}
// Verify MarshalTo
testMarshalTo(t, c, "foobar", "foobar 125.002\n")
}
func TestFloatCounterConcurrent(t *testing.T) {
name := "FloatCounterConcurrent"
c := NewFloatCounter(name)
err := testConcurrent(func() error {
nPrev := c.Get()
for i := 0; i < 10; i++ {
c.Add(1.001)
if n := c.Get(); n <= nPrev {
return fmt.Errorf("counter value must be greater than %f; got %f", nPrev, n)
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
func TestGetOrCreateFloatCounterSerial(t *testing.T) {
name := "GetOrCreateFloatCounterSerial"
if err := testGetOrCreateCounter(name); err != nil {
t.Fatal(err)
}
}
func TestGetOrCreateFloatCounterConcurrent(t *testing.T) {
name := "GetOrCreateFloatCounterConcurrent"
err := testConcurrent(func() error {
return testGetOrCreateFloatCounter(name)
})
if err != nil {
t.Fatal(err)
}
}
func testGetOrCreateFloatCounter(name string) error {
c1 := GetOrCreateFloatCounter(name)
for i := 0; i < 10; i++ {
c2 := GetOrCreateFloatCounter(name)
if c1 != c2 {
return fmt.Errorf("unexpected counter returned; got %p; want %p", c2, c1)
}
}
return nil
}

58
set.go
View File

@ -169,6 +169,64 @@ func (s *Set) GetOrCreateCounter(name string) *Counter {
return c
}
// NewFloatCounter registers and returns new FloatCounter with the given name in the s.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned FloatCounter is safe to use from concurrent goroutines.
func (s *Set) NewFloatCounter(name string) *FloatCounter {
c := &FloatCounter{}
s.registerMetric(name, c)
return c
}
// GetOrCreateFloatCounter returns registered FloatCounter in s with the given name
// or creates new FloatCounter if s doesn't contain FloatCounter with the given name.
//
// name must be valid Prometheus-compatible metric with possible labels.
// For instance,
//
// * foo
// * foo{bar="baz"}
// * foo{bar="baz",aaa="b"}
//
// The returned FloatCounter is safe to use from concurrent goroutines.
//
// Performance tip: prefer NewFloatCounter instead of GetOrCreateFloatCounter.
func (s *Set) GetOrCreateFloatCounter(name string) *FloatCounter {
s.mu.Lock()
nm := s.m[name]
s.mu.Unlock()
if nm == nil {
// Slow path - create and register missing counter.
if err := validateMetric(name); err != nil {
panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
}
nmNew := &namedMetric{
name: name,
metric: &FloatCounter{},
}
s.mu.Lock()
nm = s.m[name]
if nm == nil {
nm = nmNew
s.m[name] = nm
s.a = append(s.a, nm)
}
s.mu.Unlock()
}
c, ok := nm.metric.(*FloatCounter)
if !ok {
panic(fmt.Errorf("BUG: metric %q isn't a Counter. It is %T", name, nm.metric))
}
return c
}
// NewGauge registers and returns gauge with the given name in s, which calls f
// to obtain gauge value.
//