diff --git a/floatcounter.go b/floatcounter.go new file mode 100644 index 0000000..d01dd85 --- /dev/null +++ b/floatcounter.go @@ -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) +} diff --git a/floatcounter_example_test.go b/floatcounter_example_test.go new file mode 100644 index 0000000..3f32770 --- /dev/null +++ b/floatcounter_example_test.go @@ -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 +} diff --git a/floatcounter_test.go b/floatcounter_test.go new file mode 100644 index 0000000..44931c3 --- /dev/null +++ b/floatcounter_test.go @@ -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 +} diff --git a/set.go b/set.go index 15976df..3b37e91 100644 --- a/set.go +++ b/set.go @@ -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. //