Skip to content

Commit

Permalink
fix monotonic counter implementations (#100)
Browse files Browse the repository at this point in the history
This change is a companion to the fix for monotonic counter implementations
in spectatord.

Netflix-Skunkworks/spectatord#90

The original monotonic counter (`C`) was always intended to be used for the
case where a monotonic data source needs to be transformed into base units
for recording data. For example, transforming nanoseconds into seconds. This
requires division, which results in floats.

There is a valid use case for handling uints in monotonic counters, if the
data source is already in a base unit, such as bytes. Thus, a new meter type
`U` is added to spectatord which supports this use case.
  • Loading branch information
copperlight committed Jun 14, 2024
1 parent 9a2b37c commit 22102a0
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 65 deletions.
8 changes: 6 additions & 2 deletions spectator/meter/monotonic_counter.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import (
// The value is a monotonically increasing number. A minimum of two samples must be received
// in order for spectatord to calculate a delta value and report it to the backend.
//
// This version of the monotonic counter is intended to support use cases where a data source value
// needs to be transformed into base units through division (e.g. nanoseconds into seconds), and
// thus, the data type is float64.
//
// A variety of networking metrics may be reported monotonically and this metric type provides a
// convenient means of recording these values, at the expense of a slower time-to-first metric.
type MonotonicCounter struct {
Expand All @@ -30,7 +34,7 @@ func (c *MonotonicCounter) MeterId() *Id {
}

// Set sets a value as the current measurement; spectatord calculates the delta.
func (c *MonotonicCounter) Set(value uint64) {
var line = fmt.Sprintf("%s:%s:%d", c.meterTypeSymbol, c.id.spectatordId, value)
func (c *MonotonicCounter) Set(value float64) {
var line = fmt.Sprintf("%s:%s:%f", c.meterTypeSymbol, c.id.spectatordId, value)
c.writer.Write(line)
}
2 changes: 1 addition & 1 deletion spectator/meter/monotonic_counter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func TestMonotonicCounter_Set(t *testing.T) {

c.Set(4)

expected := "C:set:4"
expected := "C:set:4.000000"
if w.Lines()[0] != expected {
t.Error("Expected ", expected, " got ", w.Lines()[0])
}
Expand Down
40 changes: 40 additions & 0 deletions spectator/meter/monotonic_counter_uint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package meter

import (
"fmt"
"github.com/Netflix/spectator-go/v2/spectator/writer"
)

// MonotonicCounterUint is used to measure the rate at which some event is occurring. This
// type is safe for concurrent use.
//
// The value is a monotonically increasing number. A minimum of two samples must be received
// in order for spectatord to calculate a delta value and report it to the backend.
//
// This version of the monotonic counter is intended to support use cases where a data source value
// can be sampled as-is, because it is already in base units, such as bytes, and thus, the data type
// is uint64.
//
// A variety of networking metrics may be reported monotonically and this metric type provides a
// convenient means of recording these values, at the expense of a slower time-to-first metric.
type MonotonicCounterUint struct {
id *Id
writer writer.Writer
meterTypeSymbol string
}

// NewMonotonicCounterUint generates a new counter, using the provided meter identifier.
func NewMonotonicCounterUint(id *Id, writer writer.Writer) *MonotonicCounterUint {
return &MonotonicCounterUint{id, writer, "U"}
}

// MeterId returns the meter identifier.
func (c *MonotonicCounterUint) MeterId() *Id {
return c.id
}

// Set sets a value as the current measurement; spectatord calculates the delta.
func (c *MonotonicCounterUint) Set(value uint64) {
var line = fmt.Sprintf("%s:%s:%d", c.meterTypeSymbol, c.id.spectatordId, value)
c.writer.Write(line)
}
19 changes: 19 additions & 0 deletions spectator/meter/monotonic_counter_uint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package meter

import (
"github.com/Netflix/spectator-go/v2/spectator/writer"
"testing"
)

func TestMonotonicCounterUint_Set(t *testing.T) {
w := writer.MemoryWriter{}
id := NewId("set", nil)
c := NewMonotonicCounterUint(id, &w)

c.Set(4)

expected := "U:set:4"
if w.Lines()[0] != expected {
t.Error("Expected ", expected, " got ", w.Lines()[0])
}
}
80 changes: 42 additions & 38 deletions spectator/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,28 @@ type Meter interface {
type Registry interface {
GetLogger() logger.Logger
NewId(name string, tags map[string]string) *meter.Id
AgeGauge(name string, tags map[string]string) *meter.AgeGauge
AgeGaugeWithId(id *meter.Id) *meter.AgeGauge
Counter(name string, tags map[string]string) *meter.Counter
CounterWithId(id *meter.Id) *meter.Counter
MonotonicCounter(name string, tags map[string]string) *meter.MonotonicCounter
MonotonicCounterWithId(id *meter.Id) *meter.MonotonicCounter
Timer(name string, tags map[string]string) *meter.Timer
TimerWithId(id *meter.Id) *meter.Timer
DistributionSummary(name string, tags map[string]string) *meter.DistributionSummary
DistributionSummaryWithId(id *meter.Id) *meter.DistributionSummary
Gauge(name string, tags map[string]string) *meter.Gauge
GaugeWithId(id *meter.Id) *meter.Gauge
GaugeWithTTL(name string, tags map[string]string, ttl time.Duration) *meter.Gauge
GaugeWithIdWithTTL(id *meter.Id, ttl time.Duration) *meter.Gauge
AgeGauge(name string, tags map[string]string) *meter.AgeGauge
AgeGaugeWithId(id *meter.Id) *meter.AgeGauge
MaxGauge(name string, tags map[string]string) *meter.MaxGauge
MaxGaugeWithId(id *meter.Id) *meter.MaxGauge
DistributionSummary(name string, tags map[string]string) *meter.DistributionSummary
DistributionSummaryWithId(id *meter.Id) *meter.DistributionSummary
MonotonicCounter(name string, tags map[string]string) *meter.MonotonicCounter
MonotonicCounterWithId(id *meter.Id) *meter.MonotonicCounter
MonotonicCounterUint(name string, tags map[string]string) *meter.MonotonicCounterUint
MonotonicCounterUintWithId(id *meter.Id) *meter.MonotonicCounterUint
PercentileDistributionSummary(name string, tags map[string]string) *meter.PercentileDistributionSummary
PercentileDistributionSummaryWithId(id *meter.Id) *meter.PercentileDistributionSummary
PercentileTimer(name string, tags map[string]string) *meter.PercentileTimer
PercentileTimerWithId(id *meter.Id) *meter.PercentileTimer
Timer(name string, tags map[string]string) *meter.Timer
TimerWithId(id *meter.Id) *meter.Timer
GetWriter() writer.Writer
Close()
}
Expand Down Expand Up @@ -97,34 +99,28 @@ func (r *spectatordRegistry) NewId(name string, tags map[string]string) *meter.I
return newId
}

// Counter calls NewId() with the name and tags, and then calls r.CounterWithId()
// using that *Id.
func (r *spectatordRegistry) Counter(name string, tags map[string]string) *meter.Counter {
return meter.NewCounter(r.NewId(name, tags), r.writer)
func (r *spectatordRegistry) AgeGauge(name string, tags map[string]string) *meter.AgeGauge {
return meter.NewAgeGauge(r.NewId(name, tags), r.writer)
}

func (r *spectatordRegistry) CounterWithId(id *meter.Id) *meter.Counter {
return meter.NewCounter(id, r.writer)
func (r *spectatordRegistry) AgeGaugeWithId(id *meter.Id) *meter.AgeGauge {
return meter.NewAgeGauge(id, r.writer)
}

// MonotonicCounter calls NewId() with the name and tags, and then calls r.MonotonicCounterWithId()
// using that *Id.
func (r *spectatordRegistry) MonotonicCounter(name string, tags map[string]string) *meter.MonotonicCounter {
return meter.NewMonotonicCounter(r.NewId(name, tags), r.writer)
func (r *spectatordRegistry) Counter(name string, tags map[string]string) *meter.Counter {
return meter.NewCounter(r.NewId(name, tags), r.writer)
}

func (r *spectatordRegistry) MonotonicCounterWithId(id *meter.Id) *meter.MonotonicCounter {
return meter.NewMonotonicCounter(id, r.writer)
func (r *spectatordRegistry) CounterWithId(id *meter.Id) *meter.Counter {
return meter.NewCounter(id, r.writer)
}

// Timer calls NewId() with the name and tags, and then calls r.TimerWithId() using that *Id.
func (r *spectatordRegistry) Timer(name string, tags map[string]string) *meter.Timer {
return meter.NewTimer(r.NewId(name, tags), r.writer)
func (r *spectatordRegistry) DistributionSummary(name string, tags map[string]string) *meter.DistributionSummary {
return meter.NewDistributionSummary(r.NewId(name, tags), r.writer)
}

// TimerWithId returns a new *Timer, using the provided meter identifier.
func (r *spectatordRegistry) TimerWithId(id *meter.Id) *meter.Timer {
return meter.NewTimer(id, r.writer)
func (r *spectatordRegistry) DistributionSummaryWithId(id *meter.Id) *meter.DistributionSummary {
return meter.NewDistributionSummary(id, r.writer)
}

func (r *spectatordRegistry) Gauge(name string, tags map[string]string) *meter.Gauge {
Expand All @@ -143,14 +139,6 @@ func (r *spectatordRegistry) GaugeWithIdWithTTL(id *meter.Id, duration time.Dura
return meter.NewGaugeWithTTL(id, r.writer, duration)
}

func (r *spectatordRegistry) AgeGauge(name string, tags map[string]string) *meter.AgeGauge {
return meter.NewAgeGauge(r.NewId(name, tags), r.writer)
}

func (r *spectatordRegistry) AgeGaugeWithId(id *meter.Id) *meter.AgeGauge {
return meter.NewAgeGauge(id, r.writer)
}

func (r *spectatordRegistry) MaxGauge(name string, tags map[string]string) *meter.MaxGauge {
return meter.NewMaxGauge(r.NewId(name, tags), r.writer)
}
Expand All @@ -159,12 +147,20 @@ func (r *spectatordRegistry) MaxGaugeWithId(id *meter.Id) *meter.MaxGauge {
return meter.NewMaxGauge(id, r.writer)
}

func (r *spectatordRegistry) DistributionSummary(name string, tags map[string]string) *meter.DistributionSummary {
return meter.NewDistributionSummary(r.NewId(name, tags), r.writer)
func (r *spectatordRegistry) MonotonicCounter(name string, tags map[string]string) *meter.MonotonicCounter {
return meter.NewMonotonicCounter(r.NewId(name, tags), r.writer)
}

func (r *spectatordRegistry) DistributionSummaryWithId(id *meter.Id) *meter.DistributionSummary {
return meter.NewDistributionSummary(id, r.writer)
func (r *spectatordRegistry) MonotonicCounterWithId(id *meter.Id) *meter.MonotonicCounter {
return meter.NewMonotonicCounter(id, r.writer)
}

func (r *spectatordRegistry) MonotonicCounterUint(name string, tags map[string]string) *meter.MonotonicCounterUint {
return meter.NewMonotonicCounterUint(r.NewId(name, tags), r.writer)
}

func (r *spectatordRegistry) MonotonicCounterUintWithId(id *meter.Id) *meter.MonotonicCounterUint {
return meter.NewMonotonicCounterUint(id, r.writer)
}

func (r *spectatordRegistry) PercentileDistributionSummary(name string, tags map[string]string) *meter.PercentileDistributionSummary {
Expand All @@ -183,6 +179,14 @@ func (r *spectatordRegistry) PercentileTimerWithId(id *meter.Id) *meter.Percenti
return meter.NewPercentileTimer(id, r.writer)
}

func (r *spectatordRegistry) Timer(name string, tags map[string]string) *meter.Timer {
return meter.NewTimer(r.NewId(name, tags), r.writer)
}

func (r *spectatordRegistry) TimerWithId(id *meter.Id) *meter.Timer {
return meter.NewTimer(id, r.writer)
}

func (r *spectatordRegistry) GetWriter() writer.Writer {
return r.writer
}
Expand Down
60 changes: 36 additions & 24 deletions spectator/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,37 @@ import (
"time"
)

func TestRegistryWithMemoryWriter_Counter(t *testing.T) {
func TestRegistryWithMemoryWriter_AgeGauge(t *testing.T) {
mw := &writer.MemoryWriter{}
r := NewTestRegistry(mw)

counter := r.Counter("test_counter", nil)
counter.Increment()
expected := "c:test_counter:1"
ageGauge := r.AgeGauge("test_age_gauge", nil)
ageGauge.Set(100)
expected := "A:test_age_gauge:100"
if len(mw.Lines()) != 1 || mw.Lines()[0] != expected {
t.Errorf("Expected '%s', got '%s'", expected, mw.Lines()[0])
}
}

func TestRegistryWithMemoryWriter_MonotonicCounter(t *testing.T) {
func TestRegistryWithMemoryWriter_Counter(t *testing.T) {
mw := &writer.MemoryWriter{}
r := NewTestRegistry(mw)

counter := r.MonotonicCounter("test_monotonic_counter", nil)
counter.Set(1)
expected := "C:test_monotonic_counter:1"
counter := r.Counter("test_counter", nil)
counter.Increment()
expected := "c:test_counter:1"
if len(mw.Lines()) != 1 || mw.Lines()[0] != expected {
t.Errorf("Expected '%s', got '%s'", expected, mw.Lines()[0])
}
}

func TestRegistryWithMemoryWriter_Timer(t *testing.T) {
func TestRegistryWithMemoryWriter_DistributionSummary(t *testing.T) {
mw := &writer.MemoryWriter{}
r := NewTestRegistry(mw)

timer := r.Timer("test_timer", nil)
timer.Record(100 * time.Millisecond)
expected := "t:test_timer:0.100000"
distSummary := r.DistributionSummary("test_distributionsummary", nil)
distSummary.Record(300)
expected := "d:test_distributionsummary:300"
if len(mw.Lines()) != 1 || mw.Lines()[0] != expected {
t.Errorf("Expected '%s', got '%s'", expected, mw.Lines()[0])
}
Expand Down Expand Up @@ -70,37 +70,37 @@ func TestRegistryWithMemoryWriter_GaugeWithTTL(t *testing.T) {
}
}

func TestRegistryWithMemoryWriter_AgeGauge(t *testing.T) {
func TestRegistryWithMemoryWriter_MaxGauge(t *testing.T) {
mw := &writer.MemoryWriter{}
r := NewTestRegistry(mw)

ageGauge := r.AgeGauge("test_age_gauge", nil)
ageGauge.Set(100)
expected := "A:test_age_gauge:100"
maxGauge := r.MaxGauge("test_maxgauge", nil)
maxGauge.Set(200)
expected := "m:test_maxgauge:200.000000"
if len(mw.Lines()) != 1 || mw.Lines()[0] != expected {
t.Errorf("Expected '%s', got '%s'", expected, mw.Lines()[0])
}
}

func TestRegistryWithMemoryWriter_MaxGauge(t *testing.T) {
func TestRegistryWithMemoryWriter_MonotonicCounter(t *testing.T) {
mw := &writer.MemoryWriter{}
r := NewTestRegistry(mw)

maxGauge := r.MaxGauge("test_maxgauge", nil)
maxGauge.Set(200)
expected := "m:test_maxgauge:200.000000"
counter := r.MonotonicCounter("test_monotonic_counter", nil)
counter.Set(1)
expected := "C:test_monotonic_counter:1.000000"
if len(mw.Lines()) != 1 || mw.Lines()[0] != expected {
t.Errorf("Expected '%s', got '%s'", expected, mw.Lines()[0])
}
}

func TestRegistryWithMemoryWriter_DistributionSummary(t *testing.T) {
func TestRegistryWithMemoryWriter_MonotonicCounterUint(t *testing.T) {
mw := &writer.MemoryWriter{}
r := NewTestRegistry(mw)

distSummary := r.DistributionSummary("test_distributionsummary", nil)
distSummary.Record(300)
expected := "d:test_distributionsummary:300"
counter := r.MonotonicCounterUint("test_monotonic_counter_uint", nil)
counter.Set(1)
expected := "U:test_monotonic_counter_uint:1"
if len(mw.Lines()) != 1 || mw.Lines()[0] != expected {
t.Errorf("Expected '%s', got '%s'", expected, mw.Lines()[0])
}
Expand Down Expand Up @@ -130,6 +130,18 @@ func TestRegistryWithMemoryWriter_PercentileTimer(t *testing.T) {
}
}

func TestRegistryWithMemoryWriter_Timer(t *testing.T) {
mw := &writer.MemoryWriter{}
r := NewTestRegistry(mw)

timer := r.Timer("test_timer", nil)
timer.Record(100 * time.Millisecond)
expected := "t:test_timer:0.100000"
if len(mw.Lines()) != 1 || mw.Lines()[0] != expected {
t.Errorf("Expected '%s', got '%s'", expected, mw.Lines()[0])
}
}

func TestNewRegistryWithEmptyConfig(t *testing.T) {
_, err := NewRegistry(&Config{})

Expand Down

0 comments on commit 22102a0

Please sign in to comment.