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:
parent
21c3ffd10e
commit
e6d6f46b5d
82
floatcounter.go
Normal file
82
floatcounter.go
Normal 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)
|
||||||
|
}
|
41
floatcounter_example_test.go
Normal file
41
floatcounter_example_test.go
Normal 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
76
floatcounter_test.go
Normal 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
58
set.go
@ -169,6 +169,64 @@ func (s *Set) GetOrCreateCounter(name string) *Counter {
|
|||||||
return c
|
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
|
// NewGauge registers and returns gauge with the given name in s, which calls f
|
||||||
// to obtain gauge value.
|
// to obtain gauge value.
|
||||||
//
|
//
|
||||||
|
Loading…
Reference in New Issue
Block a user