From 3cac55e5d3cb823cff882976d2892dda83659a3a Mon Sep 17 00:00:00 2001 From: grapebaba <281165273@qq.com> Date: Wed, 26 Jul 2017 17:36:54 +0800 Subject: [PATCH] [FAB-5465]Init common metrics module First commit for fabric metrics module, all exposed interfaces are defined in types.go, currently only one implemetation based uber-go/tally library. For consumer, First need use IsEnabled() method to verify if metrics module enabled, if yes get root metrics scope instance using NewRootScope() method and then define self sub scope and metrics. Record metrics value in code after defined. Close() method need be invoked when peer/orderer service stop if metrics module enabled. A example below func reportXXX(){ if metrics.IsEnabled() { metrics.NewRootScope() .SubScope('XX') .Counter('XXX') .Inc(1) } } func useXXXreport(){ if success { reportXXX() } } Change-Id: I3860a16b22321970eb51aeedb764af769e55ee73 Signed-off-by: grapebaba <281165273@qq.com> --- common/metrics/service.go | 60 ++ common/metrics/service_test.go | 43 ++ common/metrics/tally_provider.go | 219 ++++++ common/metrics/tally_provider_test.go | 342 ++++++++ common/metrics/types.go | 35 + vendor/github.com/facebookgo/clock/LICENSE | 21 + vendor/github.com/facebookgo/clock/README.md | 104 +++ vendor/github.com/facebookgo/clock/clock.go | 363 +++++++++ vendor/github.com/uber-go/tally/LICENSE | 21 + vendor/github.com/uber-go/tally/Makefile | 80 ++ vendor/github.com/uber-go/tally/README.md | 207 +++++ .../github.com/uber-go/tally/check_license.sh | 4 + vendor/github.com/uber-go/tally/glide.lock | 78 ++ vendor/github.com/uber-go/tally/glide.yaml | 44 ++ vendor/github.com/uber-go/tally/histogram.go | 318 ++++++++ vendor/github.com/uber-go/tally/key_gen.go | 111 +++ vendor/github.com/uber-go/tally/pool.go | 63 ++ .../uber-go/tally/prometheus/README.md | 118 +++ .../uber-go/tally/prometheus/reporter.go | 582 ++++++++++++++ vendor/github.com/uber-go/tally/reporter.go | 140 ++++ vendor/github.com/uber-go/tally/scope.go | 727 ++++++++++++++++++ vendor/github.com/uber-go/tally/stats.go | 475 ++++++++++++ .../github.com/uber-go/tally/statsd/README.md | 29 + .../uber-go/tally/statsd/reporter.go | 156 ++++ vendor/github.com/uber-go/tally/types.go | 152 ++++ vendor/vendor.json | 24 + 26 files changed, 4516 insertions(+) create mode 100644 common/metrics/service.go create mode 100644 common/metrics/service_test.go create mode 100644 common/metrics/tally_provider.go create mode 100644 common/metrics/tally_provider_test.go create mode 100644 common/metrics/types.go create mode 100644 vendor/github.com/facebookgo/clock/LICENSE create mode 100644 vendor/github.com/facebookgo/clock/README.md create mode 100644 vendor/github.com/facebookgo/clock/clock.go create mode 100644 vendor/github.com/uber-go/tally/LICENSE create mode 100644 vendor/github.com/uber-go/tally/Makefile create mode 100644 vendor/github.com/uber-go/tally/README.md create mode 100755 vendor/github.com/uber-go/tally/check_license.sh create mode 100644 vendor/github.com/uber-go/tally/glide.lock create mode 100644 vendor/github.com/uber-go/tally/glide.yaml create mode 100644 vendor/github.com/uber-go/tally/histogram.go create mode 100644 vendor/github.com/uber-go/tally/key_gen.go create mode 100644 vendor/github.com/uber-go/tally/pool.go create mode 100644 vendor/github.com/uber-go/tally/prometheus/README.md create mode 100644 vendor/github.com/uber-go/tally/prometheus/reporter.go create mode 100644 vendor/github.com/uber-go/tally/reporter.go create mode 100644 vendor/github.com/uber-go/tally/scope.go create mode 100644 vendor/github.com/uber-go/tally/stats.go create mode 100644 vendor/github.com/uber-go/tally/statsd/README.md create mode 100644 vendor/github.com/uber-go/tally/statsd/reporter.go create mode 100644 vendor/github.com/uber-go/tally/types.go diff --git a/common/metrics/service.go b/common/metrics/service.go new file mode 100644 index 00000000000..aa31b12fdbf --- /dev/null +++ b/common/metrics/service.go @@ -0,0 +1,60 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package metrics + +import ( + "io" + "sync" + "sync/atomic" + "time" + + "github.com/uber-go/tally" +) + +const ( + namespace string = "hyperledger.fabric" +) + +var rootScope Scope +var closer io.Closer +var once sync.Once +var started uint32 + +//NewRootScope creates a global root metrics scope instance, all callers can only use it to extend sub scope +func NewRootScope() Scope { + once.Do(func() { + //TODO:Use config yaml + conf := config{ + interval: 1 * time.Second, + reporter: "nullstatreporter", + } + rootScope, closer = newRootScope( + tally.ScopeOptions{ + Prefix: namespace, + Reporter: tally.NullStatsReporter}, conf.interval) + atomic.StoreUint32(&started, 1) + }) + return rootScope +} + +//Close closes underlying resources used by metrics module +func Close() { + if atomic.LoadUint32(&started) == 1 { + closer.Close() + } +} + +//IsEnabled represents if metrics feature enabled or not based config +func IsEnabled() bool { + //TODO:Use config yaml + return true +} + +type config struct { + reporter string + interval time.Duration +} diff --git a/common/metrics/service_test.go b/common/metrics/service_test.go new file mode 100644 index 00000000000..c45a816594e --- /dev/null +++ b/common/metrics/service_test.go @@ -0,0 +1,43 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package metrics + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewRootScope(t *testing.T) { + s := NewRootScope() + assert.NotNil(t, s) +} + +func TestNewRootScopeConcurrent(t *testing.T) { + var s1 Scope + var s2 Scope + var wg sync.WaitGroup + wg.Add(2) + go func() { + s1 = NewRootScope() + wg.Done() + }() + go func() { + s2 = NewRootScope() + wg.Done() + }() + wg.Wait() + assert.Exactly(t, &s1, &s2) +} + +func TestClose(t *testing.T) { + NewRootScope() + assert.NotPanics(t, func() { + Close() + }) +} diff --git a/common/metrics/tally_provider.go b/common/metrics/tally_provider.go new file mode 100644 index 00000000000..788fac5eed7 --- /dev/null +++ b/common/metrics/tally_provider.go @@ -0,0 +1,219 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package metrics + +import ( + "fmt" + "io" + "sync" + "time" + + "github.com/uber-go/tally" +) + +var scopeRegistryKey = tally.KeyForPrefixedStringMap + +type counter struct { + tallyCounter tally.Counter +} + +func newCounter(tallyCounter tally.Counter) *counter { + return &counter{tallyCounter: tallyCounter} +} + +func (c *counter) Inc(v int64) { + c.tallyCounter.Inc(v) +} + +type gauge struct { + tallyGauge tally.Gauge +} + +func newGauge(tallyGauge tally.Gauge) *gauge { + return &gauge{tallyGauge: tallyGauge} +} + +func (g *gauge) Update(v float64) { + g.tallyGauge.Update(v) +} + +type scopeRegistry struct { + sync.RWMutex + subScopes map[string]*scope +} + +type scope struct { + separator string + prefix string + tags map[string]string + tallyScope tally.Scope + registry *scopeRegistry + + cm sync.RWMutex + gm sync.RWMutex + + counters map[string]*counter + gauges map[string]*gauge +} + +func newRootScope(opts tally.ScopeOptions, interval time.Duration) (Scope, io.Closer) { + s, closer := tally.NewRootScope(opts, interval) + return &scope{ + prefix: opts.Prefix, + separator: opts.Separator, + tallyScope: s, + registry: &scopeRegistry{ + subScopes: make(map[string]*scope), + }, + counters: make(map[string]*counter), + gauges: make(map[string]*gauge)}, closer +} + +func (s *scope) Counter(name string) Counter { + s.cm.RLock() + val, ok := s.counters[name] + s.cm.RUnlock() + if !ok { + s.cm.Lock() + val, ok = s.counters[name] + if !ok { + counter := s.tallyScope.Counter(name) + val = newCounter(counter) + s.counters[name] = val + } + s.cm.Unlock() + } + return val +} + +func (s *scope) Gauge(name string) Gauge { + s.gm.RLock() + val, ok := s.gauges[name] + s.gm.RUnlock() + if !ok { + s.gm.Lock() + val, ok = s.gauges[name] + if !ok { + gauge := s.tallyScope.Gauge(name) + val = newGauge(gauge) + s.gauges[name] = val + } + s.gm.Unlock() + } + return val +} + +func (s *scope) Tagged(tags map[string]string) Scope { + originTags := tags + tags = mergeRightTags(s.tags, tags) + key := scopeRegistryKey(s.prefix, tags) + + s.registry.RLock() + existing, ok := s.registry.subScopes[key] + if ok { + s.registry.RUnlock() + return existing + } + s.registry.RUnlock() + + s.registry.Lock() + defer s.registry.Unlock() + + existing, ok = s.registry.subScopes[key] + if ok { + return existing + } + + subScope := &scope{ + separator: s.separator, + prefix: s.prefix, + // NB(r): Take a copy of the tags on creation + // so that it cannot be modified after set. + tags: copyStringMap(tags), + tallyScope: s.tallyScope.Tagged(originTags), + registry: s.registry, + + counters: make(map[string]*counter), + gauges: make(map[string]*gauge), + } + + s.registry.subScopes[key] = subScope + return subScope +} + +func (s *scope) SubScope(prefix string) Scope { + key := scopeRegistryKey(s.fullyQualifiedName(prefix), s.tags) + + s.registry.RLock() + existing, ok := s.registry.subScopes[key] + if ok { + s.registry.RUnlock() + return existing + } + s.registry.RUnlock() + + s.registry.Lock() + defer s.registry.Unlock() + + existing, ok = s.registry.subScopes[key] + if ok { + return existing + } + + subScope := &scope{ + separator: s.separator, + prefix: s.prefix, + // NB(r): Take a copy of the tags on creation + // so that it cannot be modified after set. + tags: copyStringMap(s.tags), + tallyScope: s.tallyScope.SubScope(prefix), + registry: s.registry, + + counters: make(map[string]*counter), + gauges: make(map[string]*gauge), + } + + s.registry.subScopes[key] = subScope + return subScope +} + +func (s *scope) fullyQualifiedName(name string) string { + if len(s.prefix) == 0 { + return name + } + return fmt.Sprintf("%s%s%s", s.prefix, s.separator, name) +} + +// mergeRightTags merges 2 sets of tags with the tags from tagsRight overriding values from tagsLeft +func mergeRightTags(tagsLeft, tagsRight map[string]string) map[string]string { + if tagsLeft == nil && tagsRight == nil { + return nil + } + if len(tagsRight) == 0 { + return tagsLeft + } + if len(tagsLeft) == 0 { + return tagsRight + } + + result := make(map[string]string, len(tagsLeft)+len(tagsRight)) + for k, v := range tagsLeft { + result[k] = v + } + for k, v := range tagsRight { + result[k] = v + } + return result +} + +func copyStringMap(stringMap map[string]string) map[string]string { + result := make(map[string]string, len(stringMap)) + for k, v := range stringMap { + result[k] = v + } + return result +} diff --git a/common/metrics/tally_provider_test.go b/common/metrics/tally_provider_test.go new file mode 100644 index 00000000000..7b8565e8c29 --- /dev/null +++ b/common/metrics/tally_provider_test.go @@ -0,0 +1,342 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package metrics + +import ( + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/uber-go/tally" +) + +type testIntValue struct { + val int64 + tags map[string]string + reporter *testStatsReporter +} + +func (m *testIntValue) ReportCount(value int64) { + m.val = value + m.reporter.cg.Done() +} + +type testFloatValue struct { + val float64 + tags map[string]string + reporter *testStatsReporter +} + +func (m *testFloatValue) ReportGauge(value float64) { + m.val = value + m.reporter.gg.Done() +} + +type testStatsReporter struct { + cg sync.WaitGroup + gg sync.WaitGroup + + scope Scope + + counters map[string]*testIntValue + gauges map[string]*testFloatValue + + flushes int32 +} + +// newTestStatsReporter returns a new TestStatsReporter +func newTestStatsReporter() *testStatsReporter { + return &testStatsReporter{ + counters: make(map[string]*testIntValue), + gauges: make(map[string]*testFloatValue)} +} + +func (r *testStatsReporter) WaitAll() { + r.cg.Wait() + r.gg.Wait() +} + +func (r *testStatsReporter) AllocateCounter( + name string, tags map[string]string, +) tally.CachedCount { + counter := &testIntValue{ + val: 0, + tags: tags, + reporter: r, + } + r.counters[name] = counter + return counter +} + +func (r *testStatsReporter) ReportCounter(name string, tags map[string]string, value int64) { + r.counters[name] = &testIntValue{ + val: value, + tags: tags, + } + r.cg.Done() +} + +func (r *testStatsReporter) AllocateGauge( + name string, tags map[string]string, +) tally.CachedGauge { + gauge := &testFloatValue{ + val: 0, + tags: tags, + reporter: r, + } + r.gauges[name] = gauge + return gauge +} + +func (r *testStatsReporter) ReportGauge(name string, tags map[string]string, value float64) { + r.gauges[name] = &testFloatValue{ + val: value, + tags: tags, + } + r.gg.Done() +} + +func (r *testStatsReporter) AllocateTimer( + name string, tags map[string]string, +) tally.CachedTimer { + return nil +} + +func (r *testStatsReporter) ReportTimer(name string, tags map[string]string, interval time.Duration) { + +} + +func (r *testStatsReporter) AllocateHistogram( + name string, + tags map[string]string, + buckets tally.Buckets, +) tally.CachedHistogram { + return nil +} + +func (r *testStatsReporter) ReportHistogramValueSamples( + name string, + tags map[string]string, + buckets tally.Buckets, + bucketLowerBound, + bucketUpperBound float64, + samples int64, +) { + +} + +func (r *testStatsReporter) ReportHistogramDurationSamples( + name string, + tags map[string]string, + buckets tally.Buckets, + bucketLowerBound, + bucketUpperBound time.Duration, + samples int64, +) { + +} + +func (r *testStatsReporter) Capabilities() tally.Capabilities { + return nil +} + +func (r *testStatsReporter) Flush() { + atomic.AddInt32(&r.flushes, 1) +} + +func TestCounter(t *testing.T) { + t.Parallel() + r := newTestStatsReporter() + opts := tally.ScopeOptions{ + Prefix: namespace, + Separator: tally.DefaultSeparator, + Reporter: r} + + s, c := newRootScope(opts, 1*time.Second) + defer c.Close() + r.cg.Add(1) + s.Counter("foo").Inc(1) + r.cg.Wait() + + assert.Equal(t, int64(1), r.counters[namespace+".foo"].val) + + defer func() { + if r := recover(); r == nil { + t.Errorf("Should panic when wrong key used") + } + }() + assert.Equal(t, int64(1), r.counters[namespace+".foo1"].val) +} + +func TestMultiCounterReport(t *testing.T) { + t.Parallel() + r := newTestStatsReporter() + opts := tally.ScopeOptions{ + Prefix: namespace, + Separator: tally.DefaultSeparator, + Reporter: r} + + s, c := newRootScope(opts, 2*time.Second) + defer c.Close() + r.cg.Add(1) + go s.Counter("foo").Inc(1) + go s.Counter("foo").Inc(3) + go s.Counter("foo").Inc(5) + r.cg.Wait() + + assert.Equal(t, int64(9), r.counters[namespace+".foo"].val) +} + +func TestGauge(t *testing.T) { + t.Parallel() + r := newTestStatsReporter() + opts := tally.ScopeOptions{ + Prefix: namespace, + Separator: tally.DefaultSeparator, + Reporter: r} + + s, c := newRootScope(opts, 1*time.Second) + defer c.Close() + r.gg.Add(1) + s.Gauge("foo").Update(float64(1.33)) + r.gg.Wait() + + assert.Equal(t, float64(1.33), r.gauges[namespace+".foo"].val) +} + +func TestMultiGaugeReport(t *testing.T) { + t.Parallel() + r := newTestStatsReporter() + opts := tally.ScopeOptions{ + Prefix: namespace, + Separator: tally.DefaultSeparator, + Reporter: r} + + s, c := newRootScope(opts, 1*time.Second) + defer c.Close() + + r.gg.Add(1) + s.Gauge("foo").Update(float64(1.33)) + s.Gauge("foo").Update(float64(3.33)) + r.gg.Wait() + + assert.Equal(t, float64(3.33), r.gauges[namespace+".foo"].val) +} + +func TestSubScope(t *testing.T) { + t.Parallel() + r := newTestStatsReporter() + opts := tally.ScopeOptions{ + Prefix: namespace, + Separator: tally.DefaultSeparator, + Reporter: r} + + s, c := newRootScope(opts, 1*time.Second) + defer c.Close() + subs := s.SubScope("foo") + + r.gg.Add(1) + subs.Gauge("bar").Update(float64(1.33)) + r.gg.Wait() + + assert.Equal(t, float64(1.33), r.gauges[namespace+".foo.bar"].val) + + r.cg.Add(1) + subs.Counter("haha").Inc(1) + r.cg.Wait() + + assert.Equal(t, int64(1), r.counters[namespace+".foo.haha"].val) +} + +func TestTagged(t *testing.T) { + t.Parallel() + r := newTestStatsReporter() + opts := tally.ScopeOptions{ + Prefix: namespace, + Separator: tally.DefaultSeparator, + Reporter: r} + + s, c := newRootScope(opts, 1*time.Second) + defer c.Close() + subs := s.Tagged(map[string]string{"env": "test"}) + + r.gg.Add(1) + subs.Gauge("bar").Update(float64(1.33)) + r.gg.Wait() + + assert.Equal(t, float64(1.33), r.gauges[namespace+".bar"].val) + assert.EqualValues(t, map[string]string{ + "env": "test", + }, r.gauges[namespace+".bar"].tags) + + r.cg.Add(1) + subs.Counter("haha").Inc(1) + r.cg.Wait() + + assert.Equal(t, int64(1), r.counters[namespace+".haha"].val) + assert.EqualValues(t, map[string]string{ + "env": "test", + }, r.counters[namespace+".haha"].tags) +} + +func TestTaggedExistingReturnsSameScope(t *testing.T) { + t.Parallel() + r := newTestStatsReporter() + + for _, initialTags := range []map[string]string{ + nil, + {"env": "test"}, + } { + root, c := newRootScope(tally.ScopeOptions{Prefix: "foo", Tags: initialTags, Reporter: r}, 0) + + rootScope := root.(*scope) + fooScope := root.Tagged(map[string]string{"foo": "bar"}).(*scope) + + assert.NotEqual(t, rootScope, fooScope) + assert.Equal(t, fooScope, fooScope.Tagged(nil)) + + fooBarScope := fooScope.Tagged(map[string]string{"bar": "baz"}).(*scope) + + assert.NotEqual(t, fooScope, fooBarScope) + assert.Equal(t, fooBarScope, fooScope.Tagged(map[string]string{"bar": "baz"}).(*scope)) + c.Close() + } +} + +func TestSubScopeTagged(t *testing.T) { + t.Parallel() + r := newTestStatsReporter() + opts := tally.ScopeOptions{ + Prefix: namespace, + Separator: tally.DefaultSeparator, + Reporter: r} + + s, c := newRootScope(opts, 1*time.Second) + defer c.Close() + subs := s.SubScope("sub") + subtags := subs.Tagged(map[string]string{"env": "test"}) + + r.gg.Add(1) + subtags.Gauge("bar").Update(float64(1.33)) + r.gg.Wait() + + assert.Equal(t, float64(1.33), r.gauges[namespace+".sub.bar"].val) + assert.EqualValues(t, map[string]string{ + "env": "test", + }, r.gauges[namespace+".sub.bar"].tags) + + r.cg.Add(1) + subtags.Counter("haha").Inc(1) + r.cg.Wait() + + assert.Equal(t, int64(1), r.counters[namespace+".sub.haha"].val) + assert.EqualValues(t, map[string]string{ + "env": "test", + }, r.counters[namespace+".sub.haha"].tags) +} diff --git a/common/metrics/types.go b/common/metrics/types.go new file mode 100644 index 00000000000..247014dfa1c --- /dev/null +++ b/common/metrics/types.go @@ -0,0 +1,35 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package metrics + +// Counter is the interface for emitting Counter type metrics. +type Counter interface { + // Inc increments the Counter by a delta. + Inc(delta int64) +} + +// Gauge is the interface for emitting Gauge metrics. +type Gauge interface { + // Update sets the gauges absolute value. + Update(value float64) +} + +// Scope is a namespace wrapper around a stats reporter, ensuring that +// all emitted values have a given prefix or set of tags. +type Scope interface { + // Counter returns the Counter object corresponding to the name. + Counter(name string) Counter + + // Gauge returns the Gauge object corresponding to the name. + Gauge(name string) Gauge + + // Tagged returns a new child Scope with the given tags and current tags. + Tagged(tags map[string]string) Scope + + // SubScope returns a new child Scope appending a further name prefix. + SubScope(name string) Scope +} diff --git a/vendor/github.com/facebookgo/clock/LICENSE b/vendor/github.com/facebookgo/clock/LICENSE new file mode 100644 index 00000000000..ce212cb1cee --- /dev/null +++ b/vendor/github.com/facebookgo/clock/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ben Johnson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/facebookgo/clock/README.md b/vendor/github.com/facebookgo/clock/README.md new file mode 100644 index 00000000000..5d4f4fe72e7 --- /dev/null +++ b/vendor/github.com/facebookgo/clock/README.md @@ -0,0 +1,104 @@ +clock [![Build Status](https://drone.io/github.com/benbjohnson/clock/status.png)](https://drone.io/github.com/benbjohnson/clock/latest) [![Coverage Status](https://coveralls.io/repos/benbjohnson/clock/badge.png?branch=master)](https://coveralls.io/r/benbjohnson/clock?branch=master) [![GoDoc](https://godoc.org/github.com/benbjohnson/clock?status.png)](https://godoc.org/github.com/benbjohnson/clock) ![Project status](http://img.shields.io/status/experimental.png?color=red) +===== + +Clock is a small library for mocking time in Go. It provides an interface +around the standard library's [`time`][time] package so that the application +can use the realtime clock while tests can use the mock clock. + +[time]: http://golang.org/pkg/time/ + + +## Usage + +### Realtime Clock + +Your application can maintain a `Clock` variable that will allow realtime and +mock clocks to be interchangable. For example, if you had an `Application` type: + +```go +import "github.com/benbjohnson/clock" + +type Application struct { + Clock clock.Clock +} +``` + +You could initialize it to use the realtime clock like this: + +```go +var app Application +app.Clock = clock.New() +... +``` + +Then all timers and time-related functionality should be performed from the +`Clock` variable. + + +### Mocking time + +In your tests, you will want to use a `Mock` clock: + +```go +import ( + "testing" + + "github.com/benbjohnson/clock" +) + +func TestApplication_DoSomething(t *testing.T) { + mock := clock.NewMock() + app := Application{Clock: mock} + ... +} +``` + +Now that you've initialized your application to use the mock clock, you can +adjust the time programmatically. The mock clock always starts from the Unix +epoch (midnight, Jan 1, 1970 UTC). + + +### Controlling time + +The mock clock provides the same functions that the standard library's `time` +package provides. For example, to find the current time, you use the `Now()` +function: + +```go +mock := clock.NewMock() + +// Find the current time. +mock.Now().UTC() // 1970-01-01 00:00:00 +0000 UTC + +// Move the clock forward. +mock.Add(2 * time.Hour) + +// Check the time again. It's 2 hours later! +mock.Now().UTC() // 1970-01-01 02:00:00 +0000 UTC +``` + +Timers and Tickers are also controlled by this same mock clock. They will only +execute when the clock is moved forward: + +``` +mock := clock.NewMock() +count := 0 + +// Kick off a timer to increment every 1 mock second. +go func() { + ticker := clock.Ticker(1 * time.Second) + for { + <-ticker.C + count++ + } +}() +runtime.Gosched() + +// Move the clock forward 10 second. +mock.Add(10 * time.Second) + +// This prints 10. +fmt.Println(count) +``` + + diff --git a/vendor/github.com/facebookgo/clock/clock.go b/vendor/github.com/facebookgo/clock/clock.go new file mode 100644 index 00000000000..bca1a7ba8b3 --- /dev/null +++ b/vendor/github.com/facebookgo/clock/clock.go @@ -0,0 +1,363 @@ +package clock + +import ( + "runtime" + "sort" + "sync" + "time" +) + +// Clock represents an interface to the functions in the standard library time +// package. Two implementations are available in the clock package. The first +// is a real-time clock which simply wraps the time package's functions. The +// second is a mock clock which will only make forward progress when +// programmatically adjusted. +type Clock interface { + After(d time.Duration) <-chan time.Time + AfterFunc(d time.Duration, f func()) *Timer + Now() time.Time + Sleep(d time.Duration) + Tick(d time.Duration) <-chan time.Time + Ticker(d time.Duration) *Ticker + Timer(d time.Duration) *Timer +} + +// New returns an instance of a real-time clock. +func New() Clock { + return &clock{} +} + +// clock implements a real-time clock by simply wrapping the time package functions. +type clock struct{} + +func (c *clock) After(d time.Duration) <-chan time.Time { return time.After(d) } + +func (c *clock) AfterFunc(d time.Duration, f func()) *Timer { + return &Timer{timer: time.AfterFunc(d, f)} +} + +func (c *clock) Now() time.Time { return time.Now() } + +func (c *clock) Sleep(d time.Duration) { time.Sleep(d) } + +func (c *clock) Tick(d time.Duration) <-chan time.Time { return time.Tick(d) } + +func (c *clock) Ticker(d time.Duration) *Ticker { + t := time.NewTicker(d) + return &Ticker{C: t.C, ticker: t} +} + +func (c *clock) Timer(d time.Duration) *Timer { + t := time.NewTimer(d) + return &Timer{C: t.C, timer: t} +} + +// Mock represents a mock clock that only moves forward programmically. +// It can be preferable to a real-time clock when testing time-based functionality. +type Mock struct { + mu sync.Mutex + now time.Time // current time + timers clockTimers // tickers & timers + + calls Calls + waiting []waiting + callsMutex sync.Mutex +} + +// NewMock returns an instance of a mock clock. +// The current time of the mock clock on initialization is the Unix epoch. +func NewMock() *Mock { + return &Mock{now: time.Unix(0, 0)} +} + +// Add moves the current time of the mock clock forward by the duration. +// This should only be called from a single goroutine at a time. +func (m *Mock) Add(d time.Duration) { + // Calculate the final current time. + t := m.now.Add(d) + + // Continue to execute timers until there are no more before the new time. + for { + if !m.runNextTimer(t) { + break + } + } + + // Ensure that we end with the new time. + m.mu.Lock() + m.now = t + m.mu.Unlock() + + // Give a small buffer to make sure the other goroutines get handled. + gosched() +} + +// runNextTimer executes the next timer in chronological order and moves the +// current time to the timer's next tick time. The next time is not executed if +// it's next time if after the max time. Returns true if a timer is executed. +func (m *Mock) runNextTimer(max time.Time) bool { + m.mu.Lock() + + // Sort timers by time. + sort.Sort(m.timers) + + // If we have no more timers then exit. + if len(m.timers) == 0 { + m.mu.Unlock() + return false + } + + // Retrieve next timer. Exit if next tick is after new time. + t := m.timers[0] + if t.Next().After(max) { + m.mu.Unlock() + return false + } + + // Move "now" forward and unlock clock. + m.now = t.Next() + m.mu.Unlock() + + // Execute timer. + t.Tick(m.now) + return true +} + +// After waits for the duration to elapse and then sends the current time on the returned channel. +func (m *Mock) After(d time.Duration) <-chan time.Time { + defer m.inc(&m.calls.After) + return m.Timer(d).C +} + +// AfterFunc waits for the duration to elapse and then executes a function. +// A Timer is returned that can be stopped. +func (m *Mock) AfterFunc(d time.Duration, f func()) *Timer { + defer m.inc(&m.calls.AfterFunc) + t := m.Timer(d) + t.C = nil + t.fn = f + return t +} + +// Now returns the current wall time on the mock clock. +func (m *Mock) Now() time.Time { + defer m.inc(&m.calls.Now) + m.mu.Lock() + defer m.mu.Unlock() + return m.now +} + +// Sleep pauses the goroutine for the given duration on the mock clock. +// The clock must be moved forward in a separate goroutine. +func (m *Mock) Sleep(d time.Duration) { + defer m.inc(&m.calls.Sleep) + <-m.After(d) +} + +// Tick is a convenience function for Ticker(). +// It will return a ticker channel that cannot be stopped. +func (m *Mock) Tick(d time.Duration) <-chan time.Time { + defer m.inc(&m.calls.Tick) + return m.Ticker(d).C +} + +// Ticker creates a new instance of Ticker. +func (m *Mock) Ticker(d time.Duration) *Ticker { + defer m.inc(&m.calls.Ticker) + m.mu.Lock() + defer m.mu.Unlock() + ch := make(chan time.Time) + t := &Ticker{ + C: ch, + c: ch, + mock: m, + d: d, + next: m.now.Add(d), + } + m.timers = append(m.timers, (*internalTicker)(t)) + return t +} + +// Timer creates a new instance of Timer. +func (m *Mock) Timer(d time.Duration) *Timer { + defer m.inc(&m.calls.Timer) + m.mu.Lock() + defer m.mu.Unlock() + ch := make(chan time.Time) + t := &Timer{ + C: ch, + c: ch, + mock: m, + next: m.now.Add(d), + } + m.timers = append(m.timers, (*internalTimer)(t)) + return t +} + +func (m *Mock) removeClockTimer(t clockTimer) { + m.mu.Lock() + defer m.mu.Unlock() + for i, timer := range m.timers { + if timer == t { + copy(m.timers[i:], m.timers[i+1:]) + m.timers[len(m.timers)-1] = nil + m.timers = m.timers[:len(m.timers)-1] + break + } + } + sort.Sort(m.timers) +} + +func (m *Mock) inc(addr *uint32) { + m.callsMutex.Lock() + defer m.callsMutex.Unlock() + *addr++ + var newWaiting []waiting + for _, w := range m.waiting { + if m.calls.atLeast(w.expected) { + close(w.done) + continue + } + newWaiting = append(newWaiting, w) + } + m.waiting = newWaiting +} + +// Wait waits for at least the relevant calls before returning. The expected +// Calls are always over the lifetime of the Mock. Values in the Calls struct +// are used as the minimum number of calls, this allows you to wait for only +// the calls you care about. +func (m *Mock) Wait(s Calls) { + m.callsMutex.Lock() + if m.calls.atLeast(s) { + m.callsMutex.Unlock() + return + } + done := make(chan struct{}) + m.waiting = append(m.waiting, waiting{expected: s, done: done}) + m.callsMutex.Unlock() + <-done +} + +// clockTimer represents an object with an associated start time. +type clockTimer interface { + Next() time.Time + Tick(time.Time) +} + +// clockTimers represents a list of sortable timers. +type clockTimers []clockTimer + +func (a clockTimers) Len() int { return len(a) } +func (a clockTimers) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a clockTimers) Less(i, j int) bool { return a[i].Next().Before(a[j].Next()) } + +// Timer represents a single event. +// The current time will be sent on C, unless the timer was created by AfterFunc. +type Timer struct { + C <-chan time.Time + c chan time.Time + timer *time.Timer // realtime impl, if set + next time.Time // next tick time + mock *Mock // mock clock, if set + fn func() // AfterFunc function, if set +} + +// Stop turns off the ticker. +func (t *Timer) Stop() { + if t.timer != nil { + t.timer.Stop() + } else { + t.mock.removeClockTimer((*internalTimer)(t)) + } +} + +type internalTimer Timer + +func (t *internalTimer) Next() time.Time { return t.next } +func (t *internalTimer) Tick(now time.Time) { + if t.fn != nil { + t.fn() + } else { + t.c <- now + } + t.mock.removeClockTimer((*internalTimer)(t)) + gosched() +} + +// Ticker holds a channel that receives "ticks" at regular intervals. +type Ticker struct { + C <-chan time.Time + c chan time.Time + ticker *time.Ticker // realtime impl, if set + next time.Time // next tick time + mock *Mock // mock clock, if set + d time.Duration // time between ticks +} + +// Stop turns off the ticker. +func (t *Ticker) Stop() { + if t.ticker != nil { + t.ticker.Stop() + } else { + t.mock.removeClockTimer((*internalTicker)(t)) + } +} + +type internalTicker Ticker + +func (t *internalTicker) Next() time.Time { return t.next } +func (t *internalTicker) Tick(now time.Time) { + select { + case t.c <- now: + case <-time.After(1 * time.Millisecond): + } + t.next = now.Add(t.d) + gosched() +} + +// Sleep momentarily so that other goroutines can process. +func gosched() { runtime.Gosched() } + +// Calls keeps track of the count of calls for each of the methods on the Clock +// interface. +type Calls struct { + After uint32 + AfterFunc uint32 + Now uint32 + Sleep uint32 + Tick uint32 + Ticker uint32 + Timer uint32 +} + +// atLeast returns true if at least the number of calls in o have been made. +func (c Calls) atLeast(o Calls) bool { + if c.After < o.After { + return false + } + if c.AfterFunc < o.AfterFunc { + return false + } + if c.Now < o.Now { + return false + } + if c.Sleep < o.Sleep { + return false + } + if c.Tick < o.Tick { + return false + } + if c.Ticker < o.Ticker { + return false + } + if c.Timer < o.Timer { + return false + } + return true +} + +type waiting struct { + expected Calls + done chan struct{} +} diff --git a/vendor/github.com/uber-go/tally/LICENSE b/vendor/github.com/uber-go/tally/LICENSE new file mode 100644 index 00000000000..a8fc8514898 --- /dev/null +++ b/vendor/github.com/uber-go/tally/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/uber-go/tally/Makefile b/vendor/github.com/uber-go/tally/Makefile new file mode 100644 index 00000000000..9e81ca21db5 --- /dev/null +++ b/vendor/github.com/uber-go/tally/Makefile @@ -0,0 +1,80 @@ +export GO15VENDOREXPERIMENT=1 + +BENCH_FLAGS ?= -cpuprofile=cpu.pprof -memprofile=mem.pprof -benchmem +PKGS ?= $(shell glide novendor) +PKG_FILES ?= *.go example/*.go m3 +LINT_IGNORE = m3/thrift + +# The linting tools evolve with each Go version, so run them only on the latest +# stable release. +GO_VERSION := $(shell go version | cut -d " " -f 3) +GO_MINOR_VERSION := $(word 2,$(subst ., ,$(GO_VERSION))) +LINTABLE_MINOR_VERSIONS := 6 7 +ifneq ($(filter $(LINTABLE_MINOR_VERSIONS),$(GO_MINOR_VERSION)),) +SHOULD_LINT := true +endif + + +.PHONY: all +all: lint test + +.PHONY: dependencies +dependencies: + @echo "Installing Glide and locked dependencies..." + glide --version || go get -u -f github.com/Masterminds/glide + glide install + @echo "Installing test dependencies..." + go install ./vendor/github.com/axw/gocov/gocov + go install ./vendor/github.com/mattn/goveralls +ifdef SHOULD_LINT + @echo "Installing golint..." + go install ./vendor/github.com/golang/lint/golint +else + @echo "Not installing golint, since we don't expect to lint on" $(GO_VERSION) +endif + +.PHONY: lint +lint: +ifdef SHOULD_LINT + @rm -rf lint.log + @echo "Checking formatting..." + @gofmt -d -s $(PKG_FILES) 2>&1 | grep -v $(LINT_IGNORE) | tee lint.log + @echo "Installing test dependencies for vet..." + @go test -i $(PKGS) + @echo "Checking vet..." + @$(foreach dir,$(PKG_FILES),go tool vet $(dir) 2>&1 | grep -v $(LINT_IGNORE) | tee -a lint.log;) + @echo "Checking lint..." + @$(foreach dir,$(PKGS),golint $(dir) 2>&1 | grep -v $(LINT_IGNORE) | tee -a lint.log;) + @echo "Checking for unresolved FIXMEs..." + @git grep -i fixme | grep -v -e vendor -e Makefile | grep -v $(LINT_IGNORE) | tee -a lint.log + @echo "Checking for license headers..." + @./check_license.sh | tee -a lint.log + @[ ! -s lint.log ] +else + @echo "Skipping linters on" $(GO_VERSION) +endif + +.PHONY: test +test: + go test -race -v $(PKGS) + +.PHONY: examples +examples: + mkdir -p ./bin + go build -o ./bin/print_example ./example/ + go build -o ./bin/m3_example ./m3/example/ + go build -o ./bin/prometheus_example ./prometheus/example/ + go build -o ./bin/statsd_example ./statsd/example/ + +.PHONY: cover +cover: + go test -cover -coverprofile cover.out -race -v $(PKGS) + +.PHONY: coveralls +coveralls: + goveralls -service=travis-ci || echo "Coveralls failed" + +.PHONY: bench +BENCH ?= . +bench: + @$(foreach pkg,$(PKGS),go test -bench=$(BENCH) -run="^$$" $(BENCH_FLAGS) $(pkg);) diff --git a/vendor/github.com/uber-go/tally/README.md b/vendor/github.com/uber-go/tally/README.md new file mode 100644 index 00000000000..86307a5d8a9 --- /dev/null +++ b/vendor/github.com/uber-go/tally/README.md @@ -0,0 +1,207 @@ +# :heavy_check_mark: tally [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] + +Fast, buffered, hierarchical stats collection in Go. + +## Installation +`go get -u github.com/uber-go/tally` + +## Abstract + +Tally provides a common interface for emitting metrics, while letting you not worry about the velocity of metrics emission. + +By default it buffers counters, gauges and histograms at a specified interval but does not buffer timer values. This is primarily so timer values can have all their values sampled if desired and if not they can be sampled as summaries or histograms independently by a reporter. + +## Structure + +- Scope: Keeps track of metrics, and their common metadata. +- Metrics: Counters, Gauges, Timers and Histograms. +- Reporter: Implemented by you. Accepts aggregated values from the scope. Forwards the aggregated values to your metrics ingestion pipeline. + - The reporters already available listed alphabetically are: + - `github.com/uber-go/tally/m3`: Report m3 metrics, timers are not sampled and forwarded directly. + - `github.com/uber-go/tally/multi`: Report to multiple reporters, you can multi-write metrics to other reporters simply. + - `github.com/uber-go/tally/prometheus`: Report prometheus metrics, timers by default are made summaries with an option to make them histograms instead. + - `github.com/uber-go/tally/statsd`: Report statsd metrics, no support for tags. + +### Acquire a Scope ### +```go +reporter = NewMyStatsReporter() // Implement as you will +tags := map[string]string{ + "dc": "east-1", + "type": "master", +} +reportEvery := time.Second + +scope := tally.NewRootScope(tally.ScopeOptions{ + Tags: tags, + Reporter: reporter, +}, reportEvery) +``` + +### Get/Create a metric, use it ### +```go +// Get a counter, increment a counter +reqCounter := scope.Counter("requests") // cache me +reqCounter.Inc(1) + +queueGauge := scope.Gauge("queue_length") // cache me +queueGauge.Update(42) +``` + +### Report your metrics ### +Use the inbuilt statsd reporter: + +```go +import ( + "io" + "github.com/cactus/go-statsd-client/statsd" + "github.com/uber-go/tally" + tallystatsd "github.com/uber-go/tally/statsd" + // ... +) + +func newScope() (tally.Scope, io.Closer) { + statter, _ := statsd.NewBufferedClient("127.0.0.1:8125", + "stats", 100*time.Millisecond, 1440) + + reporter := tallystatsd.NewReporter(statter, tallystatsd.Options{ + SampleRate: 1.0, + }) + + scope, closer := tally.NewRootScope(tally.ScopeOptions{ + Prefix: "my-service", + Tags: map[string]string{}, + Reporter: r, + }, time.Second) + + return scope, closer +} +``` + +Implement your own reporter using the `StatsReporter` interface: + +```go + +// BaseStatsReporter implements the shared reporter methods. +type BaseStatsReporter interface { + Capabilities() Capabilities + Flush() +} + +// StatsReporter is a backend for Scopes to report metrics to. +type StatsReporter interface { + BaseStatsReporter + + // ReportCounter reports a counter value + ReportCounter( + name string, + tags map[string]string, + value int64, + ) + + // ReportGauge reports a gauge value + ReportGauge( + name string, + tags map[string]string, + value float64, + ) + + // ReportTimer reports a timer value + ReportTimer( + name string, + tags map[string]string, + interval time.Duration, + ) + + // ReportHistogramValueSamples reports histogram samples for a bucket + ReportHistogramValueSamples( + name string, + tags map[string]string, + buckets Buckets, + bucketLowerBound, + bucketUpperBound float64, + samples int64, + ) + + // ReportHistogramDurationSamples reports histogram samples for a bucket + ReportHistogramDurationSamples( + name string, + tags map[string]string, + buckets Buckets, + bucketLowerBound, + bucketUpperBound time.Duration, + samples int64, + ) +} +``` + +Or implement your own metrics implementation that matches the tally `Scope` interface to use different buffering semantics: + +```go +type Scope interface { + // Counter returns the Counter object corresponding to the name. + Counter(name string) Counter + + // Gauge returns the Gauge object corresponding to the name. + Gauge(name string) Gauge + + // Timer returns the Timer object corresponding to the name. + Timer(name string) Timer + + // Histogram returns the Histogram object corresponding to the name. + // To use default value and duration buckets configured for the scope + // simply pass tally.DefaultBuckets or nil. + // You can use tally.ValueBuckets{x, y, ...} for value buckets. + // You can use tally.DurationBuckets{x, y, ...} for duration buckets. + // You can use tally.MustMakeLinearValueBuckets(start, width, count) for linear values. + // You can use tally.MustMakeLinearDurationBuckets(start, width, count) for linear durations. + // You can use tally.MustMakeExponentialValueBuckets(start, factor, count) for exponential values. + // You can use tally.MustMakeExponentialDurationBuckets(start, factor, count) for exponential durations. + Histogram(name string, buckets Buckets) Histogram + + // Tagged returns a new child scope with the given tags and current tags. + Tagged(tags map[string]string) Scope + + // SubScope returns a new child scope appending a further name prefix. + SubScope(name string) Scope + + // Capabilities returns a description of metrics reporting capabilities. + Capabilities() Capabilities +} + +// Capabilities is a description of metrics reporting capabilities. +type Capabilities interface { + // Reporting returns whether the reporter has the ability to actively report. + Reporting() bool + + // Tagging returns whether the reporter has the capability for tagged metrics. + Tagging() bool +} +``` + +## Performance + +This stuff needs to be fast. With that in mind, we avoid locks and unnecessary memory allocations. + +``` +BenchmarkCounterInc-8 200000000 7.68 ns/op +BenchmarkReportCounterNoData-8 300000000 4.88 ns/op +BenchmarkReportCounterWithData-8 100000000 21.6 ns/op +BenchmarkGaugeSet-8 100000000 16.0 ns/op +BenchmarkReportGaugeNoData-8 100000000 10.4 ns/op +BenchmarkReportGaugeWithData-8 50000000 27.6 ns/op +BenchmarkTimerInterval-8 50000000 37.7 ns/op +BenchmarkTimerReport-8 300000000 5.69 ns/op +``` + +
+ +Released under the [MIT License](LICENSE). + +[doc-img]: https://godoc.org/github.com/uber-go/tally?status.svg +[doc]: https://godoc.org/github.com/uber-go/tally +[ci-img]: https://travis-ci.org/uber-go/tally.svg?branch=master +[ci]: https://travis-ci.org/uber-go/tally +[cov-img]: https://coveralls.io/repos/github/uber-go/tally/badge.svg?branch=master +[cov]: https://coveralls.io/github/uber-go/tally?branch=master +[glide.lock]: https://github.com/uber-go/tally/blob/master/glide.lock +[v1]: https://github.com/uber-go/tally/milestones diff --git a/vendor/github.com/uber-go/tally/check_license.sh b/vendor/github.com/uber-go/tally/check_license.sh new file mode 100755 index 00000000000..f3b66823097 --- /dev/null +++ b/vendor/github.com/uber-go/tally/check_license.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +./node_modules/.bin/uber-licence --version || npm i uber-licence@latest +./node_modules/.bin/uber-licence --dry --file "*.go" diff --git a/vendor/github.com/uber-go/tally/glide.lock b/vendor/github.com/uber-go/tally/glide.lock new file mode 100644 index 00000000000..2045d77c4f8 --- /dev/null +++ b/vendor/github.com/uber-go/tally/glide.lock @@ -0,0 +1,78 @@ +hash: 307f62540a5487b27efc13eb3e2c4a67bf9b50b91d5857067beca6df0f3cd8cc +updated: 2017-05-09T18:18:15.264370538-04:00 +imports: +- name: github.com/apache/thrift + version: 9549b25c77587b29be4e0b5c258221a4ed85d37a + subpackages: + - lib/go/thrift +- name: github.com/beorn7/perks + version: 4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9 + subpackages: + - quantile +- name: github.com/cactus/go-statsd-client + version: 91c326c3f7bd20f0226d3d1c289dd9f8ce28d33d + subpackages: + - statsd +- name: github.com/facebookgo/clock + version: 600d898af40aa09a7a93ecb9265d87b0504b6f03 +- name: github.com/golang/protobuf + version: 2bc9827a78f95c6665b5fe0abd1fd66b496ae2d8 + subpackages: + - proto +- name: github.com/matttproud/golang_protobuf_extensions + version: c12348ce28de40eed0136aa2b644d0ee0650e56c + subpackages: + - pbutil +- name: github.com/prometheus/client_golang + version: c5b7fccd204277076155f10851dad72b76a49317 + subpackages: + - prometheus + - prometheus/promhttp +- name: github.com/prometheus/client_model + version: fa8ad6fec33561be4280a8f0514318c79d7f6cb6 + subpackages: + - go +- name: github.com/prometheus/common + version: 6d76b79f239843a04e8ad8dfd8fcadfa3920236f + subpackages: + - expfmt + - internal/bitbucket.org/ww/goautoneg + - model +- name: github.com/prometheus/procfs + version: fcdb11ccb4389efb1b210b7ffb623ab71c5fdd60 +- name: github.com/uber-go/atomic + version: e682c1008ac17bf26d2e4b5ad6cdd08520ed0b22 +- name: gopkg.in/validator.v2 + version: 3e4f037f12a1221a0864cf0dd2e81c452ab22448 +- name: gopkg.in/yaml.v2 + version: a83829b6f1293c91addabc89d0571c246397bbf4 +testImports: +- name: github.com/axw/gocov + version: 54b98cfcac0c63fb3f9bd8e7ad241b724d4e985b + subpackages: + - gocov +- name: github.com/davecgh/go-spew + version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d + subpackages: + - spew +- name: github.com/golang/lint + version: c7bacac2b21ca01afa1dee0acf64df3ce047c28f + subpackages: + - golint +- name: github.com/mattn/goveralls + version: f4d273b02ce1b4e48acf3662b717aa987bfc4118 +- name: github.com/pborman/uuid + version: c55201b036063326c5b1b89ccfe45a184973d073 +- name: github.com/pmezard/go-difflib + version: d8ed2627bdf02c080bf22230dbb337003b7aba2d + subpackages: + - difflib +- name: github.com/stretchr/testify + version: d77da356e56a7428ad25149ca77381849a6a5232 + subpackages: + - assert + - require +- name: golang.org/x/tools + version: 3fe2afc9e626f32e91aff6eddb78b14743446865 + subpackages: + - cover diff --git a/vendor/github.com/uber-go/tally/glide.yaml b/vendor/github.com/uber-go/tally/glide.yaml new file mode 100644 index 00000000000..f5d36d3183e --- /dev/null +++ b/vendor/github.com/uber-go/tally/glide.yaml @@ -0,0 +1,44 @@ +package: github.com/uber-go/tally +version: 1.0.0 +import: +- package: github.com/cactus/go-statsd-client + version: ~3.1.0 + subpackages: + - statsd +- package: github.com/facebookgo/clock + version: 600d898af40aa09a7a93ecb9265d87b0504b6f03 +- package: github.com/prometheus/client_golang + version: ^0.8.0 + subpackages: + - prometheus +- package: github.com/apache/thrift + version: 9549b25c77587b29be4e0b5c258221a4ed85d37a + subpackages: + - lib/go/thrift +- package: github.com/uber-go/atomic + version: e682c1008ac17bf26d2e4b5ad6cdd08520ed0b22 +testImport: +- package: github.com/axw/gocov + version: 54b98cfcac0c63fb3f9bd8e7ad241b724d4e985b + subpackages: + - gocov +- package: github.com/mattn/goveralls + version: f4d273b02ce1b4e48acf3662b717aa987bfc4118 +- package: golang.org/x/tools + version: 3fe2afc9e626f32e91aff6eddb78b14743446865 + subpackages: + - cover +- package: github.com/golang/lint + version: c7bacac2b21ca01afa1dee0acf64df3ce047c28f + subpackages: + - golint +- package: github.com/pborman/uuid + version: c55201b036063326c5b1b89ccfe45a184973d073 +- package: github.com/stretchr/testify + version: d77da356e56a7428ad25149ca77381849a6a5232 + subpackages: + - assert + - require +- package: gopkg.in/validator.v2 +- package: gopkg.in/yaml.v2 + diff --git a/vendor/github.com/uber-go/tally/histogram.go b/vendor/github.com/uber-go/tally/histogram.go new file mode 100644 index 00000000000..8e531d8a557 --- /dev/null +++ b/vendor/github.com/uber-go/tally/histogram.go @@ -0,0 +1,318 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package tally + +import ( + "errors" + "fmt" + "math" + "sort" + "time" +) + +var ( + // DefaultBuckets can be passed to specify to default buckets. + DefaultBuckets Buckets + + errBucketsCountNeedsGreaterThanZero = errors.New("n needs to be > 0") + errBucketsStartNeedsGreaterThanZero = errors.New("start needs to be > 0") + errBucketsFactorNeedsGreaterThanOne = errors.New("factor needs to be > 1") +) + +// ValueBuckets is a set of float64 values that implements Buckets. +type ValueBuckets []float64 + +// Implements sort.Interface +func (v ValueBuckets) Len() int { + return len(v) +} + +// Implements sort.Interface +func (v ValueBuckets) Swap(i, j int) { + v[i], v[j] = v[j], v[i] +} + +// Implements sort.Interface +func (v ValueBuckets) Less(i, j int) bool { + return v[i] < v[j] +} + +func (v ValueBuckets) String() string { + values := make([]string, len(v)) + for i := range values { + values[i] = fmt.Sprintf("%f", v[i]) + } + return fmt.Sprint(values) +} + +// AsValues implements Buckets. +func (v ValueBuckets) AsValues() []float64 { + return []float64(v) +} + +// AsDurations implements Buckets and returns time.Duration +// representations of the float64 values divided by time.Second. +func (v ValueBuckets) AsDurations() []time.Duration { + values := make([]time.Duration, len(v)) + for i := range values { + values[i] = time.Duration(v[i] * float64(time.Second)) + } + return values +} + +// DurationBuckets is a set of time.Duration values that implements Buckets. +type DurationBuckets []time.Duration + +// Implements sort.Interface +func (v DurationBuckets) Len() int { + return len(v) +} + +// Implements sort.Interface +func (v DurationBuckets) Swap(i, j int) { + v[i], v[j] = v[j], v[i] +} + +// Implements sort.Interface +func (v DurationBuckets) Less(i, j int) bool { + return v[i] < v[j] +} + +func (v DurationBuckets) String() string { + values := make([]string, len(v)) + for i := range values { + values[i] = v[i].String() + } + return fmt.Sprintf("%v", values) +} + +// AsValues implements Buckets and returns float64 +// representations of the time.Duration values divided by time.Second. +func (v DurationBuckets) AsValues() []float64 { + values := make([]float64, len(v)) + for i := range values { + values[i] = float64(v[i]) / float64(time.Second) + } + return values +} + +// AsDurations implements Buckets. +func (v DurationBuckets) AsDurations() []time.Duration { + return []time.Duration(v) +} + +// BucketPairs creates a set of bucket pairs from a set +// of buckets describing the lower and upper bounds for +// each derived bucket. +func BucketPairs(buckets Buckets) []BucketPair { + if buckets == nil || buckets.Len() < 1 { + return []BucketPair{ + bucketPair{ + lowerBoundValue: -math.MaxFloat64, + upperBoundValue: math.MaxFloat64, + lowerBoundDuration: time.Duration(math.MinInt64), + upperBoundDuration: time.Duration(math.MaxInt64), + }, + } + } + + if durationBuckets, ok := buckets.(DurationBuckets); ok { + // If using duration buckets separating negative times and + // positive times is very much desirable as depending on the + // reporter will create buckets "-infinity,0" and "0,{first_bucket}" + // instead of just "-infinity,{first_bucket}" which for time + // durations is not desirable nor pragmatic + hasZero := false + for _, b := range buckets.AsDurations() { + if b == 0 { + hasZero = true + break + } + } + if !hasZero { + buckets = append(DurationBuckets{0}, durationBuckets...) + } + } + + // Sort before iterating to create pairs + sort.Sort(buckets) + + var ( + asValueBuckets = buckets.AsValues() + asDurationBuckets = buckets.AsDurations() + pairs = make([]BucketPair, 0, buckets.Len()+2) + ) + pairs = append(pairs, bucketPair{ + lowerBoundValue: -math.MaxFloat64, + upperBoundValue: asValueBuckets[0], + lowerBoundDuration: time.Duration(math.MinInt64), + upperBoundDuration: asDurationBuckets[0], + }) + + prevValueBucket, prevDurationBucket := + asValueBuckets[0], asDurationBuckets[0] + for i := 1; i < buckets.Len(); i++ { + pairs = append(pairs, bucketPair{ + lowerBoundValue: prevValueBucket, + upperBoundValue: asValueBuckets[i], + lowerBoundDuration: prevDurationBucket, + upperBoundDuration: asDurationBuckets[i], + }) + prevValueBucket, prevDurationBucket = + asValueBuckets[i], asDurationBuckets[i] + } + + pairs = append(pairs, bucketPair{ + lowerBoundValue: prevValueBucket, + upperBoundValue: math.MaxFloat64, + lowerBoundDuration: prevDurationBucket, + upperBoundDuration: time.Duration(math.MaxInt64), + }) + + return pairs +} + +type bucketPair struct { + lowerBoundValue float64 + upperBoundValue float64 + lowerBoundDuration time.Duration + upperBoundDuration time.Duration +} + +func (p bucketPair) LowerBoundValue() float64 { + return p.lowerBoundValue +} + +func (p bucketPair) UpperBoundValue() float64 { + return p.upperBoundValue +} + +func (p bucketPair) LowerBoundDuration() time.Duration { + return p.lowerBoundDuration +} + +func (p bucketPair) UpperBoundDuration() time.Duration { + return p.upperBoundDuration +} + +// LinearValueBuckets creates a set of linear value buckets. +func LinearValueBuckets(start, width float64, n int) (ValueBuckets, error) { + if n <= 0 { + return nil, errBucketsCountNeedsGreaterThanZero + } + buckets := make([]float64, n) + for i := range buckets { + buckets[i] = start + (float64(i) * width) + } + return ValueBuckets(buckets), nil +} + +// MustMakeLinearValueBuckets creates a set of linear value buckets +// or panics. +func MustMakeLinearValueBuckets(start, width float64, n int) ValueBuckets { + buckets, err := LinearValueBuckets(start, width, n) + if err != nil { + panic(err) + } + return buckets +} + +// LinearDurationBuckets creates a set of linear duration buckets. +func LinearDurationBuckets(start, width time.Duration, n int) (DurationBuckets, error) { + if n <= 0 { + return nil, errBucketsCountNeedsGreaterThanZero + } + buckets := make([]time.Duration, n) + for i := range buckets { + buckets[i] = start + (time.Duration(i) * width) + } + return DurationBuckets(buckets), nil +} + +// MustMakeLinearDurationBuckets creates a set of linear duration buckets. +// or panics. +func MustMakeLinearDurationBuckets(start, width time.Duration, n int) DurationBuckets { + buckets, err := LinearDurationBuckets(start, width, n) + if err != nil { + panic(err) + } + return buckets +} + +// ExponentialValueBuckets creates a set of exponential value buckets. +func ExponentialValueBuckets(start, factor float64, n int) (ValueBuckets, error) { + if n <= 0 { + return nil, errBucketsCountNeedsGreaterThanZero + } + if start <= 0 { + return nil, errBucketsStartNeedsGreaterThanZero + } + if factor <= 1 { + return nil, errBucketsFactorNeedsGreaterThanOne + } + buckets := make([]float64, n) + curr := start + for i := range buckets { + buckets[i] = curr + curr *= factor + } + return ValueBuckets(buckets), nil +} + +// MustMakeExponentialValueBuckets creates a set of exponential value buckets +// or panics. +func MustMakeExponentialValueBuckets(start, factor float64, n int) ValueBuckets { + buckets, err := ExponentialValueBuckets(start, factor, n) + if err != nil { + panic(err) + } + return buckets +} + +// ExponentialDurationBuckets creates a set of exponential duration buckets. +func ExponentialDurationBuckets(start time.Duration, factor float64, n int) (DurationBuckets, error) { + if n <= 0 { + return nil, errBucketsCountNeedsGreaterThanZero + } + if start <= 0 { + return nil, errBucketsStartNeedsGreaterThanZero + } + if factor <= 1 { + return nil, errBucketsFactorNeedsGreaterThanOne + } + buckets := make([]time.Duration, n) + curr := start + for i := range buckets { + buckets[i] = curr + curr = time.Duration(float64(curr) * factor) + } + return DurationBuckets(buckets), nil +} + +// MustMakeExponentialDurationBuckets creates a set of exponential value buckets +// or panics. +func MustMakeExponentialDurationBuckets(start time.Duration, factor float64, n int) DurationBuckets { + buckets, err := ExponentialDurationBuckets(start, factor, n) + if err != nil { + panic(err) + } + return buckets +} diff --git a/vendor/github.com/uber-go/tally/key_gen.go b/vendor/github.com/uber-go/tally/key_gen.go new file mode 100644 index 00000000000..264d2e9f5d1 --- /dev/null +++ b/vendor/github.com/uber-go/tally/key_gen.go @@ -0,0 +1,111 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package tally + +import ( + "bytes" + "sort" +) + +const ( + prefixSplitter = '+' + keyPairSplitter = ',' + keyNameSplitter = '=' +) + +var ( + keyGenPool = newKeyGenerationPool(1024, 1024, 32) + nilString = "" +) + +type keyGenerationPool struct { + bufferPool *ObjectPool + stringsPool *ObjectPool +} + +// KeyForStringMap generates a unique key for a map string set combination. +func KeyForStringMap( + stringMap map[string]string, +) string { + return KeyForPrefixedStringMap(nilString, stringMap) +} + +// KeyForPrefixedStringMap generates a unique key for a +// a prefix and a map string set combination. +func KeyForPrefixedStringMap( + prefix string, + stringMap map[string]string, +) string { + keys := keyGenPool.stringsPool.Get().([]string) + for k := range stringMap { + keys = append(keys, k) + } + + sort.Strings(keys) + + buf := keyGenPool.bufferPool.Get().(*bytes.Buffer) + + if prefix != nilString { + buf.WriteString(prefix) + buf.WriteByte(prefixSplitter) + } + + sortedKeysLen := len(stringMap) + for i := 0; i < sortedKeysLen; i++ { + buf.WriteString(keys[i]) + buf.WriteByte(keyNameSplitter) + buf.WriteString(stringMap[keys[i]]) + if i != sortedKeysLen-1 { + buf.WriteByte(keyPairSplitter) + } + } + + key := buf.String() + keyGenPool.release(buf, keys) + return key +} + +func newKeyGenerationPool(size, blen, slen int) *keyGenerationPool { + b := NewObjectPool(size) + b.Init(func() interface{} { + return bytes.NewBuffer(make([]byte, 0, blen)) + }) + + s := NewObjectPool(size) + s.Init(func() interface{} { + return make([]string, 0, slen) + }) + + return &keyGenerationPool{ + bufferPool: b, + stringsPool: s, + } +} + +func (s *keyGenerationPool) release(b *bytes.Buffer, strs []string) { + b.Reset() + s.bufferPool.Put(b) + + for i := range strs { + strs[i] = nilString + } + s.stringsPool.Put(strs[:0]) +} diff --git a/vendor/github.com/uber-go/tally/pool.go b/vendor/github.com/uber-go/tally/pool.go new file mode 100644 index 00000000000..8b90ada712c --- /dev/null +++ b/vendor/github.com/uber-go/tally/pool.go @@ -0,0 +1,63 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package tally + +// ObjectPool is an minimalistic object pool to avoid +// any circular dependencies on any other object pool. +type ObjectPool struct { + values chan interface{} + alloc func() interface{} +} + +// NewObjectPool creates a new pool. +func NewObjectPool(size int) *ObjectPool { + return &ObjectPool{ + values: make(chan interface{}, size), + } +} + +// Init initializes the object pool. +func (p *ObjectPool) Init(alloc func() interface{}) { + p.alloc = alloc + + for i := 0; i < cap(p.values); i++ { + p.values <- p.alloc() + } +} + +// Get gets an object from the pool. +func (p *ObjectPool) Get() interface{} { + var v interface{} + select { + case v = <-p.values: + default: + v = p.alloc() + } + return v +} + +// Put puts an object back to the pool. +func (p *ObjectPool) Put(obj interface{}) { + select { + case p.values <- obj: + default: + } +} diff --git a/vendor/github.com/uber-go/tally/prometheus/README.md b/vendor/github.com/uber-go/tally/prometheus/README.md new file mode 100644 index 00000000000..3f6fdad2806 --- /dev/null +++ b/vendor/github.com/uber-go/tally/prometheus/README.md @@ -0,0 +1,118 @@ +# A buffered Prometheus reporter + +See `examples/prometheus_main.go` for an end to end example. + +## Options + +You can use a specific Prometheus registry, and you can use +either summaries or histograms for timers. + +The reporter options are: + +```go +// Options is a set of options for the tally reporter. +type Options struct { + // Registerer is the prometheus registerer to register + // metrics with. Use nil to specify the default registerer. + Registerer prom.Registerer + + // DefaultTimerType is the default type timer type to create + // when using timers. It's default value is a histogram timer type. + DefaultTimerType TimerType + + // DefaultHistogramBuckets is the default histogram buckets + // to use. Use nil to specify the default histogram buckets. + DefaultHistogramBuckets []float64 + + // DefaultSummaryObjectives is the default summary objectives + // to use. Use nil to specify the default summary objectives. + DefaultSummaryObjectives map[float64]float64 + + // OnRegisterError defines a method to call to when registering + // a metric with the registerer fails. Use nil to specify + // to panic by default when registering a metric fails. + OnRegisterError func(err error) +} +``` + +The timer types are: + +```go +// TimerType describes a type of timer +type TimerType int + +const ( + // SummaryTimerType is a timer type that reports into a summary + SummaryTimerType TimerType = iota + + // HistogramTimerType is a timer type that reports into a histogram + HistogramTimerType +) +``` + +You can also pre-register help description text ahead of using a metric +that will be named and tagged identically with `tally`. You can also +access the Prometheus HTTP handler directly. + +The returned reporter interface: + +```go +// Reporter is a Prometheus backed tally reporter. +type Reporter interface { + tally.CachedStatsReporter + + // HTTPHandler provides the Prometheus HTTP scrape handler. + HTTPHandler() http.Handler + + // RegisterCounter is a helper method to initialize a counter + // in the Prometheus backend with a given help text. + // If not called explicitly, the Reporter will create one for + // you on first use, with a not super helpful HELP string. + RegisterCounter( + name string, + tagKeys []string, + desc string, + ) (*prom.CounterVec, error) + + // RegisterGauge is a helper method to initialize a gauge + // in the prometheus backend with a given help text. + // If not called explicitly, the Reporter will create one for + // you on first use, with a not super helpful HELP string. + RegisterGauge( + name string, + tagKeys []string, + desc string, + ) (*prom.GaugeVec, error) + + // RegisterTimer is a helper method to initialize a timer + // summary or histogram vector in the prometheus backend + // with a given help text. + // If not called explicitly, the Reporter will create one for + // you on first use, with a not super helpful HELP string. + // You may pass opts as nil to get the default timer type + // and objectives/buckets. + // You may also pass objectives/buckets as nil in opts to + // get the default objectives/buckets for the specified + // timer type. + RegisterTimer( + name string, + tagKeys []string, + desc string, + opts *RegisterTimerOptions, + ) (TimerUnion, error) +} +``` + +The register timer options: + +```go +// RegisterTimerOptions provides options when registering a timer on demand. +// By default you can pass nil for the options to get the reporter defaults. +type RegisterTimerOptions struct { + TimerType TimerType + HistogramBuckets []float64 + SummaryObjectives map[float64]float64 +} +``` + + diff --git a/vendor/github.com/uber-go/tally/prometheus/reporter.go b/vendor/github.com/uber-go/tally/prometheus/reporter.go new file mode 100644 index 00000000000..32b7bbe6bad --- /dev/null +++ b/vendor/github.com/uber-go/tally/prometheus/reporter.go @@ -0,0 +1,582 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package prometheus + +import ( + "errors" + "net/http" + "sync" + "time" + + "github.com/uber-go/tally" + + prom "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +const ( + // DefaultSeparator is the default separator that should be used with + // a tally scope for a prometheus reporter. + DefaultSeparator = "_" +) + +var ( + errUnknownTimerType = errors.New("unknown metric timer type") + ms = float64(time.Millisecond) / float64(time.Second) +) + +// DefaultHistogramBuckets is the default histogram buckets used when +// creating a new Histogram in the prometheus registry. +// See: https://godoc.org/github.com/prometheus/client_golang/prometheus#HistogramOpts +func DefaultHistogramBuckets() []float64 { + return []float64{ + ms, + 2 * ms, + 5 * ms, + 10 * ms, + 20 * ms, + 50 * ms, + 100 * ms, + 200 * ms, + 500 * ms, + 1000 * ms, + 2000 * ms, + 5000 * ms, + 10000 * ms, + } +} + +// DefaultSummaryObjectives is the default objectives used when +// creating a new Summary in the prometheus registry. +// See: https://godoc.org/github.com/prometheus/client_golang/prometheus#SummaryOpts +func DefaultSummaryObjectives() map[float64]float64 { + return map[float64]float64{ + 0.5: 0.01, + 0.75: 0.001, + 0.95: 0.001, + 0.99: 0.001, + 0.999: 0.0001, + } +} + +// Reporter is a Prometheus backed tally reporter. +type Reporter interface { + tally.CachedStatsReporter + + // HTTPHandler provides the Prometheus HTTP scrape handler. + HTTPHandler() http.Handler + + // RegisterCounter is a helper method to initialize a counter + // in the Prometheus backend with a given help text. + // If not called explicitly, the Reporter will create one for + // you on first use, with a not super helpful HELP string. + RegisterCounter( + name string, + tagKeys []string, + desc string, + ) (*prom.CounterVec, error) + + // RegisterGauge is a helper method to initialize a gauge + // in the prometheus backend with a given help text. + // If not called explicitly, the Reporter will create one for + // you on first use, with a not super helpful HELP string. + RegisterGauge( + name string, + tagKeys []string, + desc string, + ) (*prom.GaugeVec, error) + + // RegisterTimer is a helper method to initialize a timer + // summary or histogram vector in the prometheus backend + // with a given help text. + // If not called explicitly, the Reporter will create one for + // you on first use, with a not super helpful HELP string. + // You may pass opts as nil to get the default timer type + // and objectives/buckets. + // You may also pass objectives/buckets as nil in opts to + // get the default objectives/buckets for the specified + // timer type. + RegisterTimer( + name string, + tagKeys []string, + desc string, + opts *RegisterTimerOptions, + ) (TimerUnion, error) +} + +// RegisterTimerOptions provides options when registering a timer on demand. +// By default you can pass nil for the options to get the reporter defaults. +type RegisterTimerOptions struct { + TimerType TimerType + HistogramBuckets []float64 + SummaryObjectives map[float64]float64 +} + +// TimerUnion is a representation of either a summary or a histogram +// described by the TimerType. +type TimerUnion struct { + TimerType TimerType + Histogram *prom.HistogramVec + Summary *prom.SummaryVec +} + +type metricID string + +type reporter struct { + sync.RWMutex + registerer prom.Registerer + timerType TimerType + objectives map[float64]float64 + buckets []float64 + onRegisterError func(e error) + counters map[metricID]*prom.CounterVec + gauges map[metricID]*prom.GaugeVec + timers map[metricID]*promTimerVec + histograms map[metricID]*prom.HistogramVec +} + +type promTimerVec struct { + summary *prom.SummaryVec + histogram *prom.HistogramVec +} + +type cachedMetric struct { + counter prom.Counter + gauge prom.Gauge + reportTimer func(d time.Duration) + histogram prom.Histogram + summary prom.Summary +} + +func (m *cachedMetric) ReportCount(value int64) { + m.counter.Add(float64(value)) +} + +func (m *cachedMetric) ReportGauge(value float64) { + m.gauge.Set(value) +} + +func (m *cachedMetric) ReportTimer(interval time.Duration) { + m.reportTimer(interval) +} + +func (m *cachedMetric) reportTimerHistogram(interval time.Duration) { + m.histogram.Observe(float64(interval) / float64(time.Second)) +} + +func (m *cachedMetric) reportTimerSummary(interval time.Duration) { + m.summary.Observe(float64(interval) / float64(time.Second)) +} + +func (m *cachedMetric) ValueBucket( + bucketLowerBound, bucketUpperBound float64, +) tally.CachedHistogramBucket { + return cachedHistogramBucket{m, bucketUpperBound} +} + +func (m *cachedMetric) DurationBucket( + bucketLowerBound, bucketUpperBound time.Duration, +) tally.CachedHistogramBucket { + upperBound := float64(bucketUpperBound) / float64(time.Second) + return cachedHistogramBucket{m, upperBound} +} + +type cachedHistogramBucket struct { + metric *cachedMetric + upperBound float64 +} + +func (b cachedHistogramBucket) ReportSamples(value int64) { + for i := int64(0); i < value; i++ { + b.metric.histogram.Observe(b.upperBound) + } +} + +type noopMetric struct{} + +func (m noopMetric) ReportCount(value int64) {} +func (m noopMetric) ReportGauge(value float64) {} +func (m noopMetric) ReportTimer(interval time.Duration) {} +func (m noopMetric) ReportSamples(value int64) {} +func (m noopMetric) ValueBucket(lower, upper float64) tally.CachedHistogramBucket { + return m +} +func (m noopMetric) DurationBucket(lower, upper time.Duration) tally.CachedHistogramBucket { + return m +} + +func (r *reporter) HTTPHandler() http.Handler { + return promhttp.Handler() +} + +// TimerType describes a type of timer +type TimerType int + +const ( + // SummaryTimerType is a timer type that reports into a summary + SummaryTimerType TimerType = iota + + // HistogramTimerType is a timer type that reports into a histogram + HistogramTimerType +) + +// Options is a set of options for the tally reporter. +type Options struct { + // Registerer is the prometheus registerer to register + // metrics with. Use nil to specify the default registerer. + Registerer prom.Registerer + + // DefaultTimerType is the default type timer type to create + // when using timers. It's default value is a summary timer type. + DefaultTimerType TimerType + + // DefaultHistogramBuckets is the default histogram buckets + // to use. Use nil to specify the default histogram buckets. + DefaultHistogramBuckets []float64 + + // DefaultSummaryObjectives is the default summary objectives + // to use. Use nil to specify the default summary objectives. + DefaultSummaryObjectives map[float64]float64 + + // OnRegisterError defines a method to call to when registering + // a metric with the registerer fails. Use nil to specify + // to panic by default when registering fails. + OnRegisterError func(err error) +} + +// NewReporter returns a new Reporter for Prometheus client backed metrics +// objectives is the objectives used when creating a new Summary histogram for Timers. See +// https://godoc.org/github.com/prometheus/client_golang/prometheus#SummaryOpts for more details. +func NewReporter(opts Options) Reporter { + if opts.Registerer == nil { + opts.Registerer = prom.DefaultRegisterer + } + if opts.DefaultHistogramBuckets == nil { + opts.DefaultHistogramBuckets = DefaultHistogramBuckets() + } + if opts.DefaultSummaryObjectives == nil { + opts.DefaultSummaryObjectives = DefaultSummaryObjectives() + } + if opts.OnRegisterError == nil { + opts.OnRegisterError = func(err error) { + panic(err) + } + } + return &reporter{ + registerer: opts.Registerer, + timerType: opts.DefaultTimerType, + buckets: opts.DefaultHistogramBuckets, + objectives: opts.DefaultSummaryObjectives, + onRegisterError: opts.OnRegisterError, + counters: make(map[metricID]*prom.CounterVec), + gauges: make(map[metricID]*prom.GaugeVec), + timers: make(map[metricID]*promTimerVec), + } +} + +func (r *reporter) RegisterCounter( + name string, + tagKeys []string, + desc string, +) (*prom.CounterVec, error) { + return r.counterVec(name, tagKeys, desc) +} + +func (r *reporter) counterVec( + name string, + tagKeys []string, + desc string, +) (*prom.CounterVec, error) { + id := canonicalMetricID(name, tagKeys) + + r.Lock() + defer r.Unlock() + + if ctr, ok := r.counters[id]; ok { + return ctr, nil + } + + ctr := prom.NewCounterVec( + prom.CounterOpts{ + Name: name, + Help: desc, + }, + tagKeys, + ) + + if err := r.registerer.Register(ctr); err != nil { + return nil, err + } + + r.counters[id] = ctr + return ctr, nil +} + +// AllocateCounter implements tally.CachedStatsReporter. +func (r *reporter) AllocateCounter(name string, tags map[string]string) tally.CachedCount { + tagKeys := keysFromMap(tags) + counterVec, err := r.counterVec(name, tagKeys, name+" counter") + if err != nil { + r.onRegisterError(err) + return noopMetric{} + } + return &cachedMetric{counter: counterVec.With(tags)} +} + +func (r *reporter) RegisterGauge( + name string, + tagKeys []string, + desc string, +) (*prom.GaugeVec, error) { + return r.gaugeVec(name, tagKeys, desc) +} + +func (r *reporter) gaugeVec( + name string, + tagKeys []string, + desc string, +) (*prom.GaugeVec, error) { + id := canonicalMetricID(name, tagKeys) + + r.Lock() + defer r.Unlock() + + if g, ok := r.gauges[id]; ok { + return g, nil + } + + g := prom.NewGaugeVec( + prom.GaugeOpts{ + Name: name, + Help: desc, + }, + tagKeys, + ) + + if err := r.registerer.Register(g); err != nil { + return nil, err + } + + r.gauges[id] = g + return g, nil +} + +// AllocateGauge implements tally.CachedStatsReporter. +func (r *reporter) AllocateGauge(name string, tags map[string]string) tally.CachedGauge { + tagKeys := keysFromMap(tags) + gaugeVec, err := r.gaugeVec(name, tagKeys, name+" gauge") + if err != nil { + r.onRegisterError(err) + return noopMetric{} + } + return &cachedMetric{gauge: gaugeVec.With(tags)} +} + +func (r *reporter) RegisterTimer( + name string, + tagKeys []string, + desc string, + opts *RegisterTimerOptions, +) (TimerUnion, error) { + timerType, buckets, objectives := r.timerConfig(opts) + switch timerType { + case HistogramTimerType: + h, err := r.histogramVec(name, tagKeys, desc, buckets) + return TimerUnion{TimerType: timerType, Histogram: h}, err + case SummaryTimerType: + s, err := r.summaryVec(name, tagKeys, desc, objectives) + return TimerUnion{TimerType: timerType, Summary: s}, err + } + return TimerUnion{}, errUnknownTimerType +} + +func (r *reporter) timerConfig( + opts *RegisterTimerOptions, +) ( + timerType TimerType, + buckets []float64, + objectives map[float64]float64, +) { + timerType = r.timerType + objectives = r.objectives + buckets = r.buckets + if opts != nil { + timerType = opts.TimerType + if opts.SummaryObjectives != nil { + objectives = opts.SummaryObjectives + } + if opts.HistogramBuckets != nil { + buckets = opts.HistogramBuckets + } + } + return +} + +func (r *reporter) summaryVec( + name string, + tagKeys []string, + desc string, + objectives map[float64]float64, +) (*prom.SummaryVec, error) { + id := canonicalMetricID(name, tagKeys) + + r.Lock() + defer r.Unlock() + + if s, ok := r.timers[id]; ok { + return s.summary, nil + } + + s := prom.NewSummaryVec( + prom.SummaryOpts{ + Name: name, + Help: desc, + Objectives: objectives, + }, + tagKeys, + ) + + if err := r.registerer.Register(s); err != nil { + return nil, err + } + + r.timers[id] = &promTimerVec{summary: s} + return s, nil +} + +func (r *reporter) histogramVec( + name string, + tagKeys []string, + desc string, + buckets []float64, +) (*prom.HistogramVec, error) { + id := canonicalMetricID(name, tagKeys) + + r.Lock() + defer r.Unlock() + + if h, ok := r.timers[id]; ok { + return h.histogram, nil + } + + h := prom.NewHistogramVec( + prom.HistogramOpts{ + Name: name, + Help: desc, + Buckets: buckets, + }, + tagKeys, + ) + + if err := r.registerer.Register(h); err != nil { + return nil, err + } + + r.timers[id] = &promTimerVec{histogram: h} + return h, nil +} + +// AllocateTimer implements tally.CachedStatsReporter. +func (r *reporter) AllocateTimer(name string, tags map[string]string) tally.CachedTimer { + var ( + timer tally.CachedTimer + err error + ) + tagKeys := keysFromMap(tags) + timerType, buckets, objectives := r.timerConfig(nil) + switch timerType { + case HistogramTimerType: + var histogramVec *prom.HistogramVec + histogramVec, err = r.histogramVec(name, tagKeys, name+" histogram", buckets) + if err == nil { + t := &cachedMetric{histogram: histogramVec.With(tags)} + t.reportTimer = t.reportTimerHistogram + timer = t + } + case SummaryTimerType: + var summaryVec *prom.SummaryVec + summaryVec, err = r.summaryVec(name, tagKeys, name+" summary", objectives) + if err == nil { + t := &cachedMetric{summary: summaryVec.With(tags)} + t.reportTimer = t.reportTimerSummary + timer = t + } + default: + err = errUnknownTimerType + } + if err != nil { + r.onRegisterError(err) + return noopMetric{} + } + return timer +} + +func (r *reporter) AllocateHistogram( + name string, + tags map[string]string, + buckets tally.Buckets, +) tally.CachedHistogram { + tagKeys := keysFromMap(tags) + histogramVec, err := r.histogramVec(name, tagKeys, name+" histogram", buckets.AsValues()) + if err != nil { + r.onRegisterError(err) + return noopMetric{} + } + return &cachedMetric{histogram: histogramVec.With(tags)} +} + +func (r *reporter) Capabilities() tally.Capabilities { + return r +} + +func (r *reporter) Reporting() bool { + return true +} + +func (r *reporter) Tagging() bool { + return true +} + +// Flush does nothing for prometheus +func (r *reporter) Flush() {} + +var metricIDKeyValue = "1" + +// NOTE: this generates a canonical MetricID for a given name+label keys, +// not values. This omits label values, as we track metrics as +// Vectors in order to support on-the-fly label changes. +func canonicalMetricID(name string, tagKeys []string) metricID { + keySet := make(map[string]string, len(tagKeys)) + for _, key := range tagKeys { + keySet[key] = metricIDKeyValue + } + return metricID(tally.KeyForPrefixedStringMap(name, keySet)) +} + +func keysFromMap(m map[string]string) []string { + labelKeys := make([]string, len(m)) + i := 0 + for k := range m { + labelKeys[i] = k + i++ + } + return labelKeys +} diff --git a/vendor/github.com/uber-go/tally/reporter.go b/vendor/github.com/uber-go/tally/reporter.go new file mode 100644 index 00000000000..6384475858d --- /dev/null +++ b/vendor/github.com/uber-go/tally/reporter.go @@ -0,0 +1,140 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package tally + +import "time" + +// BaseStatsReporter implements the shared reporter methods. +type BaseStatsReporter interface { + // Capabilities returns the capabilities description of the reporter. + Capabilities() Capabilities + + // Flush asks the reporter to flush all reported values. + Flush() +} + +// StatsReporter is a backend for Scopes to report metrics to. +type StatsReporter interface { + BaseStatsReporter + + // ReportCounter reports a counter value + ReportCounter( + name string, + tags map[string]string, + value int64, + ) + + // ReportGauge reports a gauge value + ReportGauge( + name string, + tags map[string]string, + value float64, + ) + + // ReportTimer reports a timer value + ReportTimer( + name string, + tags map[string]string, + interval time.Duration, + ) + + // ReportHistogramValueSamples reports histogram samples for a bucket + ReportHistogramValueSamples( + name string, + tags map[string]string, + buckets Buckets, + bucketLowerBound, + bucketUpperBound float64, + samples int64, + ) + + // ReportHistogramDurationSamples reports histogram samples for a bucket + ReportHistogramDurationSamples( + name string, + tags map[string]string, + buckets Buckets, + bucketLowerBound, + bucketUpperBound time.Duration, + samples int64, + ) +} + +// CachedStatsReporter is a backend for Scopes that pre allocates all +// counter, gauges, timers & histograms. This is harder to implement but more performant. +type CachedStatsReporter interface { + BaseStatsReporter + + // AllocateCounter pre allocates a counter data structure with name & tags. + AllocateCounter( + name string, + tags map[string]string, + ) CachedCount + + // AllocateGauge pre allocates a gauge data structure with name & tags. + AllocateGauge( + name string, + tags map[string]string, + ) CachedGauge + + // AllocateTimer pre allocates a timer data structure with name & tags. + AllocateTimer( + name string, + tags map[string]string, + ) CachedTimer + + // AllocateHistogram pre allocates a histogram data structure with name, tags, + // value buckets and duration buckets. + AllocateHistogram( + name string, + tags map[string]string, + buckets Buckets, + ) CachedHistogram +} + +// CachedCount interface for reporting an individual counter +type CachedCount interface { + ReportCount(value int64) +} + +// CachedGauge interface for reporting an individual gauge +type CachedGauge interface { + ReportGauge(value float64) +} + +// CachedTimer interface for reporting an individual timer +type CachedTimer interface { + ReportTimer(interval time.Duration) +} + +// CachedHistogram interface for reporting histogram samples to buckets +type CachedHistogram interface { + ValueBucket( + bucketLowerBound, bucketUpperBound float64, + ) CachedHistogramBucket + DurationBucket( + bucketLowerBound, bucketUpperBound time.Duration, + ) CachedHistogramBucket +} + +// CachedHistogramBucket interface for reporting histogram samples to a specific bucket +type CachedHistogramBucket interface { + ReportSamples(value int64) +} diff --git a/vendor/github.com/uber-go/tally/scope.go b/vendor/github.com/uber-go/tally/scope.go new file mode 100644 index 00000000000..834a8ef785a --- /dev/null +++ b/vendor/github.com/uber-go/tally/scope.go @@ -0,0 +1,727 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package tally + +import ( + "fmt" + "io" + "sync" + "time" + + "github.com/facebookgo/clock" +) + +var ( + // NoopScope is a scope that does nothing + NoopScope, _ = NewRootScope(ScopeOptions{Reporter: NullStatsReporter}, 0) + // DefaultSeparator is the default separator used to join nested scopes + DefaultSeparator = "." + + globalClock = clock.New() + + defaultScopeBuckets = DurationBuckets{ + 0 * time.Millisecond, + 10 * time.Millisecond, + 25 * time.Millisecond, + 50 * time.Millisecond, + 75 * time.Millisecond, + 100 * time.Millisecond, + 200 * time.Millisecond, + 300 * time.Millisecond, + 400 * time.Millisecond, + 500 * time.Millisecond, + 600 * time.Millisecond, + 800 * time.Millisecond, + 1 * time.Second, + 2 * time.Second, + 5 * time.Second, + } +) + +type scope struct { + separator string + prefix string + tags map[string]string + reporter StatsReporter + cachedReporter CachedStatsReporter + baseReporter BaseStatsReporter + defaultBuckets Buckets + + registry *scopeRegistry + status scopeStatus + + cm sync.RWMutex + gm sync.RWMutex + tm sync.RWMutex + hm sync.RWMutex + + counters map[string]*counter + gauges map[string]*gauge + timers map[string]*timer + histograms map[string]*histogram +} + +type scopeStatus struct { + sync.RWMutex + closed bool + quit chan struct{} +} + +type scopeRegistry struct { + sync.RWMutex + subscopes map[string]*scope +} + +var scopeRegistryKey = KeyForPrefixedStringMap + +// ScopeOptions is a set of options to construct a scope. +type ScopeOptions struct { + Tags map[string]string + Prefix string + Reporter StatsReporter + CachedReporter CachedStatsReporter + Separator string + DefaultBuckets Buckets +} + +// NewRootScope creates a new root Scope with a set of options and +// a reporting interval. +// Must provide either a StatsReporter or a CachedStatsReporter. +func NewRootScope(opts ScopeOptions, interval time.Duration) (Scope, io.Closer) { + s := newRootScope(opts, interval) + return s, s +} + +// NewTestScope creates a new Scope without a stats reporter with the +// given prefix and adds the ability to take snapshots of metrics emitted +// to it. +func NewTestScope( + prefix string, + tags map[string]string, +) TestScope { + return newRootScope(ScopeOptions{Prefix: prefix, Tags: tags}, 0) +} + +func newRootScope(opts ScopeOptions, interval time.Duration) *scope { + if opts.Tags == nil { + opts.Tags = make(map[string]string) + } + if opts.Separator == "" { + opts.Separator = DefaultSeparator + } + + var baseReporter BaseStatsReporter + if opts.Reporter != nil { + baseReporter = opts.Reporter + } else if opts.CachedReporter != nil { + baseReporter = opts.CachedReporter + } + + if opts.DefaultBuckets == nil || opts.DefaultBuckets.Len() < 1 { + opts.DefaultBuckets = defaultScopeBuckets + } + + s := &scope{ + separator: opts.Separator, + prefix: opts.Prefix, + // NB(r): Take a copy of the tags on creation + // so that it cannot be modified after set. + tags: copyStringMap(opts.Tags), + reporter: opts.Reporter, + cachedReporter: opts.CachedReporter, + baseReporter: baseReporter, + defaultBuckets: opts.DefaultBuckets, + + registry: &scopeRegistry{ + subscopes: make(map[string]*scope), + }, + status: scopeStatus{ + closed: false, + quit: make(chan struct{}, 1), + }, + + counters: make(map[string]*counter), + gauges: make(map[string]*gauge), + timers: make(map[string]*timer), + histograms: make(map[string]*histogram), + } + + // Register the root scope + s.registry.subscopes[scopeRegistryKey(s.prefix, s.tags)] = s + + if interval > 0 { + go s.reportLoop(interval) + } + + return s +} + +// report dumps all aggregated stats into the reporter. Should be called automatically by the root scope periodically. +func (s *scope) report(r StatsReporter) { + s.cm.RLock() + for name, counter := range s.counters { + counter.report(s.fullyQualifiedName(name), s.tags, r) + } + s.cm.RUnlock() + + s.gm.RLock() + for name, gauge := range s.gauges { + gauge.report(s.fullyQualifiedName(name), s.tags, r) + } + s.gm.RUnlock() + + // we do nothing for timers here because timers report directly to ths StatsReporter without buffering + + s.hm.RLock() + for name, histogram := range s.histograms { + histogram.report(s.fullyQualifiedName(name), s.tags, r) + } + s.hm.RUnlock() + + r.Flush() +} + +func (s *scope) cachedReport(c CachedStatsReporter) { + s.cm.RLock() + for _, counter := range s.counters { + counter.cachedReport() + } + s.cm.RUnlock() + + s.gm.RLock() + for _, gauge := range s.gauges { + gauge.cachedReport() + } + s.gm.RUnlock() + + // we do nothing for timers here because timers report directly to ths StatsReporter without buffering + + s.hm.RLock() + for _, histogram := range s.histograms { + histogram.cachedReport() + } + s.hm.RUnlock() + + c.Flush() +} + +// reportLoop is used by the root scope for periodic reporting +func (s *scope) reportLoop(interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + s.reportLoopRun() + case <-s.status.quit: + return + } + } +} + +func (s *scope) reportLoopRun() { + // Need to hold a status lock to ensure not to report + // and flush after a close + s.status.RLock() + if s.status.closed { + s.status.RUnlock() + return + } + + s.reportRegistryWithLock() + + s.status.RUnlock() +} + +// reports current registry with scope status lock held +func (s *scope) reportRegistryWithLock() { + s.registry.RLock() + if s.reporter != nil { + for _, ss := range s.registry.subscopes { + ss.report(s.reporter) + } + } else if s.cachedReporter != nil { + for _, ss := range s.registry.subscopes { + ss.cachedReport(s.cachedReporter) + } + } + s.registry.RUnlock() +} + +func (s *scope) Counter(name string) Counter { + s.cm.RLock() + val, ok := s.counters[name] + s.cm.RUnlock() + if !ok { + s.cm.Lock() + val, ok = s.counters[name] + if !ok { + var cachedCounter CachedCount + if s.cachedReporter != nil { + cachedCounter = s.cachedReporter.AllocateCounter( + s.fullyQualifiedName(name), s.tags, + ) + } + val = newCounter(cachedCounter) + s.counters[name] = val + } + s.cm.Unlock() + } + return val +} + +func (s *scope) Gauge(name string) Gauge { + s.gm.RLock() + val, ok := s.gauges[name] + s.gm.RUnlock() + if !ok { + s.gm.Lock() + val, ok = s.gauges[name] + if !ok { + var cachedGauge CachedGauge + if s.cachedReporter != nil { + cachedGauge = s.cachedReporter.AllocateGauge( + s.fullyQualifiedName(name), s.tags, + ) + } + val = newGauge(cachedGauge) + s.gauges[name] = val + } + s.gm.Unlock() + } + return val +} + +func (s *scope) Timer(name string) Timer { + s.tm.RLock() + val, ok := s.timers[name] + s.tm.RUnlock() + if !ok { + s.tm.Lock() + val, ok = s.timers[name] + if !ok { + var cachedTimer CachedTimer + if s.cachedReporter != nil { + cachedTimer = s.cachedReporter.AllocateTimer( + s.fullyQualifiedName(name), s.tags, + ) + } + val = newTimer( + s.fullyQualifiedName(name), s.tags, s.reporter, cachedTimer, + ) + s.timers[name] = val + } + s.tm.Unlock() + } + return val +} + +func (s *scope) Histogram(name string, b Buckets) Histogram { + if b == nil { + b = s.defaultBuckets + } + + s.hm.RLock() + val, ok := s.histograms[name] + s.hm.RUnlock() + if !ok { + s.hm.Lock() + val, ok = s.histograms[name] + if !ok { + var cachedHistogram CachedHistogram + if s.cachedReporter != nil { + cachedHistogram = s.cachedReporter.AllocateHistogram( + s.fullyQualifiedName(name), s.tags, b, + ) + } + val = newHistogram( + s.fullyQualifiedName(name), s.tags, s.reporter, b, cachedHistogram, + ) + s.histograms[name] = val + } + s.hm.Unlock() + } + return val +} + +func (s *scope) Tagged(tags map[string]string) Scope { + return s.subscope(s.prefix, tags) +} + +func (s *scope) SubScope(prefix string) Scope { + return s.subscope(s.fullyQualifiedName(prefix), nil) +} + +func (s *scope) subscope(prefix string, tags map[string]string) Scope { + tags = mergeRightTags(s.tags, tags) + key := scopeRegistryKey(prefix, tags) + + s.registry.RLock() + existing, ok := s.registry.subscopes[key] + if ok { + s.registry.RUnlock() + return existing + } + s.registry.RUnlock() + + s.registry.Lock() + defer s.registry.Unlock() + + existing, ok = s.registry.subscopes[key] + if ok { + return existing + } + + subscope := &scope{ + separator: s.separator, + prefix: prefix, + // NB(r): Take a copy of the tags on creation + // so that it cannot be modified after set. + tags: copyStringMap(tags), + reporter: s.reporter, + cachedReporter: s.cachedReporter, + baseReporter: s.baseReporter, + defaultBuckets: s.defaultBuckets, + registry: s.registry, + + counters: make(map[string]*counter), + gauges: make(map[string]*gauge), + timers: make(map[string]*timer), + histograms: make(map[string]*histogram), + } + + s.registry.subscopes[key] = subscope + return subscope +} + +func (s *scope) Capabilities() Capabilities { + if s.baseReporter == nil { + return capabilitiesNone + } + return s.baseReporter.Capabilities() +} + +func (s *scope) Snapshot() Snapshot { + snap := newSnapshot() + + s.registry.RLock() + for _, ss := range s.registry.subscopes { + // NB(r): tags are immutable, no lock required to read. + tags := make(map[string]string, len(s.tags)) + for k, v := range ss.tags { + tags[k] = v + } + + ss.cm.RLock() + for key, c := range ss.counters { + name := ss.fullyQualifiedName(key) + id := KeyForPrefixedStringMap(name, tags) + snap.counters[id] = &counterSnapshot{ + name: name, + tags: tags, + value: c.snapshot(), + } + } + ss.cm.RUnlock() + ss.gm.RLock() + for key, g := range ss.gauges { + name := ss.fullyQualifiedName(key) + id := KeyForPrefixedStringMap(name, tags) + snap.gauges[id] = &gaugeSnapshot{ + name: name, + tags: tags, + value: g.snapshot(), + } + } + ss.gm.RUnlock() + ss.tm.RLock() + for key, t := range ss.timers { + name := ss.fullyQualifiedName(key) + id := KeyForPrefixedStringMap(name, tags) + snap.timers[id] = &timerSnapshot{ + name: name, + tags: tags, + values: t.snapshot(), + } + } + ss.tm.RUnlock() + ss.hm.RLock() + for key, h := range ss.histograms { + name := ss.fullyQualifiedName(key) + id := KeyForPrefixedStringMap(name, tags) + snap.histograms[id] = &histogramSnapshot{ + name: name, + tags: tags, + values: h.snapshotValues(), + durations: h.snapshotDurations(), + } + } + ss.hm.RUnlock() + } + s.registry.RUnlock() + + return snap +} + +func (s *scope) Close() error { + s.status.Lock() + + // don't wait to close more than once (panic on double close of + // s.status.quit) + if s.status.closed { + s.status.Unlock() + return nil + } + + s.status.closed = true + close(s.status.quit) + s.reportRegistryWithLock() + + s.status.Unlock() + + if closer, ok := s.baseReporter.(io.Closer); ok { + return closer.Close() + } + return nil +} + +func (s *scope) fullyQualifiedName(name string) string { + if len(s.prefix) == 0 { + return name + } + return fmt.Sprintf("%s%s%s", s.prefix, s.separator, name) +} + +// TestScope is a metrics collector that has no reporting, ensuring that +// all emitted values have a given prefix or set of tags +type TestScope interface { + Scope + + // Snapshot returns a copy of all values since the last report execution, + // this is an expensive operation and should only be use for testing purposes + Snapshot() Snapshot +} + +// Snapshot is a snapshot of values since last report execution +type Snapshot interface { + // Counters returns a snapshot of all counter summations since last report execution + Counters() map[string]CounterSnapshot + + // Gauges returns a snapshot of gauge last values since last report execution + Gauges() map[string]GaugeSnapshot + + // Timers returns a snapshot of timer values since last report execution + Timers() map[string]TimerSnapshot + + // Histograms returns a snapshot of histogram samples since last report execution + Histograms() map[string]HistogramSnapshot +} + +// CounterSnapshot is a snapshot of a counter +type CounterSnapshot interface { + // Name returns the name + Name() string + + // Tags returns the tags + Tags() map[string]string + + // Value returns the value + Value() int64 +} + +// GaugeSnapshot is a snapshot of a gauge +type GaugeSnapshot interface { + // Name returns the name + Name() string + + // Tags returns the tags + Tags() map[string]string + + // Value returns the value + Value() float64 +} + +// TimerSnapshot is a snapshot of a timer +type TimerSnapshot interface { + // Name returns the name + Name() string + + // Tags returns the tags + Tags() map[string]string + + // Values returns the values + Values() []time.Duration +} + +// HistogramSnapshot is a snapshot of a histogram +type HistogramSnapshot interface { + // Name returns the name + Name() string + + // Tags returns the tags + Tags() map[string]string + + // Values returns the sample values by upper bound for a valueHistogram + Values() map[float64]int64 + + // Durations returns the sample values by upper bound for a durationHistogram + Durations() map[time.Duration]int64 +} + +// mergeRightTags merges 2 sets of tags with the tags from tagsRight overriding values from tagsLeft +func mergeRightTags(tagsLeft, tagsRight map[string]string) map[string]string { + if tagsLeft == nil && tagsRight == nil { + return nil + } + if len(tagsRight) == 0 { + return tagsLeft + } + if len(tagsLeft) == 0 { + return tagsRight + } + + result := make(map[string]string, len(tagsLeft)+len(tagsRight)) + for k, v := range tagsLeft { + result[k] = v + } + for k, v := range tagsRight { + result[k] = v + } + return result +} + +func copyStringMap(stringMap map[string]string) map[string]string { + result := make(map[string]string, len(stringMap)) + for k, v := range stringMap { + result[k] = v + } + return result +} + +type snapshot struct { + counters map[string]CounterSnapshot + gauges map[string]GaugeSnapshot + timers map[string]TimerSnapshot + histograms map[string]HistogramSnapshot +} + +func newSnapshot() *snapshot { + return &snapshot{ + counters: make(map[string]CounterSnapshot), + gauges: make(map[string]GaugeSnapshot), + timers: make(map[string]TimerSnapshot), + histograms: make(map[string]HistogramSnapshot), + } +} + +func (s *snapshot) Counters() map[string]CounterSnapshot { + return s.counters +} + +func (s *snapshot) Gauges() map[string]GaugeSnapshot { + return s.gauges +} + +func (s *snapshot) Timers() map[string]TimerSnapshot { + return s.timers +} + +func (s *snapshot) Histograms() map[string]HistogramSnapshot { + return s.histograms +} + +type counterSnapshot struct { + name string + tags map[string]string + value int64 +} + +func (s *counterSnapshot) Name() string { + return s.name +} + +func (s *counterSnapshot) Tags() map[string]string { + return s.tags +} + +func (s *counterSnapshot) Value() int64 { + return s.value +} + +type gaugeSnapshot struct { + name string + tags map[string]string + value float64 +} + +func (s *gaugeSnapshot) Name() string { + return s.name +} + +func (s *gaugeSnapshot) Tags() map[string]string { + return s.tags +} + +func (s *gaugeSnapshot) Value() float64 { + return s.value +} + +type timerSnapshot struct { + name string + tags map[string]string + values []time.Duration +} + +func (s *timerSnapshot) Name() string { + return s.name +} + +func (s *timerSnapshot) Tags() map[string]string { + return s.tags +} + +func (s *timerSnapshot) Values() []time.Duration { + return s.values +} + +type histogramSnapshot struct { + name string + tags map[string]string + values map[float64]int64 + durations map[time.Duration]int64 +} + +func (s *histogramSnapshot) Name() string { + return s.name +} + +func (s *histogramSnapshot) Tags() map[string]string { + return s.tags +} + +func (s *histogramSnapshot) Values() map[float64]int64 { + return s.values +} + +func (s *histogramSnapshot) Durations() map[time.Duration]int64 { + return s.durations +} diff --git a/vendor/github.com/uber-go/tally/stats.go b/vendor/github.com/uber-go/tally/stats.go new file mode 100644 index 00000000000..d30dec0f2f9 --- /dev/null +++ b/vendor/github.com/uber-go/tally/stats.go @@ -0,0 +1,475 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package tally + +import ( + "math" + "sort" + "sync" + "sync/atomic" + "time" +) + +var ( + capabilitiesNone = &capabilities{ + reporting: false, + tagging: false, + } + capabilitiesReportingNoTagging = &capabilities{ + reporting: true, + tagging: false, + } + capabilitiesReportingTagging = &capabilities{ + reporting: true, + tagging: true, + } +) + +type capabilities struct { + reporting bool + tagging bool +} + +func (c *capabilities) Reporting() bool { + return c.reporting +} + +func (c *capabilities) Tagging() bool { + return c.tagging +} + +type counter struct { + prev int64 + curr int64 + cachedCount CachedCount +} + +func newCounter(cachedCount CachedCount) *counter { + return &counter{cachedCount: cachedCount} +} + +func (c *counter) Inc(v int64) { + atomic.AddInt64(&c.curr, v) +} + +func (c *counter) value() int64 { + curr := atomic.LoadInt64(&c.curr) + + prev := atomic.LoadInt64(&c.prev) + if prev == curr { + return 0 + } + atomic.StoreInt64(&c.prev, curr) + return curr - prev +} + +func (c *counter) report(name string, tags map[string]string, r StatsReporter) { + delta := c.value() + if delta == 0 { + return + } + + r.ReportCounter(name, tags, delta) +} + +func (c *counter) cachedReport() { + delta := c.value() + if delta == 0 { + return + } + + c.cachedCount.ReportCount(delta) +} + +func (c *counter) snapshot() int64 { + return atomic.LoadInt64(&c.curr) - atomic.LoadInt64(&c.prev) +} + +type gauge struct { + updated uint64 + curr uint64 + cachedGauge CachedGauge +} + +func newGauge(cachedGauge CachedGauge) *gauge { + return &gauge{cachedGauge: cachedGauge} +} + +func (g *gauge) Update(v float64) { + atomic.StoreUint64(&g.curr, math.Float64bits(v)) + atomic.StoreUint64(&g.updated, 1) +} + +func (g *gauge) value() float64 { + return math.Float64frombits(atomic.LoadUint64(&g.curr)) +} + +func (g *gauge) report(name string, tags map[string]string, r StatsReporter) { + if atomic.SwapUint64(&g.updated, 0) == 1 { + r.ReportGauge(name, tags, g.value()) + } +} + +func (g *gauge) cachedReport() { + if atomic.SwapUint64(&g.updated, 0) == 1 { + g.cachedGauge.ReportGauge(g.value()) + } +} + +func (g *gauge) snapshot() float64 { + return math.Float64frombits(atomic.LoadUint64(&g.curr)) +} + +// NB(jra3): timers are a little special because they do no aggregate any data +// at the timer level. The reporter buffers may timer entries and periodically +// flushes. +type timer struct { + name string + tags map[string]string + reporter StatsReporter + cachedTimer CachedTimer + unreported timerValues +} + +type timerValues struct { + sync.RWMutex + values []time.Duration +} + +func newTimer( + name string, + tags map[string]string, + r StatsReporter, + cachedTimer CachedTimer, +) *timer { + t := &timer{ + name: name, + tags: tags, + reporter: r, + cachedTimer: cachedTimer, + } + if r == nil { + t.reporter = &timerNoReporterSink{timer: t} + } + return t +} + +func (t *timer) Record(interval time.Duration) { + if t.cachedTimer != nil { + t.cachedTimer.ReportTimer(interval) + } else { + t.reporter.ReportTimer(t.name, t.tags, interval) + } +} + +func (t *timer) Start() Stopwatch { + return NewStopwatch(globalClock.Now(), t) +} + +func (t *timer) RecordStopwatch(stopwatchStart time.Time) { + d := globalClock.Now().Sub(stopwatchStart) + t.Record(d) +} + +func (t *timer) snapshot() []time.Duration { + t.unreported.RLock() + snap := make([]time.Duration, len(t.unreported.values)) + for i := range t.unreported.values { + snap[i] = t.unreported.values[i] + } + t.unreported.RUnlock() + return snap +} + +type timerNoReporterSink struct { + sync.RWMutex + timer *timer +} + +func (r *timerNoReporterSink) ReportCounter( + name string, + tags map[string]string, + value int64, +) { +} + +func (r *timerNoReporterSink) ReportGauge( + name string, + tags map[string]string, + value float64, +) { +} + +func (r *timerNoReporterSink) ReportTimer( + name string, + tags map[string]string, + interval time.Duration, +) { + r.timer.unreported.Lock() + r.timer.unreported.values = append(r.timer.unreported.values, interval) + r.timer.unreported.Unlock() +} + +func (r *timerNoReporterSink) ReportHistogramValueSamples( + name string, + tags map[string]string, + buckets Buckets, + bucketLowerBound, + bucketUpperBound float64, + samples int64, +) { +} + +func (r *timerNoReporterSink) ReportHistogramDurationSamples( + name string, + tags map[string]string, + buckets Buckets, + bucketLowerBound, + bucketUpperBound time.Duration, + samples int64, +) { +} + +func (r *timerNoReporterSink) Capabilities() Capabilities { + return capabilitiesReportingTagging +} + +func (r *timerNoReporterSink) Flush() { +} + +type histogram struct { + htype histogramType + name string + tags map[string]string + reporter StatsReporter + specification Buckets + buckets []histogramBucket + lookupByValue []float64 + lookupByDuration []int +} + +type histogramType int + +const ( + valueHistogramType histogramType = iota + durationHistogramType +) + +func newHistogram( + name string, + tags map[string]string, + reporter StatsReporter, + buckets Buckets, + cachedHistogram CachedHistogram, +) *histogram { + htype := valueHistogramType + if _, ok := buckets.(DurationBuckets); ok { + htype = durationHistogramType + } + + pairs := BucketPairs(buckets) + + h := &histogram{ + htype: htype, + name: name, + tags: tags, + reporter: reporter, + specification: buckets, + buckets: make([]histogramBucket, 0, len(pairs)), + lookupByValue: make([]float64, 0, len(pairs)), + lookupByDuration: make([]int, 0, len(pairs)), + } + + for _, pair := range pairs { + h.addBucket(newHistogramBucket(h, + pair.LowerBoundValue(), pair.UpperBoundValue(), + pair.LowerBoundDuration(), pair.UpperBoundDuration(), + cachedHistogram)) + } + + return h +} + +func (h *histogram) addBucket(b histogramBucket) { + h.buckets = append(h.buckets, b) + h.lookupByValue = append(h.lookupByValue, b.valueUpperBound) + h.lookupByDuration = append(h.lookupByDuration, int(b.durationUpperBound)) +} + +func (h *histogram) report(name string, tags map[string]string, r StatsReporter) { + for i := range h.buckets { + samples := h.buckets[i].samples.value() + if samples == 0 { + continue + } + switch h.htype { + case valueHistogramType: + r.ReportHistogramValueSamples(name, tags, h.specification, + h.buckets[i].valueLowerBound, h.buckets[i].valueUpperBound, + samples) + case durationHistogramType: + r.ReportHistogramDurationSamples(name, tags, h.specification, + h.buckets[i].durationLowerBound, h.buckets[i].durationUpperBound, + samples) + } + } +} + +func (h *histogram) cachedReport() { + for i := range h.buckets { + samples := h.buckets[i].samples.value() + if samples == 0 { + continue + } + switch h.htype { + case valueHistogramType: + h.buckets[i].cachedValueBucket.ReportSamples(samples) + case durationHistogramType: + h.buckets[i].cachedDurationBucket.ReportSamples(samples) + } + } +} + +func (h *histogram) RecordValue(value float64) { + // Find the highest inclusive of the bucket upper bound + // and emit directly to it. Since we use BucketPairs to derive + // buckets there will always be an inclusive bucket as + // we always have a math.MaxFloat64 bucket. + idx := sort.SearchFloat64s(h.lookupByValue, value) + h.buckets[idx].samples.Inc(1) +} + +func (h *histogram) RecordDuration(value time.Duration) { + // Find the highest inclusive of the bucket upper bound + // and emit directly to it. Since we use BucketPairs to derive + // buckets there will always be an inclusive bucket as + // we always have a math.MaxInt64 bucket. + idx := sort.SearchInts(h.lookupByDuration, int(value)) + h.buckets[idx].samples.Inc(1) +} + +func (h *histogram) Start() Stopwatch { + return NewStopwatch(globalClock.Now(), h) +} + +func (h *histogram) RecordStopwatch(stopwatchStart time.Time) { + d := globalClock.Now().Sub(stopwatchStart) + h.RecordDuration(d) +} + +func (h *histogram) snapshotValues() map[float64]int64 { + if h.htype == durationHistogramType { + return nil + } + + vals := make(map[float64]int64, len(h.buckets)) + for i := range h.buckets { + vals[h.buckets[i].valueUpperBound] = h.buckets[i].samples.value() + } + + return vals +} + +func (h *histogram) snapshotDurations() map[time.Duration]int64 { + if h.htype == valueHistogramType { + return nil + } + + durations := make(map[time.Duration]int64, len(h.buckets)) + for i := range h.buckets { + durations[h.buckets[i].durationUpperBound] = h.buckets[i].samples.value() + } + + return durations +} + +type histogramBucket struct { + h *histogram + samples *counter + valueLowerBound float64 + valueUpperBound float64 + durationLowerBound time.Duration + durationUpperBound time.Duration + cachedValueBucket CachedHistogramBucket + cachedDurationBucket CachedHistogramBucket +} + +func newHistogramBucket( + h *histogram, + valueLowerBound, + valueUpperBound float64, + durationLowerBound, + durationUpperBound time.Duration, + cachedHistogram CachedHistogram, +) histogramBucket { + bucket := histogramBucket{ + samples: newCounter(nil), + valueLowerBound: valueLowerBound, + valueUpperBound: valueUpperBound, + durationLowerBound: durationLowerBound, + durationUpperBound: durationUpperBound, + } + if cachedHistogram != nil { + bucket.cachedValueBucket = cachedHistogram.ValueBucket( + bucket.valueLowerBound, bucket.valueUpperBound, + ) + bucket.cachedDurationBucket = cachedHistogram.DurationBucket( + bucket.durationLowerBound, bucket.durationUpperBound, + ) + } + return bucket +} + +// NullStatsReporter is an implementation of StatsReporter than simply does nothing. +var NullStatsReporter StatsReporter = nullStatsReporter{} + +func (r nullStatsReporter) ReportCounter(name string, tags map[string]string, value int64) { +} +func (r nullStatsReporter) ReportGauge(name string, tags map[string]string, value float64) { +} +func (r nullStatsReporter) ReportTimer(name string, tags map[string]string, interval time.Duration) { +} +func (r nullStatsReporter) ReportHistogramValueSamples( + name string, + tags map[string]string, + buckets Buckets, + bucketLowerBound, + bucketUpperBound float64, + samples int64, +) { +} + +func (r nullStatsReporter) ReportHistogramDurationSamples( + name string, + tags map[string]string, + buckets Buckets, + bucketLowerBound, + bucketUpperBound time.Duration, + samples int64, +) { +} +func (r nullStatsReporter) Capabilities() Capabilities { + return capabilitiesNone +} +func (r nullStatsReporter) Flush() { +} + +type nullStatsReporter struct{} diff --git a/vendor/github.com/uber-go/tally/statsd/README.md b/vendor/github.com/uber-go/tally/statsd/README.md new file mode 100644 index 00000000000..af2df15c32b --- /dev/null +++ b/vendor/github.com/uber-go/tally/statsd/README.md @@ -0,0 +1,29 @@ +# A buffered statsd reporter + +See `examples/statsd_main.go` for an end to end example. + +Some emitted stats using the example listening with `nc 8125 -l -u`: + +``` +stats.my-service.test-histogram.100ms-200ms:2|c +stats.my-service.test-histogram.300ms-400ms:1|c +stats.my-service.test-histogram.600ms-800ms:1|c +stats.my-service.test-counter:1|c +stats.my-service.test-gauge:813|g +``` + +## Options + +You can use either a basic or a buffered statsd client +and pass it to the reporter along with options. + +The reporter options are: + +```go +// Options is a set of options for the tally reporter. +type Options struct { + // SampleRate is the metrics emission sample rate. If you + // do not set this value it will be set to 1. + SampleRate float32 +} +``` diff --git a/vendor/github.com/uber-go/tally/statsd/reporter.go b/vendor/github.com/uber-go/tally/statsd/reporter.go new file mode 100644 index 00000000000..78ba7004e6e --- /dev/null +++ b/vendor/github.com/uber-go/tally/statsd/reporter.go @@ -0,0 +1,156 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package statsd + +import ( + "fmt" + "math" + "strconv" + "time" + + "github.com/uber-go/tally" + + "github.com/cactus/go-statsd-client/statsd" +) + +const ( + // DefaultHistogramBucketNamePrecision is the default + // precision to use when formatting the metric name + // with the histogram bucket bound values. + DefaultHistogramBucketNamePrecision = uint(6) +) + +type cactusStatsReporter struct { + statter statsd.Statter + sampleRate float32 + bucketFmt string +} + +// Options is a set of options for the tally reporter. +type Options struct { + // SampleRate is the metrics emission sample rate. If you + // do not set this value it will be set to 1. + SampleRate float32 + + // HistogramBucketNamePrecision is the precision to use when + // formatting the metric name with the histogram bucket bound values. + // By default this will be set to the const DefaultHistogramBucketPrecision. + HistogramBucketNamePrecision uint +} + +// NewReporter wraps a statsd.Statter for use with tally. Use either +// statsd.NewClient or statsd.NewBufferedClient. +func NewReporter(statsd statsd.Statter, opts Options) tally.StatsReporter { + var nilSampleRate float32 + if opts.SampleRate == nilSampleRate { + opts.SampleRate = 1.0 + } + if opts.HistogramBucketNamePrecision == 0 { + opts.HistogramBucketNamePrecision = DefaultHistogramBucketNamePrecision + } + return &cactusStatsReporter{ + statter: statsd, + sampleRate: opts.SampleRate, + bucketFmt: "%." + strconv.Itoa(int(opts.HistogramBucketNamePrecision)) + "f", + } +} + +func (r *cactusStatsReporter) ReportCounter(name string, tags map[string]string, value int64) { + r.statter.Inc(name, value, r.sampleRate) +} + +func (r *cactusStatsReporter) ReportGauge(name string, tags map[string]string, value float64) { + r.statter.Gauge(name, int64(value), r.sampleRate) +} + +func (r *cactusStatsReporter) ReportTimer(name string, tags map[string]string, interval time.Duration) { + r.statter.TimingDuration(name, interval, r.sampleRate) +} + +func (r *cactusStatsReporter) ReportHistogramValueSamples( + name string, + tags map[string]string, + buckets tally.Buckets, + bucketLowerBound, + bucketUpperBound float64, + samples int64, +) { + r.statter.Inc( + fmt.Sprintf("%s.%s-%s", name, + r.valueBucketString(bucketLowerBound), + r.valueBucketString(bucketUpperBound)), + samples, r.sampleRate) +} + +func (r *cactusStatsReporter) ReportHistogramDurationSamples( + name string, + tags map[string]string, + buckets tally.Buckets, + bucketLowerBound, + bucketUpperBound time.Duration, + samples int64, +) { + r.statter.Inc( + fmt.Sprintf("%s.%s-%s", name, + r.durationBucketString(bucketLowerBound), + r.durationBucketString(bucketUpperBound)), + samples, r.sampleRate) +} + +func (r *cactusStatsReporter) valueBucketString( + upperBound float64, +) string { + if upperBound == math.MaxFloat64 { + return "infinity" + } + if upperBound == -math.MaxFloat64 { + return "-infinity" + } + return fmt.Sprintf(r.bucketFmt, upperBound) +} + +func (r *cactusStatsReporter) durationBucketString( + upperBound time.Duration, +) string { + if upperBound == time.Duration(math.MaxInt64) { + return "infinity" + } + if upperBound == time.Duration(math.MinInt64) { + return "-infinity" + } + return upperBound.String() +} + +func (r *cactusStatsReporter) Capabilities() tally.Capabilities { + return r +} + +func (r *cactusStatsReporter) Reporting() bool { + return true +} + +func (r *cactusStatsReporter) Tagging() bool { + return false +} + +func (r *cactusStatsReporter) Flush() { + // no-op +} diff --git a/vendor/github.com/uber-go/tally/types.go b/vendor/github.com/uber-go/tally/types.go new file mode 100644 index 00000000000..c54b87b9360 --- /dev/null +++ b/vendor/github.com/uber-go/tally/types.go @@ -0,0 +1,152 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package tally + +import ( + "fmt" + "sort" + "time" +) + +// Scope is a namespace wrapper around a stats reporter, ensuring that +// all emitted values have a given prefix or set of tags. +type Scope interface { + // Counter returns the Counter object corresponding to the name. + Counter(name string) Counter + + // Gauge returns the Gauge object corresponding to the name. + Gauge(name string) Gauge + + // Timer returns the Timer object corresponding to the name. + Timer(name string) Timer + + // Histogram returns the Histogram object corresponding to the name. + // To use default value and duration buckets configured for the scope + // simply pass tally.DefaultBuckets or nil. + // You can use tally.ValueBuckets{x, y, ...} for value buckets. + // You can use tally.DurationBuckets{x, y, ...} for duration buckets. + // You can use tally.MustMakeLinearValueBuckets(start, width, count) for linear values. + // You can use tally.MustMakeLinearDurationBuckets(start, width, count) for linear durations. + // You can use tally.MustMakeExponentialValueBuckets(start, factor, count) for exponential values. + // You can use tally.MustMakeExponentialDurationBuckets(start, factor, count) for exponential durations. + Histogram(name string, buckets Buckets) Histogram + + // Tagged returns a new child scope with the given tags and current tags. + Tagged(tags map[string]string) Scope + + // SubScope returns a new child scope appending a further name prefix. + SubScope(name string) Scope + + // Capabilities returns a description of metrics reporting capabilities. + Capabilities() Capabilities +} + +// Counter is the interface for emitting counter type metrics. +type Counter interface { + // Inc increments the counter by a delta. + Inc(delta int64) +} + +// Gauge is the interface for emitting gauge metrics. +type Gauge interface { + // Update sets the gauges absolute value. + Update(value float64) +} + +// Timer is the interface for emitting timer metrics. +type Timer interface { + // Record a specific duration directly. + Record(value time.Duration) + + // Start gives you back a specific point in time to report via Stop. + Start() Stopwatch +} + +// Histogram is the interface for emitting histogram metrics +type Histogram interface { + // RecordValue records a specific value directly. + // Will use the configured value buckets for the histogram. + RecordValue(value float64) + + // RecordDuration records a specific duration directly. + // Will use the configured duration buckets for the histogram. + RecordDuration(value time.Duration) + + // Start gives you a specific point in time to then record a duration. + // Will use the configured duration buckets for the histogram. + Start() Stopwatch +} + +// Stopwatch is a helper for simpler tracking of elapsed time, use the +// Stop() method to report time elapsed since its created back to the +// timer or histogram. +type Stopwatch struct { + start time.Time + recorder StopwatchRecorder +} + +// NewStopwatch creates a new immutable stopwatch for recording the start +// time to a stopwatch reporter. +func NewStopwatch(start time.Time, r StopwatchRecorder) Stopwatch { + return Stopwatch{start: start, recorder: r} +} + +// Stop reports time elapsed since the stopwatch start to the recorder. +func (sw Stopwatch) Stop() { + sw.recorder.RecordStopwatch(sw.start) +} + +// StopwatchRecorder is a recorder that is called when a stopwatch is +// stopped with Stop(). +type StopwatchRecorder interface { + RecordStopwatch(stopwatchStart time.Time) +} + +// Buckets is an interface that can represent a set of buckets +// either as float64s or as durations. +type Buckets interface { + fmt.Stringer + sort.Interface + + // AsValues returns a representation of the buckets as float64s + AsValues() []float64 + + // AsDurations returns a representation of the buckets as time.Durations + AsDurations() []time.Duration +} + +// BucketPair describes the lower and upper bounds +// for a derived bucket from a buckets set. +type BucketPair interface { + LowerBoundValue() float64 + UpperBoundValue() float64 + LowerBoundDuration() time.Duration + UpperBoundDuration() time.Duration +} + +// Capabilities is a description of metrics reporting capabilities. +type Capabilities interface { + // Reporting returns whether the reporter has the ability to actively report. + Reporting() bool + + // Tagging returns whether the reporter has the capability for tagged metrics. + Tagging() bool +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 15478a1f417..9a694d13013 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -187,6 +187,12 @@ "revision": "44cc805cf13205b55f69e14bcb69867d1ae92f98", "revisionTime": "2016-08-05T00:47:13Z" }, + { + "checksumSHA1": "imR2wF388/0fBU6RRWx8RvTi8Q8=", + "path": "github.com/facebookgo/clock", + "revision": "600d898af40aa09a7a93ecb9265d87b0504b6f03", + "revisionTime": "2015-04-10T01:09:13Z" + }, { "checksumSHA1": "uhWARwDA6NE60a3/snaz9QBM7Ow=", "path": "github.com/fsouza/go-dockerclient", @@ -535,6 +541,24 @@ "revision": "6b4daa5362b502898ddf367c5c11deb9e7a5c727", "revisionTime": "2016-10-11T05:00:08Z" }, + { + "checksumSHA1": "U35lQ4bMD3tB9Z8dqaqfeg9BxhQ=", + "path": "github.com/uber-go/tally", + "revision": "88e9c75b0cfc84139ad1bae3b7f123786cfd0770", + "revisionTime": "2017-06-23T13:27:35Z" + }, + { + "checksumSHA1": "MODwNJLvl0ZIi77yVjqY2BkjDJI=", + "path": "github.com/uber-go/tally/prometheus", + "revision": "88e9c75b0cfc84139ad1bae3b7f123786cfd0770", + "revisionTime": "2017-06-23T13:27:35Z" + }, + { + "checksumSHA1": "uogeey5t5IENcDuWeLalQ3GlIgA=", + "path": "github.com/uber-go/tally/statsd", + "revision": "88e9c75b0cfc84139ad1bae3b7f123786cfd0770", + "revisionTime": "2017-06-23T13:27:35Z" + }, { "path": "golang.org/x/crypto/hkdf", "revision": "c8b9e6388ef638d5a8a9d865c634befdc46a6784",