{lib,services}/stats: expose time series values from counter.
Change-Id: I5b5622488a525377eee1a350af9b92096419d072
diff --git a/lib/stats/counter.go b/lib/stats/counter.go
index 6482149..f4646fa 100644
--- a/lib/stats/counter.go
+++ b/lib/stats/counter.go
@@ -27,6 +27,9 @@
addCounterChild(node, name+"/rate1h", cw, time.Hour, cw.Rate1h)
addCounterChild(node, name+"/rate10m", cw, 10*time.Minute, cw.Rate10m)
addCounterChild(node, name+"/rate1m", cw, time.Minute, cw.Rate1m)
+ addCounterChild(node, name+"/timeseries1h", cw, time.Hour, cw.TimeSeries1h)
+ addCounterChild(node, name+"/timeseries10m", cw, 10*time.Minute, cw.TimeSeries10m)
+ addCounterChild(node, name+"/timeseries1m", cw, time.Minute, cw.TimeSeries1m)
return c
}
@@ -60,6 +63,15 @@
func (cw counterWrapper) Rate1m() interface{} {
return cw.c.Rate1m()
}
+func (cw counterWrapper) TimeSeries1h() interface{} {
+ return cw.c.TimeSeries1h()
+}
+func (cw counterWrapper) TimeSeries10m() interface{} {
+ return cw.c.TimeSeries10m()
+}
+func (cw counterWrapper) TimeSeries1m() interface{} {
+ return cw.c.TimeSeries1m()
+}
type counterChild struct {
c *counterWrapper
diff --git a/lib/stats/counter/counter.go b/lib/stats/counter/counter.go
index 947e1b7..f99b3d3 100644
--- a/lib/stats/counter/counter.go
+++ b/lib/stats/counter/counter.go
@@ -20,6 +20,8 @@
import (
"sync"
"time"
+
+ "v.io/x/ref/services/stats"
)
var (
@@ -141,6 +143,31 @@
return c.ts[minute].rate()
}
+// TimeSeries1h returns the time series data in the last hour.
+func (c *Counter) TimeSeries1h() stats.TimeSeries {
+ return c.timeseries(c.ts[hour])
+}
+
+// TimeSeries10m returns the time series data in the last 10 minutes.
+func (c *Counter) TimeSeries10m() stats.TimeSeries {
+ return c.timeseries(c.ts[tenminutes])
+}
+
+// TimeSeries1m returns the time series data in the last minute.
+func (c *Counter) TimeSeries1m() stats.TimeSeries {
+ return c.timeseries(c.ts[minute])
+}
+
+func (c *Counter) timeseries(ts *timeseries) stats.TimeSeries {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+ return stats.TimeSeries{
+ Values: ts.values(),
+ Resolution: ts.resolution,
+ StartTime: ts.time,
+ }
+}
+
// Reset resets the counter to an empty state.
func (c *Counter) Reset() {
c.mu.Lock()
diff --git a/lib/stats/counter/timeseries.go b/lib/stats/counter/timeseries.go
index 36dfbd8..3d46b5a 100644
--- a/lib/stats/counter/timeseries.go
+++ b/lib/stats/counter/timeseries.go
@@ -156,3 +156,19 @@
ts.stepCount = 1
ts.slots = make([]int64, ts.size)
}
+
+// values returns timeseries values from oldest (tail) to newest (head).
+func (ts *timeseries) values() []int64 {
+ tail := 0
+ steps := ts.head + 1
+ if ts.stepCount > int64(ts.size) {
+ tail = (ts.head + 1) % ts.size
+ steps = ts.size
+ }
+ values := make([]int64, 0, steps)
+ for c := 0; c < steps; c++ {
+ i := (tail + c) % ts.size
+ values = append(values, ts.slots[i])
+ }
+ return values
+}
diff --git a/lib/stats/counter/timeseries_test.go b/lib/stats/counter/timeseries_test.go
index 8bd3bbd..a6eca0a 100644
--- a/lib/stats/counter/timeseries_test.go
+++ b/lib/stats/counter/timeseries_test.go
@@ -5,6 +5,7 @@
package counter
import (
+ "reflect"
"testing"
"time"
)
@@ -121,3 +122,39 @@
}
}
+
+func TestTimeSeriesValues(t *testing.T) {
+ now := time.Unix(1, 0)
+ // 6 time slots.
+ ts := newTimeSeries(now, 5*time.Second, time.Second)
+ // Add 3 values.
+ // slots: [0, 1, 2, 3, x, x]
+ // values: [0, 1, 2, 3]
+ now = addValue(1, 3, ts, now)
+ if expected, got := []int64{0, 1, 2, 3}, ts.values(); !reflect.DeepEqual(got, expected) {
+ t.Errorf("unexpected values. Got %v, want %v", got, expected)
+ }
+ // Add 2 more values.
+ // slots: [0, 1, 2, 3, 4, 5]
+ // values: [0, 1, 2, 3, 4, 5]
+ now = addValue(1, 2, ts, now)
+ if expected, got := []int64{0, 1, 2, 3, 4, 5}, ts.values(); !reflect.DeepEqual(got, expected) {
+ t.Errorf("unexpected values. Got %v, want %v", got, expected)
+ }
+ // Add 3 more values.
+ // slots: [6, 7, 8, 3, 4, 5]
+ // values: [3, 4, 5, 6, 7, 8]
+ now = addValue(1, 3, ts, now)
+ if expected, got := []int64{3, 4, 5, 6, 7, 8}, ts.values(); !reflect.DeepEqual(got, expected) {
+ t.Errorf("unexpected values. Got %v, want %v", got, expected)
+ }
+}
+
+func addValue(increment int64, count int, ts *timeseries, now time.Time) time.Time {
+ for i := 0; i < count; i++ {
+ now = now.Add(time.Second)
+ ts.advanceTime(now)
+ ts.incr(increment)
+ }
+ return now
+}
diff --git a/lib/stats/stats_test.go b/lib/stats/stats_test.go
index 4a9cfbd..fc67406 100644
--- a/lib/stats/stats_test.go
+++ b/lib/stats/stats_test.go
@@ -109,6 +109,21 @@
libstats.KeyValue{Key: "rpc/test/ddd/rate10m", Value: float64(0)},
libstats.KeyValue{Key: "rpc/test/ddd/rate1h", Value: float64(0)},
libstats.KeyValue{Key: "rpc/test/ddd/rate1m", Value: float64(0)},
+ libstats.KeyValue{Key: "rpc/test/ddd/timeseries10m", Value: s_stats.TimeSeries{
+ Values: []int64{4},
+ Resolution: 10 * time.Second,
+ StartTime: now.Truncate(10 * time.Second),
+ }},
+ libstats.KeyValue{Key: "rpc/test/ddd/timeseries1h", Value: s_stats.TimeSeries{
+ Values: []int64{4},
+ Resolution: time.Minute,
+ StartTime: now.Truncate(time.Minute),
+ }},
+ libstats.KeyValue{Key: "rpc/test/ddd/timeseries1m", Value: s_stats.TimeSeries{
+ Values: []int64{4},
+ Resolution: time.Second,
+ StartTime: now,
+ }},
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("unexpected result. Got %#v, want %#v", result, expected)
@@ -146,6 +161,9 @@
libstats.KeyValue{Key: "rpc/test/ddd/rate10m"},
libstats.KeyValue{Key: "rpc/test/ddd/rate1h"},
libstats.KeyValue{Key: "rpc/test/ddd/rate1m"},
+ libstats.KeyValue{Key: "rpc/test/ddd/timeseries10m"},
+ libstats.KeyValue{Key: "rpc/test/ddd/timeseries1h"},
+ libstats.KeyValue{Key: "rpc/test/ddd/timeseries1m"},
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("unexpected result. Got %#v, want %#v", result, expected)
@@ -165,6 +183,21 @@
libstats.KeyValue{Key: "rpc/test/ddd/rate10m", Value: float64(10.4)},
libstats.KeyValue{Key: "rpc/test/ddd/rate1h", Value: float64(0)},
libstats.KeyValue{Key: "rpc/test/ddd/rate1m", Value: float64(10.4)},
+ libstats.KeyValue{Key: "rpc/test/ddd/timeseries10m", Value: s_stats.TimeSeries{
+ Values: []int64{4, 104},
+ Resolution: 10 * time.Second,
+ StartTime: now.Truncate(10 * time.Second),
+ }},
+ libstats.KeyValue{Key: "rpc/test/ddd/timeseries1h", Value: s_stats.TimeSeries{
+ Values: []int64{104},
+ Resolution: time.Minute,
+ StartTime: now.Truncate(time.Minute),
+ }},
+ libstats.KeyValue{Key: "rpc/test/ddd/timeseries1m", Value: s_stats.TimeSeries{
+ Values: []int64{4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 104},
+ Resolution: time.Second,
+ StartTime: now,
+ }},
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("unexpected result. Got %#v, want %#v", result, expected)
diff --git a/services/internal/statslib/stats_test.go b/services/internal/statslib/stats_test.go
index 3277fd7..4ef177e 100644
--- a/services/internal/statslib/stats_test.go
+++ b/services/internal/statslib/stats_test.go
@@ -76,6 +76,9 @@
"testing/foo/bar/rate10m",
"testing/foo/bar/rate1h",
"testing/foo/bar/rate1m",
+ "testing/foo/bar/timeseries10m",
+ "testing/foo/bar/timeseries1h",
+ "testing/foo/bar/timeseries1m",
}
sort.Strings(results)
sort.Strings(expected)
diff --git a/services/stats/stats.vdl.go b/services/stats/stats.vdl.go
index 725c16e..606eeec 100644
--- a/services/stats/stats.vdl.go
+++ b/services/stats/stats.vdl.go
@@ -9,7 +9,9 @@
package stats
import (
+ "time"
"v.io/v23/vdl"
+ vdltime "v.io/v23/vdlroot/time"
)
var _ = __VDLInit() // Must be first; see __VDLInit comments for details.
@@ -278,11 +280,172 @@
}
}
+// TimeSeries records data of a single time series.
+type TimeSeries struct {
+ // Values holds the time series values (from oldest to newest).
+ Values []int64
+ // Resolution is the time resolution of the time series.
+ Resolution time.Duration
+ // StartTime is the time of the first value of the time series.
+ StartTime time.Time
+}
+
+func (TimeSeries) __VDLReflect(struct {
+ Name string `vdl:"v.io/x/ref/services/stats.TimeSeries"`
+}) {
+}
+
+func (x TimeSeries) VDLIsZero() bool {
+ if len(x.Values) != 0 {
+ return false
+ }
+ if x.Resolution != 0 {
+ return false
+ }
+ if !x.StartTime.IsZero() {
+ return false
+ }
+ return true
+}
+
+func (x TimeSeries) VDLWrite(enc vdl.Encoder) error {
+ if err := enc.StartValue(__VDLType_struct_4); err != nil {
+ return err
+ }
+ if len(x.Values) != 0 {
+ if err := enc.NextField(0); err != nil {
+ return err
+ }
+ if err := __VDLWriteAnon_list_2(enc, x.Values); err != nil {
+ return err
+ }
+ }
+ if x.Resolution != 0 {
+ if err := enc.NextField(1); err != nil {
+ return err
+ }
+ var wire vdltime.Duration
+ if err := vdltime.DurationFromNative(&wire, x.Resolution); err != nil {
+ return err
+ }
+ if err := wire.VDLWrite(enc); err != nil {
+ return err
+ }
+ }
+ if !x.StartTime.IsZero() {
+ if err := enc.NextField(2); err != nil {
+ return err
+ }
+ var wire vdltime.Time
+ if err := vdltime.TimeFromNative(&wire, x.StartTime); err != nil {
+ return err
+ }
+ if err := wire.VDLWrite(enc); err != nil {
+ return err
+ }
+ }
+ if err := enc.NextField(-1); err != nil {
+ return err
+ }
+ return enc.FinishValue()
+}
+
+func __VDLWriteAnon_list_2(enc vdl.Encoder, x []int64) error {
+ if err := enc.StartValue(__VDLType_list_5); err != nil {
+ return err
+ }
+ if err := enc.SetLenHint(len(x)); err != nil {
+ return err
+ }
+ for _, elem := range x {
+ if err := enc.NextEntryValueInt(vdl.Int64Type, elem); err != nil {
+ return err
+ }
+ }
+ if err := enc.NextEntry(true); err != nil {
+ return err
+ }
+ return enc.FinishValue()
+}
+
+func (x *TimeSeries) VDLRead(dec vdl.Decoder) error {
+ *x = TimeSeries{}
+ if err := dec.StartValue(__VDLType_struct_4); err != nil {
+ return err
+ }
+ decType := dec.Type()
+ for {
+ index, err := dec.NextField()
+ switch {
+ case err != nil:
+ return err
+ case index == -1:
+ return dec.FinishValue()
+ }
+ if decType != __VDLType_struct_4 {
+ index = __VDLType_struct_4.FieldIndexByName(decType.Field(index).Name)
+ if index == -1 {
+ if err := dec.SkipValue(); err != nil {
+ return err
+ }
+ continue
+ }
+ }
+ switch index {
+ case 0:
+ if err := __VDLReadAnon_list_2(dec, &x.Values); err != nil {
+ return err
+ }
+ case 1:
+ var wire vdltime.Duration
+ if err := wire.VDLRead(dec); err != nil {
+ return err
+ }
+ if err := vdltime.DurationToNative(wire, &x.Resolution); err != nil {
+ return err
+ }
+ case 2:
+ var wire vdltime.Time
+ if err := wire.VDLRead(dec); err != nil {
+ return err
+ }
+ if err := vdltime.TimeToNative(wire, &x.StartTime); err != nil {
+ return err
+ }
+ }
+ }
+}
+
+func __VDLReadAnon_list_2(dec vdl.Decoder, x *[]int64) error {
+ if err := dec.StartValue(__VDLType_list_5); err != nil {
+ return err
+ }
+ if len := dec.LenHint(); len > 0 {
+ *x = make([]int64, 0, len)
+ } else {
+ *x = nil
+ }
+ for {
+ switch done, elem, err := dec.NextEntryValueInt(64); {
+ case err != nil:
+ return err
+ case done:
+ return dec.FinishValue()
+ default:
+ *x = append(*x, elem)
+ }
+ }
+}
+
// Hold type definitions in package-level variables, for better performance.
var (
__VDLType_struct_1 *vdl.Type
__VDLType_struct_2 *vdl.Type
__VDLType_list_3 *vdl.Type
+ __VDLType_struct_4 *vdl.Type
+ __VDLType_list_5 *vdl.Type
+ __VDLType_struct_6 *vdl.Type
+ __VDLType_struct_7 *vdl.Type
)
var __VDLInitCalled bool
@@ -309,11 +472,16 @@
// Register types.
vdl.Register((*HistogramBucket)(nil))
vdl.Register((*HistogramValue)(nil))
+ vdl.Register((*TimeSeries)(nil))
// Initialize type definitions.
__VDLType_struct_1 = vdl.TypeOf((*HistogramBucket)(nil)).Elem()
__VDLType_struct_2 = vdl.TypeOf((*HistogramValue)(nil)).Elem()
__VDLType_list_3 = vdl.TypeOf((*[]HistogramBucket)(nil))
+ __VDLType_struct_4 = vdl.TypeOf((*TimeSeries)(nil)).Elem()
+ __VDLType_list_5 = vdl.TypeOf((*[]int64)(nil))
+ __VDLType_struct_6 = vdl.TypeOf((*vdltime.Duration)(nil)).Elem()
+ __VDLType_struct_7 = vdl.TypeOf((*vdltime.Time)(nil)).Elem()
return struct{}{}
}
diff --git a/services/stats/types.vdl b/services/stats/types.vdl
index 1835f80..1243f99 100644
--- a/services/stats/types.vdl
+++ b/services/stats/types.vdl
@@ -5,6 +5,8 @@
// Packages stats defines the non-native types exported by the stats service.
package stats
+import "time"
+
// HistogramValue is the value of Histogram objects.
type HistogramValue struct {
// Count is the total number of values added to the histogram.
@@ -26,3 +28,13 @@
// Count is the number of values in the bucket.
Count int64
}
+
+// TimeSeries records data of a single time series.
+type TimeSeries struct {
+ // Values holds the time series values (from oldest to newest).
+ Values []int64
+ // Resolution is the time resolution of the time series.
+ Resolution time.Duration
+ // StartTime is the time of the first value of the time series.
+ StartTime time.Time
+}