Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ func NewRootScope(opts ScopeOptions, interval time.Duration) (Scope, io.Closer)
return s, s
}

// NewClosableRootScope creates a new root ClosableScope with a set of options and
// a reporting interval.
// Must provide either a StatsReporter or a CachedStatsReporter.
func NewClosableRootScope(opts ScopeOptions, interval time.Duration) (ClosableScope, io.Closer) {
s := newRootScope(opts, interval)
return s, s
}

// NewRootScopeWithDefaultInterval invokes NewRootScope with the default
// reporting interval of 2s.
func NewRootScopeWithDefaultInterval(opts ScopeOptions) (Scope, io.Closer) {
Expand Down Expand Up @@ -442,9 +450,17 @@ func (s *scope) Tagged(tags map[string]string) Scope {
return s.subscope(s.prefix, tags)
}

func (s *scope) TaggedClosable(tags map[string]string) ClosableScope {
return s.subscope(s.prefix, tags).(ClosableScope)
}

func (s *scope) SubScope(prefix string) Scope {
prefix = s.sanitizer.Name(prefix)
return s.subscope(s.fullyQualifiedName(prefix), nil)
return s.subscope(s.fullyQualifiedName(prefix), nil).(ClosableScope)
}

func (s *scope) SubScopeClosable(prefix string) ClosableScope {
return s.subscope(s.fullyQualifiedName(prefix), nil).(ClosableScope)
}

func (s *scope) subscope(prefix string, tags map[string]string) Scope {
Expand Down
173 changes: 173 additions & 0 deletions scope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,179 @@ func TestTaggedExistingReturnsSameScope(t *testing.T) {
}
}

func TestSubScope_TypeAssertionClosableScope(t *testing.T) {
r := newTestStatsReporter()
root, closer := NewRootScope(ScopeOptions{Reporter: r, OmitCardinalityMetrics: true}, time.Hour)

sub := root.SubScope("foo").(ClosableScope)

r.cg.Add(1)
sub.Counter("beep").Inc(1)
r.gg.Add(1)
sub.Gauge("morp").Update(1)
r.tg.Add(1)
sub.Timer("blork").Record(time.Millisecond * 175)
r.hg.Add(1)
sub.Histogram("baz", MustMakeLinearValueBuckets(0, 10, 10)).
RecordValue(42.42)

sub.(*scope).report(r)
r.WaitAll()

counters := r.getCounters()
assert.EqualValues(t, 1, counters["foo.beep"].val)

subtagged := root.Tagged(map[string]string{"foo": "bar"}).(ClosableScope)

r.cg.Add(1)
subtagged.Counter("borp").Inc(1)
r.gg.Add(1)
subtagged.Gauge("dorp").Update(1)
r.tg.Add(1)
subtagged.Timer("blorp").Record(time.Millisecond * 175)
r.hg.Add(1)
subtagged.Histogram("fiz", MustMakeLinearValueBuckets(0, 10, 10)).
RecordValue(42.42)

subtagged.(*scope).report(r)
r.WaitAll()

counters = r.getCounters()
assert.EqualValues(t, 1, counters["borp"].val)

assert.NoError(t, sub.Close())
assert.NoError(t, subtagged.Close())
assert.NoError(t, closer.Close())
}

func TestNewClosableRootScopeCloser(t *testing.T) {
r := newTestStatsReporter()
root, closer := NewClosableRootScope(ScopeOptions{Reporter: r, OmitCardinalityMetrics: true}, time.Hour)

r.cg.Add(1)
root.Counter("foo").Inc(1)

counters := r.getCounters()
assert.Nil(t, counters["foo"])
assert.NoError(t, root.Close())

// Ensure metrics are flushed on close
counters = r.getCounters()
assert.EqualValues(t, 1, counters["foo"].val)
// Ensure that we can close multiple times
// using the closer or the exposed close method
// from ClosableScope
assert.NoError(t, closer.Close())
}

func TestTaggedSubScopeClosable(t *testing.T) {
r := newTestStatsReporter()

ts := map[string]string{"env": "test"}
root, closer := NewClosableRootScope(
ScopeOptions{
Prefix: "foo", Tags: ts, Reporter: r, OmitCardinalityMetrics: true,
}, 0,
)

s := root.(*scope)

tags := map[string]string{"service": "test"}
tscope := root.TaggedClosable(tags).(*scope)
scope := root

r.cg.Add(1)
scope.Counter("beep").Inc(1)
r.cg.Add(1)
tscope.Counter("boop").Inc(1)
r.hg.Add(1)
scope.Histogram("baz", MustMakeLinearValueBuckets(0, 10, 10)).
RecordValue(42.42)
r.hg.Add(1)
tscope.Histogram("bar", MustMakeLinearValueBuckets(0, 10, 10)).
RecordValue(42.42)

s.report(r)
tscope.report(r)
r.cg.Wait()

var (
counters = r.getCounters()
histograms = r.getHistograms()
)

assert.EqualValues(t, 1, counters["foo.beep"].val)
assert.EqualValues(t, ts, counters["foo.beep"].tags)

assert.EqualValues(t, 1, counters["foo.boop"].val)
assert.EqualValues(
t, map[string]string{
"env": "test",
"service": "test",
}, counters["foo.boop"].tags,
)

assert.EqualValues(t, 1, histograms["foo.baz"].valueSamples[50.0])
assert.EqualValues(t, ts, histograms["foo.baz"].tags)

assert.EqualValues(t, 1, histograms["foo.bar"].valueSamples[50.0])
assert.EqualValues(
t, map[string]string{
"env": "test",
"service": "test",
}, histograms["foo.bar"].tags,
)

assert.NoError(t, tscope.Close())
assert.NoError(t, closer.Close())
}

func TestSubScopeClosable(t *testing.T) {
r := newTestStatsReporter()

root, _ := NewClosableRootScope(
ScopeOptions{Prefix: "foo", Reporter: r, OmitCardinalityMetrics: true}, 0,
)

tags := map[string]string{"foo": "bar"}
s := root.TaggedClosable(tags).SubScopeClosable("mork").(*scope)
r.cg.Add(1)
s.Counter("bar").Inc(1)
s.Counter("bar").Inc(20)
r.gg.Add(1)
s.Gauge("zed").Update(1)
r.tg.Add(1)
s.Timer("blork").Record(time.Millisecond * 175)
r.hg.Add(1)
s.Histogram("baz", MustMakeLinearValueBuckets(0, 10, 10)).
RecordValue(42.42)

s.report(r)
r.WaitAll()

var (
counters = r.getCounters()
gauges = r.getGauges()
timers = r.getTimers()
histograms = r.getHistograms()
)

// Assert prefixed correctly
assert.EqualValues(t, 21, counters["foo.mork.bar"].val)
assert.EqualValues(t, 1, gauges["foo.mork.zed"].val)
assert.EqualValues(t, time.Millisecond*175, timers["foo.mork.blork"].val)
assert.EqualValues(t, 1, histograms["foo.mork.baz"].valueSamples[50.0])

// Assert tags inherited
assert.Equal(t, tags, counters["foo.mork.bar"].tags)
assert.Equal(t, tags, gauges["foo.mork.zed"].tags)
assert.Equal(t, tags, timers["foo.mork.blork"].tags)
assert.Equal(t, tags, histograms["foo.mork.baz"].tags)

assert.NoError(t, s.Close())
assert.NoError(t, root.Close())
}

func TestSnapshot(t *testing.T) {
commonTags := map[string]string{"env": "test"}
s := NewTestScope("foo", map[string]string{"env": "test"})
Expand Down
23 changes: 23 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,29 @@ type Scope interface {
Capabilities() Capabilities
}

// ClosableScope extends Scope with the ability to close the scope
// and create closable subscopes.
//
// IMPORTANT: When using Prometheus reporters, users must take care to
// not create metrics from both parent scopes and subscopes
// that have the same metric name but different tag keys,
// as metric allocation will panic.
type ClosableScope interface {
Scope

// TaggedClosable returns a new child scope with the given tags and current tags.
TaggedClosable(tags map[string]string) ClosableScope

// SubScopeClosable returns a new child scope appending a further name prefix.
SubScopeClosable(name string) ClosableScope

// Close closes the scope and releases any resources.
// For root scopes, this will also close the reporter.
// For subscopes, this will clear all metrics and remove the scope from the registry.
// Returns an error if the scope is already closed.
Close() error
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't do this - it's a breaking change to add a method to a go interface in semver terms

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've introduced a new ClosableScope interface to embed Scope with a Close() method

}

// Counter is the interface for emitting counter type metrics.
type Counter interface {
// Inc increments the counter by a delta.
Expand Down