Add metric package containing Gauge support
diff --git a/metric/doc.go b/metric/doc.go
new file mode 100644
index 0000000..485ee8f
--- /dev/null
+++ b/metric/doc.go
@@ -0,0 +1,19 @@
+// Copyright 2018, OpenCensus Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package metric support for gauge metrics.
+//
+// This is an EXPERIMENTAL package, and may change in arbitrary ways without
+// notice.
+package metric // import "go.opencensus.io/metric"
diff --git a/metric/examples_test.go b/metric/examples_test.go
new file mode 100644
index 0000000..b510ce8
--- /dev/null
+++ b/metric/examples_test.go
@@ -0,0 +1,36 @@
+// Copyright 2018, OpenCensus Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metric_test
+
+import (
+ "net/http"
+
+ "go.opencensus.io/metric"
+ "go.opencensus.io/metric/metricdata"
+)
+
+func ExampleRegistry_AddInt64Gauge() {
+ r := metric.NewRegistry()
+ // TODO: allow exporting from a registry
+
+ g := r.AddInt64Gauge("active_request", "Number of active requests, per method.", metricdata.UnitDimensionless, "method")
+
+ http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
+ e := g.GetEntry(metricdata.NewLabelValue(request.Method))
+ e.Add(1)
+ defer e.Add(-1)
+ // process request ...
+ })
+}
diff --git a/metric/gauge.go b/metric/gauge.go
new file mode 100644
index 0000000..c64a9f0
--- /dev/null
+++ b/metric/gauge.go
@@ -0,0 +1,196 @@
+// Copyright 2018, OpenCensus Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metric
+
+import (
+ "math"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "go.opencensus.io/internal/tagencoding"
+ "go.opencensus.io/metric/metricdata"
+)
+
+// gauge represents a quantity that can go up an down, for example queue depth
+// or number of outstanding requests.
+//
+// gauge maintains a value for each combination of of label values passed to
+// the Set or Add methods.
+//
+// gauge should not be used directly, use Float64Gauge or Int64Gauge.
+type gauge struct {
+ vals sync.Map
+ desc metricdata.Descriptor
+ start time.Time
+ keys []string
+ isFloat bool
+}
+
+type gaugeEntry interface {
+ read(t time.Time) metricdata.Point
+}
+
+// Read returns the current values of the gauge as a metric for export.
+func (g *gauge) read() *metricdata.Metric {
+ now := time.Now()
+ m := &metricdata.Metric{
+ Descriptor: g.desc,
+ }
+ g.vals.Range(func(k, v interface{}) bool {
+ entry := v.(gaugeEntry)
+ key := k.(string)
+ labelVals := g.labelValues(key)
+ m.TimeSeries = append(m.TimeSeries, &metricdata.TimeSeries{
+ StartTime: now, // Gauge value is instantaneous.
+ LabelValues: labelVals,
+ Points: []metricdata.Point{
+ entry.read(now),
+ },
+ })
+ return true
+ })
+ return m
+}
+
+func (g *gauge) mapKey(labelVals []metricdata.LabelValue) string {
+ vb := &tagencoding.Values{}
+ for _, v := range labelVals {
+ b := make([]byte, 1, len(v.Value)+1)
+ if v.Present {
+ b[0] = 1
+ b = append(b, []byte(v.Value)...)
+ }
+ vb.WriteValue(b)
+ }
+ return string(vb.Bytes())
+}
+
+func (g *gauge) labelValues(s string) []metricdata.LabelValue {
+ vals := make([]metricdata.LabelValue, 0, len(g.keys))
+ vb := &tagencoding.Values{Buffer: []byte(s)}
+ for range g.keys {
+ v := vb.ReadValue()
+ if v[0] == 0 {
+ vals = append(vals, metricdata.LabelValue{})
+ } else {
+ vals = append(vals, metricdata.NewLabelValue(string(v[1:])))
+ }
+ }
+ return vals
+}
+
+func (g *gauge) entryForValues(labelVals []metricdata.LabelValue, newEntry func() gaugeEntry) interface{} {
+ if len(labelVals) != len(g.keys) {
+ panic("must supply the same number of label values as keys used to construct this gauge")
+ }
+ mapKey := g.mapKey(labelVals)
+ if entry, ok := g.vals.Load(mapKey); ok {
+ return entry
+ }
+ entry, _ := g.vals.LoadOrStore(mapKey, newEntry())
+ return entry
+}
+
+// Float64Gauge represents a float64 value that can go up and down.
+//
+// Float64Gauge maintains a float64 value for each combination of of label values
+// passed to the Set or Add methods.
+type Float64Gauge struct {
+ g gauge
+}
+
+// Float64Entry represents a single value of the gauge corresponding to a set
+// of label values.
+type Float64Entry struct {
+ val uint64 // needs to be uint64 for atomic access, interpret with math.Float64frombits
+}
+
+func (e *Float64Entry) read(t time.Time) metricdata.Point {
+ v := math.Float64frombits(atomic.LoadUint64(&e.val))
+ if v < 0 {
+ v = 0
+ }
+ return metricdata.NewFloat64Point(t, v)
+}
+
+// GetEntry returns a gauge entry where each key for this gauge has the value
+// given.
+//
+// The number of label values supplied must be exactly the same as the number
+// of keys supplied when this gauge was created.
+func (g *Float64Gauge) GetEntry(labelVals ...metricdata.LabelValue) *Float64Entry {
+ return g.g.entryForValues(labelVals, func() gaugeEntry {
+ return &Float64Entry{}
+ }).(*Float64Entry)
+}
+
+// Set sets the gauge entry value to val.
+func (e *Float64Entry) Set(val float64) {
+ atomic.StoreUint64(&e.val, math.Float64bits(val))
+}
+
+// Add increments the gauge entry value by val.
+func (e *Float64Entry) Add(val float64) {
+ var swapped bool
+ for !swapped {
+ oldVal := atomic.LoadUint64(&e.val)
+ newVal := math.Float64bits(math.Float64frombits(oldVal) + val)
+ swapped = atomic.CompareAndSwapUint64(&e.val, oldVal, newVal)
+ }
+}
+
+// Int64Gauge represents a int64 gauge value that can go up and down.
+//
+// Int64Gauge maintains an int64 value for each combination of label values passed to the
+// Set or Add methods.
+type Int64Gauge struct {
+ g gauge
+}
+
+// Int64GaugeEntry represents a single value of the gauge corresponding to a set
+// of label values.
+type Int64GaugeEntry struct {
+ val int64
+}
+
+func (e *Int64GaugeEntry) read(t time.Time) metricdata.Point {
+ v := atomic.LoadInt64(&e.val)
+ if v < 0 {
+ v = 0.0
+ }
+ return metricdata.NewInt64Point(t, v)
+}
+
+// GetEntry returns a gauge entry where each key for this gauge has the value
+// given.
+//
+// The number of label values supplied must be exactly the same as the number
+// of keys supplied when this gauge was created.
+func (g *Int64Gauge) GetEntry(labelVals ...metricdata.LabelValue) *Int64GaugeEntry {
+ return g.g.entryForValues(labelVals, func() gaugeEntry {
+ return &Int64GaugeEntry{}
+ }).(*Int64GaugeEntry)
+}
+
+// Set sets the value of the gauge entry to the provided value.
+func (e *Int64GaugeEntry) Set(val int64) {
+ atomic.StoreInt64(&e.val, val)
+}
+
+// Add increments the current gauge entry value by val, which may be negative.
+func (e *Int64GaugeEntry) Add(val int64) {
+ atomic.AddInt64(&e.val, val)
+}
diff --git a/metric/gauge_test.go b/metric/gauge_test.go
new file mode 100644
index 0000000..e5f8f2e
--- /dev/null
+++ b/metric/gauge_test.go
@@ -0,0 +1,183 @@
+// Copyright 2018, OpenCensus Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metric
+
+import (
+ "fmt"
+ "go.opencensus.io/metric/metricdata"
+ "sort"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func TestGauge(t *testing.T) {
+ r := NewRegistry()
+ f := r.AddFloat64Gauge("TestGauge", "", "", "k1", "k2")
+ f.GetEntry(metricdata.LabelValue{}, metricdata.LabelValue{}).Set(5)
+ f.GetEntry(metricdata.NewLabelValue("k1v1"), metricdata.LabelValue{}).Add(1)
+ f.GetEntry(metricdata.NewLabelValue("k1v1"), metricdata.LabelValue{}).Add(1)
+ f.GetEntry(metricdata.NewLabelValue("k1v2"), metricdata.NewLabelValue("k2v2")).Add(1)
+ m := r.ReadAll()
+ want := []*metricdata.Metric{
+ {
+ Descriptor: metricdata.Descriptor{
+ Name: "TestGauge",
+ LabelKeys: []string{"k1", "k2"},
+ },
+ TimeSeries: []*metricdata.TimeSeries{
+ {
+ LabelValues: []metricdata.LabelValue{
+ {}, {},
+ },
+ Points: []metricdata.Point{
+ metricdata.NewFloat64Point(time.Time{}, 5),
+ },
+ },
+ {
+ LabelValues: []metricdata.LabelValue{
+ metricdata.NewLabelValue("k1v1"),
+ {},
+ },
+ Points: []metricdata.Point{
+ metricdata.NewFloat64Point(time.Time{}, 2),
+ },
+ },
+ {
+ LabelValues: []metricdata.LabelValue{
+ metricdata.NewLabelValue("k1v2"),
+ metricdata.NewLabelValue("k2v2"),
+ },
+ Points: []metricdata.Point{
+ metricdata.NewFloat64Point(time.Time{}, 1),
+ },
+ },
+ },
+ },
+ }
+ canonicalize(m)
+ canonicalize(want)
+ if diff := cmp.Diff(m, want, cmp.Comparer(ignoreTimes)); diff != "" {
+ t.Errorf("-got +want: %s", diff)
+ }
+}
+
+func TestFloat64Entry_Add(t *testing.T) {
+ r := NewRegistry()
+ g := r.AddFloat64Gauge("g", "", metricdata.UnitDimensionless)
+ g.GetEntry().Add(0)
+ ms := r.ReadAll()
+ if got, want := ms[0].TimeSeries[0].Points[0].Value.(float64), 0.0; got != want {
+ t.Errorf("value = %v, want %v", got, want)
+ }
+ g.GetEntry().Add(1)
+ ms = r.ReadAll()
+ if got, want := ms[0].TimeSeries[0].Points[0].Value.(float64), 1.0; got != want {
+ t.Errorf("value = %v, want %v", got, want)
+ }
+ g.GetEntry().Add(-1)
+ ms = r.ReadAll()
+ if got, want := ms[0].TimeSeries[0].Points[0].Value.(float64), 0.0; got != want {
+ t.Errorf("value = %v, want %v", got, want)
+ }
+}
+
+func TestFloat64Gauge_Add_NegativeTotals(t *testing.T) {
+ r := NewRegistry()
+ g := r.AddFloat64Gauge("g", "", metricdata.UnitDimensionless)
+ g.GetEntry().Add(-1.0)
+ ms := r.ReadAll()
+ if got, want := ms[0].TimeSeries[0].Points[0].Value.(float64), float64(0); got != want {
+ t.Errorf("value = %v, want %v", got, want)
+ }
+}
+
+func TestInt64GaugeEntry_Add(t *testing.T) {
+ r := NewRegistry()
+ g := r.AddInt64Gauge("g", "", metricdata.UnitDimensionless)
+ g.GetEntry().Add(0)
+ ms := r.ReadAll()
+ if got, want := ms[0].TimeSeries[0].Points[0].Value.(int64), int64(0); got != want {
+ t.Errorf("value = %v, want %v", got, want)
+ }
+ g.GetEntry().Add(1)
+ ms = r.ReadAll()
+ if got, want := ms[0].TimeSeries[0].Points[0].Value.(int64), int64(1); got != want {
+ t.Errorf("value = %v, want %v", got, want)
+ }
+}
+
+func TestInt64Gauge_Add_NegativeTotals(t *testing.T) {
+ r := NewRegistry()
+ g := r.AddInt64Gauge("g", "", metricdata.UnitDimensionless)
+ g.GetEntry().Add(-1)
+ ms := r.ReadAll()
+ if got, want := ms[0].TimeSeries[0].Points[0].Value.(int64), int64(0); got != want {
+ t.Errorf("value = %v, want %v", got, want)
+ }
+}
+
+func TestMapKey(t *testing.T) {
+ cases := [][]metricdata.LabelValue{
+ {},
+ {metricdata.LabelValue{}},
+ {metricdata.NewLabelValue("")},
+ {metricdata.NewLabelValue("-")},
+ {metricdata.NewLabelValue(",")},
+ {metricdata.NewLabelValue("v1"), metricdata.NewLabelValue("v2")},
+ {metricdata.NewLabelValue("v1"), metricdata.LabelValue{}},
+ {metricdata.NewLabelValue("v1"), metricdata.LabelValue{}, metricdata.NewLabelValue(string([]byte{0}))},
+ {metricdata.LabelValue{}, metricdata.LabelValue{}},
+ }
+ for i, tc := range cases {
+ t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
+ g := &gauge{
+ keys: make([]string, len(tc)),
+ }
+ mk := g.mapKey(tc)
+ vals := g.labelValues(mk)
+ if diff := cmp.Diff(vals, tc); diff != "" {
+ t.Errorf("values differ after serialization -got +want: %s", diff)
+ }
+ })
+ }
+}
+
+func ignoreTimes(_, _ time.Time) bool {
+ return true
+}
+
+func canonicalize(ms []*metricdata.Metric) {
+ for _, m := range ms {
+ sort.Slice(m.TimeSeries, func(i, j int) bool {
+ // sort time series by their label values
+ iLabels := m.TimeSeries[i].LabelValues
+ jLabels := m.TimeSeries[j].LabelValues
+ for k := 0; k < len(iLabels); k++ {
+ if !iLabels[k].Present {
+ if jLabels[k].Present {
+ return true
+ }
+ } else if !jLabels[k].Present {
+ return false
+ } else {
+ return iLabels[k].Value < jLabels[k].Value
+ }
+ }
+ panic("should have returned")
+ })
+ }
+}
diff --git a/metric/metricdata/unit.go b/metric/metricdata/unit.go
index 72887d2..b483a13 100644
--- a/metric/metricdata/unit.go
+++ b/metric/metricdata/unit.go
@@ -18,6 +18,8 @@
// Unified Code for Units of Measure: http://unitsofmeasure.org/ucum.html
type Unit string
+// Predefined units. To record against a unit not represented here, create your
+// own Unit type constant from a string.
const (
UnitDimensionless Unit = "1"
UnitBytes Unit = "By"
diff --git a/metric/metricexport/producer.go b/metric/metricexport/producer.go
index 0b651f7..077b9fc 100644
--- a/metric/metricexport/producer.go
+++ b/metric/metricexport/producer.go
@@ -16,8 +16,6 @@
import (
"go.opencensus.io/metric/metricdata"
- "sync"
- "sync/atomic"
)
// Producer is a source of metrics.
@@ -28,73 +26,3 @@
// resource.
Read() []*metricdata.Metric
}
-
-// Registry maintains a set of metric producers for exporting. Most users will
-// rely on the DefaultRegistry.
-type Registry struct {
- mu sync.RWMutex
- state atomic.Value
-}
-
-type registryState struct {
- producers map[Producer]struct{}
-}
-
-// NewRegistry creates a new Registry.
-func NewRegistry() *Registry {
- m := &Registry{}
- m.state.Store(®istryState{
- producers: make(map[Producer]struct{}),
- })
- return m
-}
-
-// Read returns all the metrics from all the metric produces in this registry.
-func (m *Registry) ReadAll() []*metricdata.Metric {
- s := m.state.Load().(*registryState)
- ms := make([]*metricdata.Metric, 0, len(s.producers))
- for p := range s.producers {
- ms = append(ms, p.Read()...)
- }
- return ms
-}
-
-// AddProducer adds a producer to this registry.
-func (m *Registry) AddProducer(p Producer) {
- m.mu.Lock()
- defer m.mu.Unlock()
- newState := ®istryState{
- make(map[Producer]struct{}),
- }
- state := m.state.Load().(*registryState)
- for producer := range state.producers {
- newState.producers[producer] = struct{}{}
- }
- newState.producers[p] = struct{}{}
- m.state.Store(newState)
-}
-
-// RemoveProducer removes the given producer from this registry.
-func (m *Registry) RemoveProducer(p Producer) {
- m.mu.Lock()
- defer m.mu.Unlock()
- newState := ®istryState{
- make(map[Producer]struct{}),
- }
- state := m.state.Load().(*registryState)
- for producer := range state.producers {
- newState.producers[producer] = struct{}{}
- }
- delete(newState.producers, p)
- m.state.Store(newState)
-}
-
-var defaultReg = NewRegistry()
-
-// DefaultRegistry returns the default, global metric registry for the current
-// process.
-// Most applications will rely on this registry but libraries should not assume
-// the default registry is used.
-func DefaultRegistry() *Registry {
- return defaultReg
-}
diff --git a/metric/metricexport/producer_test.go b/metric/metricexport/producer_test.go
deleted file mode 100644
index 7aa653f..0000000
--- a/metric/metricexport/producer_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2018, OpenCensus Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package metricexport
-
-import (
- "go.opencensus.io/metric/metricdata"
- "testing"
-)
-
-func TestRegistry_AddProducer(t *testing.T) {
- r := NewRegistry()
- m1 := &metricdata.Metric{
- Descriptor: metricdata.Descriptor{
- Name: "test",
- Unit: metricdata.UnitDimensionless,
- },
- }
- p := &constProducer{m1}
- r.AddProducer(p)
- if got, want := len(r.ReadAll()), 1; got != want {
- t.Fatal("Expected to read a single metric")
- }
- r.RemoveProducer(p)
- if got, want := len(r.ReadAll()), 0; got != want {
- t.Fatal("Expected to read no metrics")
- }
-}
-
-type constProducer []*metricdata.Metric
-
-func (cp constProducer) Read() []*metricdata.Metric {
- return cp
-}
diff --git a/metric/registry.go b/metric/registry.go
new file mode 100644
index 0000000..ac39e42
--- /dev/null
+++ b/metric/registry.go
@@ -0,0 +1,81 @@
+// Copyright 2018, OpenCensus Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metric
+
+import (
+ "go.opencensus.io/metric/metricdata"
+ "log"
+ "time"
+)
+
+// Registry creates and manages a set of gauges.
+// External synchronization is required if you want to add gauges to the same
+// registry from multiple goroutines.
+type Registry struct {
+ gauges map[string]*gauge
+}
+
+// NewRegistry initializes a new Registry.
+func NewRegistry() *Registry {
+ return &Registry{
+ gauges: make(map[string]*gauge),
+ }
+}
+
+// AddFloat64Gauge creates and adds a new float64-valued gauge to this registry.
+func (r *Registry) AddFloat64Gauge(name, description string, unit metricdata.Unit, labelKeys ...string) *Float64Gauge {
+ f := &Float64Gauge{
+ g: gauge{
+ isFloat: true,
+ },
+ }
+ r.initGauge(&f.g, labelKeys, name, description, unit)
+ return f
+}
+
+// AddInt64Gauge creates and adds a new int64-valued gauge to this registry.
+func (r *Registry) AddInt64Gauge(name, description string, unit metricdata.Unit, labelKeys ...string) *Int64Gauge {
+ i := &Int64Gauge{}
+ r.initGauge(&i.g, labelKeys, name, description, unit)
+ return i
+}
+
+func (r *Registry) initGauge(g *gauge, labelKeys []string, name string, description string, unit metricdata.Unit) *gauge {
+ existing, ok := r.gauges[name]
+ if ok {
+ if existing.isFloat != g.isFloat {
+ log.Panicf("Gauge with name %s already exists with a different type", name)
+ }
+ }
+ g.keys = labelKeys
+ g.start = time.Now()
+ g.desc = metricdata.Descriptor{
+ Name: name,
+ Description: description,
+ Unit: unit,
+ LabelKeys: labelKeys,
+ }
+ r.gauges[name] = g
+ return g
+}
+
+// ReadAll reads all gauges in this registry and returns their values as metrics.
+func (r *Registry) ReadAll() []*metricdata.Metric {
+ ms := make([]*metricdata.Metric, 0, len(r.gauges))
+ for _, g := range r.gauges {
+ ms = append(ms, g.read())
+ }
+ return ms
+}