diff --git a/core/metrics/register.go b/core/metrics/register.go new file mode 100644 index 0000000000..2fbb74bb29 --- /dev/null +++ b/core/metrics/register.go @@ -0,0 +1,70 @@ +package metrics + +import ( + "fmt" + "sync" + + "go.opencensus.io/stats/view" +) + +var registeredViews = map[string][]*view.View{} +var mu = new(sync.Mutex) + +type ErrNamespace struct { + Namespace string +} + +// ErrUnregisteredNamespace is an error for lookup of requested unregistered Namespace +type ErrUnregisteredNamespace ErrNamespace + +func (e ErrUnregisteredNamespace) Error() string { + return fmt.Sprintf("no views found registered under Namespace %s", e.Namespace) +} + +// ErrDuplicateNamespaceRegistration is an error for a Namespace that has already +// registered views +type ErrDuplicateNamespaceRegistration ErrNamespace + +func (e ErrDuplicateNamespaceRegistration) Error() string { + return fmt.Sprintf("duplicate registration of views by Namespace %s", e.Namespace) +} + +// RegisterViews accepts a namespace and a slice of Views, which will be registered +// with opencensus and maintained in the global registered views map +func RegisterViews(namespace string, views ...*view.View) error { + mu.Lock() + defer mu.Unlock() + _, ok := registeredViews[namespace] + if ok { + return ErrDuplicateNamespaceRegistration{Namespace: namespace} + } else { + registeredViews[namespace] = views + } + + return nil +} + +// LookupViews returns all views for a Namespace name. Returns an error if the +// Namespace has not been registered. +func LookupViews(name string) ([]*view.View, error) { + mu.Lock() + defer mu.Unlock() + views, ok := registeredViews[name] + if !ok { + return nil, ErrUnregisteredNamespace{Namespace: name} + } + response := make([]*view.View, len(views)) + copy(response, views) + return response, nil +} + +// AllViews returns all registered views as a single slice +func AllViews() []*view.View { + var views []*view.View + mu.Lock() + defer mu.Unlock() + for _, vs := range registeredViews { + views = append(views, vs...) + } + return views +} diff --git a/core/metrics/register_test.go b/core/metrics/register_test.go new file mode 100644 index 0000000000..0f4e57ebc1 --- /dev/null +++ b/core/metrics/register_test.go @@ -0,0 +1,94 @@ +package metrics + +import ( + "fmt" + "go.opencensus.io/stats" + "testing" + + "go.opencensus.io/stats/view" +) + +func newTestMeasure(name string) stats.Measure { + return stats.Int64(fmt.Sprintf("test/measure/%s", name), + fmt.Sprintf("Test measurement %s", name), + stats.UnitDimensionless, + ) +} + +func newTestView(name string) *view.View { + return &view.View{ + Name: fmt.Sprintf("test/%s", name), + Description: fmt.Sprintf("Test view %s", name), + Measure: newTestMeasure(name), + Aggregation: view.LastValue(), + } +} + +func TestRegisteringViews(t *testing.T) { + registeredViews = make(map[string][]*view.View) + + t.Run("test registering first views", func(t *testing.T) { + testView := newTestView("empty-map-0") + + if err := RegisterViews("test", testView); err != nil { + t.Fatal("unable to register view in empty map", err) + } + }) + + t.Run("test registering with existing views", func(t *testing.T) { + testView := newTestView("empty-map-1") + testView2 := newTestView("existing-entity-0") + + if err := RegisterViews("test2", testView); err != nil { + t.Fatal("unable to register view in empty map", err) + } + if err := RegisterViews("test3", testView2); err != nil { + t.Fatal("unable to register view in map", err) + } + }) + + t.Run("test registering duplicate views", func(t *testing.T) { + testView := newTestView("empty-map-2") + testView2 := newTestView("existing-entity-1") + + if err := RegisterViews("test4", testView); err != nil { + t.Fatal("unable to register view in empty map", err) + } + if err := RegisterViews("test4", testView2); err == nil { + t.Fatal("allowed duplicate view registration") + } + }) + + t.Run("test looking up views", func(t *testing.T) { + testView := newTestView("empty-map-3") + + if err := RegisterViews("test5", testView); err != nil { + t.Fatal("unable to register view in empty map", err) + } + + views, err := LookupViews("test5") + if err != nil { + t.Fatal("error looking up views", err) + } + + if views[0].Name != testView.Name { + t.Fatal("incorrect view lookup, received name:", views[0].Name) + } + }) +} + +func TestAllViews(t *testing.T) { + registeredViews = make(map[string][]*view.View) + t.Run("test retrieving all views", func(t *testing.T) { + views := []*view.View{newTestView("all-views-0"), newTestView("all-views-1"), newTestView("all-views-2")} + + if err := RegisterViews("test6", views...); err != nil { + t.Fatal("unable to register multiple views at once", err) + } + + allViews := AllViews() + if len(allViews) != len(views) { + t.Fatalf("didn't receive equal number of views: %d %d", len(views), len(allViews)) + } + }) +}