diff --git a/metrics/adapters/cache.go b/metrics/adapters/cache.go index 6f2bf2e..f128e62 100644 --- a/metrics/adapters/cache.go +++ b/metrics/adapters/cache.go @@ -21,17 +21,19 @@ import ( ) type cache struct { - lock sync.Mutex - counters map[string]metrics.Counter - gauges map[string]metrics.Gauge - timers map[string]metrics.Timer + lock sync.Mutex + counters map[string]metrics.Counter + gauges map[string]metrics.Gauge + timers map[string]metrics.Timer + histograms map[string]metrics.Histogram } func newCache() *cache { return &cache{ - counters: make(map[string]metrics.Counter), - gauges: make(map[string]metrics.Gauge), - timers: make(map[string]metrics.Timer), + counters: make(map[string]metrics.Counter), + gauges: make(map[string]metrics.Gauge), + timers: make(map[string]metrics.Timer), + histograms: make(map[string]metrics.Histogram), } } @@ -67,3 +69,14 @@ func (r *cache) getOrSetTimer(name string, create func() metrics.Timer) metrics. } return t } + +func (r *cache) getOrSetHistogram(name string, create func() metrics.Histogram) metrics.Histogram { + r.lock.Lock() + defer r.lock.Unlock() + t, ok := r.histograms[name] + if !ok { + t = create() + r.histograms[name] = t + } + return t +} diff --git a/metrics/adapters/cache_test.go b/metrics/adapters/cache_test.go index dc79a66..94e0e13 100644 --- a/metrics/adapters/cache_test.go +++ b/metrics/adapters/cache_test.go @@ -28,7 +28,8 @@ func TestCache(t *testing.T) { f := metricstest.NewFactory(100 * time.Second) c1 := f.Counter(metrics.Options{Name: "x"}) g1 := f.Gauge(metrics.Options{Name: "y"}) - t1 := f.Timer(metrics.Options{Name: "z"}) + t1 := f.Timer(metrics.TimerOptions{Name: "z"}) + h1 := f.Histogram(metrics.HistogramOptions{Name: "h"}) c := newCache() @@ -38,6 +39,8 @@ func TestCache(t *testing.T) { assert.Equal(t, g1, g2) t2 := c.getOrSetTimer("z", func() metrics.Timer { return t1 }) assert.Equal(t, t1, t2) + h2 := c.getOrSetHistogram("h", func() metrics.Histogram { return h1 }) + assert.Equal(t, h1, h2) c3 := c.getOrSetCounter("x", func() metrics.Counter { panic("c1") }) assert.Equal(t, c1, c3) @@ -45,4 +48,6 @@ func TestCache(t *testing.T) { assert.Equal(t, g1, g3) t3 := c.getOrSetTimer("z", func() metrics.Timer { panic("t1") }) assert.Equal(t, t1, t3) + h3 := c.getOrSetHistogram("h", func() metrics.Histogram { panic("h1") }) + assert.Equal(t, h1, h3) } diff --git a/metrics/adapters/factory.go b/metrics/adapters/factory.go index e054fa2..85e71b8 100644 --- a/metrics/adapters/factory.go +++ b/metrics/adapters/factory.go @@ -20,9 +20,10 @@ import ( // FactoryWithTags creates metrics with fully qualified name and tags. type FactoryWithTags interface { - Counter(name string, tags map[string]string, help string) metrics.Counter - Gauge(name string, tags map[string]string, help string) metrics.Gauge - Timer(name string, tags map[string]string, help string) metrics.Timer + Counter(options metrics.Options) metrics.Counter + Gauge(options metrics.Options) metrics.Gauge + Timer(options metrics.TimerOptions) metrics.Timer + Histogram(options metrics.HistogramOptions) metrics.Histogram } // Options affect how the adapter factory behaves. @@ -66,21 +67,46 @@ type factory struct { func (f *factory) Counter(options metrics.Options) metrics.Counter { fullName, fullTags, key := f.getKey(options.Name, options.Tags) return f.cache.getOrSetCounter(key, func() metrics.Counter { - return f.factory.Counter(fullName, fullTags, options.Help) + return f.factory.Counter(metrics.Options{ + Name: fullName, + Tags: fullTags, + Help: options.Help, + }) }) } func (f *factory) Gauge(options metrics.Options) metrics.Gauge { fullName, fullTags, key := f.getKey(options.Name, options.Tags) return f.cache.getOrSetGauge(key, func() metrics.Gauge { - return f.factory.Gauge(fullName, fullTags, options.Help) + return f.factory.Gauge(metrics.Options{ + Name: fullName, + Tags: fullTags, + Help: options.Help, + }) }) } -func (f *factory) Timer(options metrics.Options) metrics.Timer { +func (f *factory) Timer(options metrics.TimerOptions) metrics.Timer { fullName, fullTags, key := f.getKey(options.Name, options.Tags) return f.cache.getOrSetTimer(key, func() metrics.Timer { - return f.factory.Timer(fullName, fullTags, options.Help) + return f.factory.Timer(metrics.TimerOptions{ + Name: fullName, + Tags: fullTags, + Help: options.Help, + Buckets: options.Buckets, + }) + }) +} + +func (f *factory) Histogram(options metrics.HistogramOptions) metrics.Histogram { + fullName, fullTags, key := f.getKey(options.Name, options.Tags) + return f.cache.getOrSetHistogram(key, func() metrics.Histogram { + return f.factory.Histogram(metrics.HistogramOptions{ + Name: fullName, + Tags: fullTags, + Help: options.Help, + Buckets: options.Buckets, + }) }) } diff --git a/metrics/adapters/factory_test.go b/metrics/adapters/factory_test.go index 89f9156..4093566 100644 --- a/metrics/adapters/factory_test.go +++ b/metrics/adapters/factory_test.go @@ -44,9 +44,10 @@ func TestSubScope(t *testing.T) { func TestFactory(t *testing.T) { var ( - counterPrefix = "counter_" - gaugePrefix = "gauge_" - timerPrefix = "timer_" + counterPrefix = "counter_" + gaugePrefix = "gauge_" + timerPrefix = "timer_" + histogramPrefix = "histogram_" tagsA = map[string]string{"a": "b"} tagsX = map[string]string{"x": "y"} @@ -55,6 +56,8 @@ func TestFactory(t *testing.T) { testCases := []struct { name string tags map[string]string + buckets []float64 + durationBuckets []time.Duration namespace string nsTags map[string]string fullName string @@ -93,9 +96,15 @@ func TestFactory(t *testing.T) { Name: gaugePrefix + testCase.name, Tags: testCase.tags, }) - timer := f.Timer(metrics.Options{ - Name: timerPrefix + testCase.name, - Tags: testCase.tags, + timer := f.Timer(metrics.TimerOptions{ + Name: timerPrefix + testCase.name, + Tags: testCase.tags, + Buckets: testCase.durationBuckets, + }) + histogram := f.Histogram(metrics.HistogramOptions{ + Name: histogramPrefix + testCase.name, + Tags: testCase.tags, + Buckets: testCase.buckets, }) assert.Equal(t, counter, f.Counter(metrics.Options{ @@ -106,45 +115,63 @@ func TestFactory(t *testing.T) { Name: gaugePrefix + testCase.name, Tags: testCase.tags, })) - assert.Equal(t, timer, f.Timer(metrics.Options{ - Name: timerPrefix + testCase.name, - Tags: testCase.tags, + assert.Equal(t, timer, f.Timer(metrics.TimerOptions{ + Name: timerPrefix + testCase.name, + Tags: testCase.tags, + Buckets: testCase.durationBuckets, + })) + assert.Equal(t, histogram, f.Histogram(metrics.HistogramOptions{ + Name: histogramPrefix + testCase.name, + Tags: testCase.tags, + Buckets: testCase.buckets, })) assert.Equal(t, fmt.Sprintf(testCase.fullName, counterPrefix), ff.counter) assert.Equal(t, fmt.Sprintf(testCase.fullName, gaugePrefix), ff.gauge) assert.Equal(t, fmt.Sprintf(testCase.fullName, timerPrefix), ff.timer) + assert.Equal(t, fmt.Sprintf(testCase.fullName, histogramPrefix), ff.histogram) }) } } type fakeTagless struct { - factory metrics.Factory - counter string - gauge string - timer string + factory metrics.Factory + counter string + gauge string + timer string + histogram string } -func (f *fakeTagless) Counter(name string, help string) metrics.Counter { - f.counter = name +func (f *fakeTagless) Counter(options TaglessOptions) metrics.Counter { + f.counter = options.Name return f.factory.Counter(metrics.Options{ - Name: name, - Help: help, + Name: options.Name, + Help: options.Help, }) } -func (f *fakeTagless) Gauge(name string, help string) metrics.Gauge { - f.gauge = name +func (f *fakeTagless) Gauge(options TaglessOptions) metrics.Gauge { + f.gauge = options.Name return f.factory.Gauge(metrics.Options{ - Name: name, - Help: help, + Name: options.Name, + Help: options.Help, + }) +} + +func (f *fakeTagless) Timer(options TaglessTimerOptions) metrics.Timer { + f.timer = options.Name + return f.factory.Timer(metrics.TimerOptions{ + Name: options.Name, + Help: options.Help, + Buckets: options.Buckets, }) } -func (f *fakeTagless) Timer(name string, help string) metrics.Timer { - f.timer = name - return f.factory.Timer(metrics.Options{ - Name: name, - Help: help, +func (f *fakeTagless) Histogram(options TaglessHistogramOptions) metrics.Histogram { + f.histogram = options.Name + return f.factory.Histogram(metrics.HistogramOptions{ + Name: options.Name, + Help: options.Help, + Buckets: options.Buckets, }) } diff --git a/metrics/adapters/tagless.go b/metrics/adapters/tagless.go index d9552c7..5496058 100644 --- a/metrics/adapters/tagless.go +++ b/metrics/adapters/tagless.go @@ -14,14 +14,39 @@ package adapters -import "github.com/uber/jaeger-lib/metrics" +import ( + "time" + + "github.com/uber/jaeger-lib/metrics" +) + +// TaglessOptions defines the information associated with a metric +type TaglessOptions struct { + Name string + Help string +} + +// TaglessTimerOptions defines the information associated with a metric +type TaglessTimerOptions struct { + Name string + Help string + Buckets []time.Duration +} + +// TaglessHistogramOptions defines the information associated with a metric +type TaglessHistogramOptions struct { + Name string + Help string + Buckets []float64 +} // FactoryWithoutTags creates metrics based on name only, without tags. // Suitable for integrating with statsd-like backends that don't support tags. type FactoryWithoutTags interface { - Counter(name string, help string) metrics.Counter - Gauge(name string, help string) metrics.Gauge - Timer(name string, help string) metrics.Timer + Counter(options TaglessOptions) metrics.Counter + Gauge(options TaglessOptions) metrics.Gauge + Timer(options TaglessTimerOptions) metrics.Timer + Histogram(options TaglessHistogramOptions) metrics.Histogram } // WrapFactoryWithoutTags creates a real metrics.Factory that supports subscopes. @@ -41,19 +66,38 @@ type tagless struct { factory FactoryWithoutTags } -func (f *tagless) Counter(name string, tags map[string]string, help string) metrics.Counter { - fullName := f.getFullName(name, tags) - return f.factory.Counter(fullName, help) +func (f *tagless) Counter(options metrics.Options) metrics.Counter { + fullName := f.getFullName(options.Name, options.Tags) + return f.factory.Counter(TaglessOptions{ + Name: fullName, + Help: options.Help, + }) +} + +func (f *tagless) Gauge(options metrics.Options) metrics.Gauge { + fullName := f.getFullName(options.Name, options.Tags) + return f.factory.Gauge(TaglessOptions{ + Name: fullName, + Help: options.Help, + }) } -func (f *tagless) Gauge(name string, tags map[string]string, help string) metrics.Gauge { - fullName := f.getFullName(name, tags) - return f.factory.Gauge(fullName, help) +func (f *tagless) Timer(options metrics.TimerOptions) metrics.Timer { + fullName := f.getFullName(options.Name, options.Tags) + return f.factory.Timer(TaglessTimerOptions{ + Name: fullName, + Help: options.Help, + Buckets: options.Buckets, + }) } -func (f *tagless) Timer(name string, tags map[string]string, help string) metrics.Timer { - fullName := f.getFullName(name, tags) - return f.factory.Timer(fullName, help) +func (f *tagless) Histogram(options metrics.HistogramOptions) metrics.Histogram { + fullName := f.getFullName(options.Name, options.Tags) + return f.factory.Histogram(TaglessHistogramOptions{ + Name: fullName, + Help: options.Help, + Buckets: options.Buckets, + }) } func (f *tagless) getFullName(name string, tags map[string]string) string { diff --git a/metrics/expvar/factory.go b/metrics/expvar/factory.go index 6b42d78..633354c 100644 --- a/metrics/expvar/factory.go +++ b/metrics/expvar/factory.go @@ -36,14 +36,20 @@ type factory struct { factory xkit.Factory } -func (f *factory) Counter(name string, help string) metrics.Counter { - return xkit.NewCounter(f.factory.Counter(name)) +func (f *factory) Counter(options adapters.TaglessOptions) metrics.Counter { + return xkit.NewCounter(f.factory.Counter(options.Name)) } -func (f *factory) Gauge(name string, help string) metrics.Gauge { - return xkit.NewGauge(f.factory.Gauge(name)) +func (f *factory) Gauge(options adapters.TaglessOptions) metrics.Gauge { + return xkit.NewGauge(f.factory.Gauge(options.Name)) } -func (f *factory) Timer(name string, help string) metrics.Timer { - return xkit.NewTimer(f.factory.Histogram(name)) +func (f *factory) Timer(options adapters.TaglessTimerOptions) metrics.Timer { + // TODO: Provide support for buckets + return xkit.NewTimer(f.factory.Histogram(options.Name)) +} + +func (f *factory) Histogram(options adapters.TaglessHistogramOptions) metrics.Histogram { + // TODO: Provide support for buckets + return xkit.NewHistogram(f.factory.Histogram(options.Name)) } diff --git a/metrics/expvar/factory_test.go b/metrics/expvar/factory_test.go index ae6650b..b9741a8 100644 --- a/metrics/expvar/factory_test.go +++ b/metrics/expvar/factory_test.go @@ -26,35 +26,39 @@ import ( ) var ( - id = time.Now().UnixNano() - prefix = fmt.Sprintf("test_%d", id) - counterPrefix = prefix + "_counter_" - gaugePrefix = prefix + "_gauge_" - timerPrefix = prefix + "_timer_" + id = time.Now().UnixNano() + prefix = fmt.Sprintf("test_%d", id) + counterPrefix = prefix + "_counter_" + gaugePrefix = prefix + "_gauge_" + timerPrefix = prefix + "_timer_" + histogramPrefix = prefix + "_histogram_" tagsA = map[string]string{"a": "b"} tagsX = map[string]string{"x": "y"} ) func TestFactory(t *testing.T) { + buckets := []float64{10, 20, 30, 40, 50, 60} testCases := []struct { name string tags map[string]string + buckets []float64 + durationBuckets []time.Duration namespace string nsTags map[string]string fullName string expectedCounter string }{ - {name: "x", fullName: "%sx"}, - {tags: tagsX, fullName: "%s.x_y"}, - {name: "x", tags: tagsA, fullName: "%sx.a_b"}, - {namespace: "y", fullName: "y.%s"}, - {nsTags: tagsA, fullName: "%s.a_b"}, - {namespace: "y", nsTags: tagsX, fullName: "y.%s.x_y"}, - {name: "x", namespace: "y", nsTags: tagsX, fullName: "y.%sx.x_y"}, - {name: "x", tags: tagsX, namespace: "y", nsTags: tagsX, fullName: "y.%sx.x_y", expectedCounter: "84"}, - {name: "x", tags: tagsA, namespace: "y", nsTags: tagsX, fullName: "y.%sx.a_b.x_y"}, - {name: "x", tags: tagsX, namespace: "y", nsTags: tagsA, fullName: "y.%sx.a_b.x_y", expectedCounter: "84"}, + {name: "x", fullName: "%sx", buckets: buckets}, + {tags: tagsX, fullName: "%s.x_y", buckets: buckets}, + {name: "x", tags: tagsA, fullName: "%sx.a_b", buckets: buckets}, + {namespace: "y", fullName: "y.%s", buckets: buckets}, + {nsTags: tagsA, fullName: "%s.a_b", buckets: buckets}, + {namespace: "y", nsTags: tagsX, fullName: "y.%s.x_y", buckets: buckets}, + {name: "x", namespace: "y", nsTags: tagsX, fullName: "y.%sx.x_y", buckets: buckets}, + {name: "x", tags: tagsX, namespace: "y", nsTags: tagsX, fullName: "y.%sx.x_y", expectedCounter: "84", buckets: buckets}, + {name: "x", tags: tagsA, namespace: "y", nsTags: tagsX, fullName: "y.%sx.a_b.x_y", buckets: buckets}, + {name: "x", tags: tagsX, namespace: "y", nsTags: tagsA, fullName: "y.%sx.a_b.x_y", expectedCounter: "84", buckets: buckets}, } f := NewFactory(2) for _, testCase := range testCases { @@ -77,9 +81,15 @@ func TestFactory(t *testing.T) { Name: gaugePrefix + testCase.name, Tags: testCase.tags, }) - timer := ff.Timer(metrics.Options{ - Name: timerPrefix + testCase.name, - Tags: testCase.tags, + timer := ff.Timer(metrics.TimerOptions{ + Name: timerPrefix + testCase.name, + Tags: testCase.tags, + Buckets: testCase.durationBuckets, + }) + histogram := ff.Histogram(metrics.HistogramOptions{ + Name: histogramPrefix + testCase.name, + Tags: testCase.tags, + Buckets: testCase.buckets, }) // register second time, should not panic @@ -91,18 +101,26 @@ func TestFactory(t *testing.T) { Name: gaugePrefix + testCase.name, Tags: testCase.tags, }) - ff.Timer(metrics.Options{ - Name: timerPrefix + testCase.name, - Tags: testCase.tags, + ff.Timer(metrics.TimerOptions{ + Name: timerPrefix + testCase.name, + Tags: testCase.tags, + Buckets: testCase.durationBuckets, + }) + ff.Histogram(metrics.HistogramOptions{ + Name: histogramPrefix + testCase.name, + Tags: testCase.tags, + Buckets: testCase.buckets, }) counter.Inc(42) gauge.Update(42) timer.Record(42 * time.Millisecond) + histogram.Record(42) assertExpvar(t, fmt.Sprintf(testCase.fullName, counterPrefix), testCase.expectedCounter) assertExpvar(t, fmt.Sprintf(testCase.fullName, gaugePrefix), "42") assertExpvar(t, fmt.Sprintf(testCase.fullName, timerPrefix)+".p99", "0.042") + assertExpvar(t, fmt.Sprintf(testCase.fullName, histogramPrefix)+".p99", "42") }) } } diff --git a/metrics/factory.go b/metrics/factory.go index 7cca820..0ead061 100644 --- a/metrics/factory.go +++ b/metrics/factory.go @@ -14,7 +14,11 @@ package metrics -// NSOptions defines the name and tags map associated with a metric +import ( + "time" +) + +// NSOptions defines the name and tags map associated with a factory namespace type NSOptions struct { Name string Tags map[string]string @@ -27,11 +31,28 @@ type Options struct { Help string } +// TimerOptions defines the information associated with a metric +type TimerOptions struct { + Name string + Tags map[string]string + Help string + Buckets []time.Duration +} + +// HistogramOptions defines the information associated with a metric +type HistogramOptions struct { + Name string + Tags map[string]string + Help string + Buckets []float64 +} + // Factory creates new metrics type Factory interface { Counter(metric Options) Counter - Timer(metric Options) Timer + Timer(metric TimerOptions) Timer Gauge(metric Options) Gauge + Histogram(metric HistogramOptions) Histogram // Namespace returns a nested metrics factory. Namespace(scope NSOptions) Factory @@ -45,10 +66,13 @@ type nullFactory struct{} func (nullFactory) Counter(options Options) Counter { return NullCounter } -func (nullFactory) Timer(options Options) Timer { +func (nullFactory) Timer(options TimerOptions) Timer { return NullTimer } func (nullFactory) Gauge(options Options) Gauge { return NullGauge } +func (nullFactory) Histogram(options HistogramOptions) Histogram { + return NullHistogram +} func (nullFactory) Namespace(scope NSOptions) Factory { return NullFactory } diff --git a/metrics/go-kit/factory.go b/metrics/go-kit/factory.go index 30aa5c2..1f137de 100644 --- a/metrics/go-kit/factory.go +++ b/metrics/go-kit/factory.go @@ -112,7 +112,7 @@ func (f *factory) Counter(options metrics.Options) metrics.Counter { return NewCounter(counter) } -func (f *factory) Timer(options metrics.Options) metrics.Timer { +func (f *factory) Timer(options metrics.TimerOptions) metrics.Timer { name, tagsList := f.nameAndTagsList(options.Name, options.Tags) hist := f.factory.Histogram(name) if len(tagsList) > 0 { @@ -130,6 +130,15 @@ func (f *factory) Gauge(options metrics.Options) metrics.Gauge { return NewGauge(gauge) } +func (f *factory) Histogram(options metrics.HistogramOptions) metrics.Histogram { + name, tagsList := f.nameAndTagsList(options.Name, options.Tags) + hist := f.factory.Histogram(name) + if len(tagsList) > 0 { + hist = hist.With(tagsList...) + } + return NewHistogram(hist) +} + func (f *factory) Namespace(scope metrics.NSOptions) metrics.Factory { return &factory{ scope: f.subScope(scope.Name), diff --git a/metrics/go-kit/factory_test.go b/metrics/go-kit/factory_test.go index dcc5bd6..746f127 100644 --- a/metrics/go-kit/factory_test.go +++ b/metrics/go-kit/factory_test.go @@ -57,9 +57,11 @@ type metricFunc func(t *testing.T, testCase testCase, f metrics.Factory) (name f type testCase struct { f Factory - prefix string - name string - tags Tags + prefix string + name string + tags Tags + buckets []float64 + durationBuckets []time.Duration useNamespace bool namespace string @@ -80,6 +82,7 @@ func TestFactoryScoping(t *testing.T) { {"counter", testCounter}, {"gauge", testGauge}, {"timer", testTimer}, + {"histogram", testHistogram}, } for _, ts := range testSuites { testSuite := ts // capture loop var @@ -201,9 +204,10 @@ func testGauge(t *testing.T, testCase testCase, f metrics.Factory) (name func() } func testTimer(t *testing.T, testCase testCase, f metrics.Factory) (name func() string, labels func() []string) { - tm := f.Timer(metrics.Options{ - Name: testCase.name, - Tags: testCase.tags, + tm := f.Timer(metrics.TimerOptions{ + Name: testCase.name, + Tags: testCase.tags, + Buckets: testCase.durationBuckets, }) tm.Record(123 * time.Millisecond) gt := tm.(*Timer).hist.(*generic.Histogram) @@ -212,3 +216,17 @@ func testTimer(t *testing.T, testCase testCase, f metrics.Factory) (name func() labels = gt.LabelValues return } + +func testHistogram(t *testing.T, testCase testCase, f metrics.Factory) (name func() string, labels func() []string) { + histogram := f.Histogram(metrics.HistogramOptions{ + Name: testCase.name, + Tags: testCase.tags, + Buckets: testCase.buckets, + }) + histogram.Record(123) + gt := histogram.(*Histogram).hist.(*generic.Histogram) + assert.InDelta(t, 123, gt.Quantile(0.9), 0.00001) + name = func() string { return gt.Name } + labels = gt.LabelValues + return +} diff --git a/metrics/go-kit/influx/factory_test.go b/metrics/go-kit/influx/factory_test.go index 3f29424..21c4753 100644 --- a/metrics/go-kit/influx/factory_test.go +++ b/metrics/go-kit/influx/factory_test.go @@ -48,7 +48,7 @@ func TestTimer(t *testing.T) { inf := NewFactory(in) wf := xkit.Wrap("namespace", inf) - timer := wf.Timer(metrics.Options{ + timer := wf.Timer(metrics.TimerOptions{ Name: "gokit.infl-timer", Tags: map[string]string{"x": "y"}, }) @@ -59,6 +59,22 @@ func TestTimer(t *testing.T) { assert.Contains(t, reportToString(in), "namespace.gokit.infl-timer,x=y p50=1,p90=10,p95=10,p99=10") } +func TestHistogram(t *testing.T) { + in := influx.New(map[string]string{}, influxdb.BatchPointsConfig{}, log.NewNopLogger()) + inf := NewFactory(in) + wf := xkit.Wrap("namespace", inf) + + histogram := wf.Histogram(metrics.HistogramOptions{ + Name: "gokit.infl-histogram", + Tags: map[string]string{"x": "y"}, + }) + histogram.Record(1) + histogram.Record(1) + histogram.Record(10) + + assert.Contains(t, reportToString(in), "namespace.gokit.infl-histogram,x=y p50=1,p90=10,p95=10,p99=10") +} + func TestWrapperNamespaces(t *testing.T) { in := influx.New(map[string]string{}, influxdb.BatchPointsConfig{}, log.NewNopLogger()) inf := NewFactory(in) diff --git a/metrics/go-kit/metrics.go b/metrics/go-kit/metrics.go index 3b55b8b..deb0fb6 100644 --- a/metrics/go-kit/metrics.go +++ b/metrics/go-kit/metrics.go @@ -64,3 +64,18 @@ func NewTimer(hist kit.Histogram) *Timer { func (t *Timer) Record(delta time.Duration) { t.hist.Observe(delta.Seconds()) } + +// Histogram is an adapter from go-kit Histogram to jaeger-lib Histogram +type Histogram struct { + hist kit.Histogram +} + +// NewHistogram creates a new Histogram +func NewHistogram(hist kit.Histogram) *Histogram { + return &Histogram{hist: hist} +} + +// Record saves the value passed in. +func (t *Histogram) Record(value float64) { + t.hist.Observe(value) +} diff --git a/metrics/go-kit/metrics_test.go b/metrics/go-kit/metrics_test.go index b62c08a..ed37e98 100644 --- a/metrics/go-kit/metrics_test.go +++ b/metrics/go-kit/metrics_test.go @@ -30,3 +30,10 @@ func TestTimer(t *testing.T) { timer.Record(100*time.Millisecond + 500*time.Microsecond) // 100.5 milliseconds assert.EqualValues(t, 0.1005, kitHist.Quantile(0.9)) } + +func TestHistogram(t *testing.T) { + kitHist := generic.NewHistogram("abc", 10) + var histogram metrics.Histogram = NewHistogram(kitHist) + histogram.Record(100) + assert.EqualValues(t, 100, kitHist.Quantile(0.9)) +} diff --git a/metrics/histogram.go b/metrics/histogram.go new file mode 100644 index 0000000..d3bd617 --- /dev/null +++ b/metrics/histogram.go @@ -0,0 +1,28 @@ +// Copyright (c) 2018 The Jaeger Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +// Histogram that keeps track of a distribution of values. +type Histogram interface { + // Records the value passed in. + Record(float64) +} + +// NullHistogram that does nothing +var NullHistogram Histogram = nullHistogram{} + +type nullHistogram struct{} + +func (nullHistogram) Record(float64) {} diff --git a/metrics/metrics.go b/metrics/metrics.go index fdea7e2..0c63968 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -17,10 +17,11 @@ package metrics import ( "fmt" "reflect" + "strconv" "strings" ) -// Init initializes the passed in metrics and initializes its fields using the passed in factory. +// MustInit initializes the passed in metrics and initializes its fields using the passed in factory. // // It uses reflection to initialize a struct containing metrics fields // by assigning new Counter/Gauge/Timer values with the metric name retrieved @@ -30,15 +31,15 @@ import ( // of type Counter or Gauge or Timer. // // Errors during Init lead to a panic. -func Init(metrics interface{}, factory Factory, globalTags map[string]string) { - if err := InitOrError(metrics, factory, globalTags); err != nil { +func MustInit(metrics interface{}, factory Factory, globalTags map[string]string) { + if err := Init(metrics, factory, globalTags); err != nil { panic(err.Error()) } } -// InitOrError does the same as Init, but returns an error instead of +// Init does the same as Init, but returns an error instead of // panicking. -func InitOrError(m interface{}, factory Factory, globalTags map[string]string) error { +func Init(m interface{}, factory Factory, globalTags map[string]string) error { // Allow user to opt out of reporting metrics by passing in nil. if factory == nil { factory = NullFactory @@ -47,6 +48,7 @@ func InitOrError(m interface{}, factory Factory, globalTags map[string]string) e counterPtrType := reflect.TypeOf((*Counter)(nil)).Elem() gaugePtrType := reflect.TypeOf((*Gauge)(nil)).Elem() timerPtrType := reflect.TypeOf((*Timer)(nil)).Elem() + histogramPtrType := reflect.TypeOf((*Histogram)(nil)).Elem() v := reflect.ValueOf(m).Elem() t := v.Type() @@ -55,6 +57,7 @@ func InitOrError(m interface{}, factory Factory, globalTags map[string]string) e for k, v := range globalTags { tags[k] = v } + var buckets []float64 field := t.Field(i) metric := field.Tag.Get("metric") if metric == "" { @@ -72,19 +75,57 @@ func InitOrError(m interface{}, factory Factory, globalTags map[string]string) e tags[tag[0]] = tag[1] } } + if bucketString := field.Tag.Get("buckets"); bucketString != "" { + if field.Type.AssignableTo(timerPtrType) { + // TODO: Parse timer duration buckets + return fmt.Errorf( + "Field [%s]: Buckets are not currently initialized for timer metrics", + field.Name) + } else if field.Type.AssignableTo(histogramPtrType) { + bucketValues := strings.Split(bucketString, ",") + for _, bucket := range bucketValues { + b, err := strconv.ParseFloat(bucket, 64) + if err != nil { + return fmt.Errorf( + "Field [%s]: Bucket [%s] could not be converted to float64 in 'buckets' string [%s]", + field.Name, bucket, bucketString) + } + buckets = append(buckets, b) + } + } else { + return fmt.Errorf( + "Field [%s]: Buckets should only be defined for Timer and Histogram metric types", + field.Name) + } + } help := field.Tag.Get("help") var obj interface{} - options := Options{ - Name: metric, - Tags: tags, - Help: help, - } if field.Type.AssignableTo(counterPtrType) { - obj = factory.Counter(options) + obj = factory.Counter(Options{ + Name: metric, + Tags: tags, + Help: help, + }) } else if field.Type.AssignableTo(gaugePtrType) { - obj = factory.Gauge(options) + obj = factory.Gauge(Options{ + Name: metric, + Tags: tags, + Help: help, + }) } else if field.Type.AssignableTo(timerPtrType) { - obj = factory.Timer(options) + // TODO: Add buckets once parsed (see TODO above) + obj = factory.Timer(TimerOptions{ + Name: metric, + Tags: tags, + Help: help, + }) + } else if field.Type.AssignableTo(histogramPtrType) { + obj = factory.Histogram(HistogramOptions{ + Name: metric, + Tags: tags, + Help: help, + Buckets: buckets, + }) } else { return fmt.Errorf( "Field %s is not a pointer to timer, gauge, or counter", diff --git a/metrics/metrics_test.go b/metrics/metrics_test.go index f27f056..2f5bcdd 100644 --- a/metrics/metrics_test.go +++ b/metrics/metrics_test.go @@ -13,9 +13,10 @@ import ( func TestInitMetrics(t *testing.T) { testMetrics := struct { - Gauge metrics.Gauge `metric:"gauge" tags:"1=one,2=two"` - Counter metrics.Counter `metric:"counter"` - Timer metrics.Timer `metric:"timer"` + Gauge metrics.Gauge `metric:"gauge" tags:"1=one,2=two"` + Counter metrics.Counter `metric:"counter"` + Timer metrics.Timer `metric:"timer"` + Histogram metrics.Histogram `metric:"histogram" buckets:"20,40,60,80"` }{} f := metricstest.NewFactory(0) @@ -23,12 +24,13 @@ func TestInitMetrics(t *testing.T) { globalTags := map[string]string{"key": "value"} - err := metrics.InitOrError(&testMetrics, f, globalTags) + err := metrics.Init(&testMetrics, f, globalTags) assert.NoError(t, err) testMetrics.Gauge.Update(10) testMetrics.Counter.Inc(5) testMetrics.Timer.Record(time.Duration(time.Second * 35)) + testMetrics.Histogram.Record(42) // wait for metrics for i := 0; i < 1000; i++ { @@ -44,6 +46,7 @@ func TestInitMetrics(t *testing.T) { assert.EqualValues(t, 5, c["counter|key=value"]) assert.EqualValues(t, 10, g["gauge|1=one|2=two|key=value"]) assert.EqualValues(t, 36863, g["timer|key=value.P50"]) + assert.EqualValues(t, 43, g["histogram|key=value.P50"]) stopwatch := metrics.StartStopwatch(testMetrics.Timer) stopwatch.Stop() @@ -62,16 +65,38 @@ var ( invalidMetricType = struct { InvalidMetricType int64 `metric:"counter"` }{} + + badHistogramBucket = struct { + BadHistogramBucket metrics.Histogram `metric:"histogram" buckets:"1,2,a,4"` + }{} + + badTimerBucket = struct { + BadTimerBucket metrics.Timer `metric:"timer" buckets:"1"` + }{} + + invalidBuckets = struct { + InvalidBuckets metrics.Counter `metric:"counter" buckets:"1"` + }{} ) func TestInitMetricsFailures(t *testing.T) { - assert.EqualError(t, metrics.InitOrError(&noMetricTag, nil, nil), "Field NoMetricTag is missing a tag 'metric'") + assert.EqualError(t, metrics.Init(&noMetricTag, nil, nil), "Field NoMetricTag is missing a tag 'metric'") - assert.EqualError(t, metrics.InitOrError(&badTags, nil, nil), + assert.EqualError(t, metrics.Init(&badTags, nil, nil), "Field [BadTags]: Tag [noValue] is not of the form key=value in 'tags' string [1=one,noValue]") - assert.EqualError(t, metrics.InitOrError(&invalidMetricType, nil, nil), + assert.EqualError(t, metrics.Init(&invalidMetricType, nil, nil), "Field InvalidMetricType is not a pointer to timer, gauge, or counter") + + assert.EqualError(t, metrics.Init(&badHistogramBucket, nil, nil), + "Field [BadHistogramBucket]: Bucket [a] could not be converted to float64 in 'buckets' string [1,2,a,4]") + + assert.EqualError(t, metrics.Init(&badTimerBucket, nil, nil), + "Field [BadTimerBucket]: Buckets are not currently initialized for timer metrics") + + assert.EqualError(t, metrics.Init(&invalidBuckets, nil, nil), + "Field [InvalidBuckets]: Buckets should only be defined for Timer and Histogram metric types") + } func TestInitPanic(t *testing.T) { @@ -81,12 +106,12 @@ func TestInitPanic(t *testing.T) { } }() - metrics.Init(&noMetricTag, metrics.NullFactory, nil) + metrics.MustInit(&noMetricTag, metrics.NullFactory, nil) } func TestNullMetrics(t *testing.T) { // This test is just for cover - metrics.NullFactory.Timer(metrics.Options{ + metrics.NullFactory.Timer(metrics.TimerOptions{ Name: "name", }).Record(0) metrics.NullFactory.Counter(metrics.Options{ @@ -95,6 +120,9 @@ func TestNullMetrics(t *testing.T) { metrics.NullFactory.Gauge(metrics.Options{ Name: "name", }).Update(0) + metrics.NullFactory.Histogram(metrics.HistogramOptions{ + Name: "name", + }).Record(0) metrics.NullFactory.Namespace(metrics.NSOptions{ Name: "name", }).Gauge(metrics.Options{ diff --git a/metrics/metricstest/local.go b/metrics/metricstest/local.go index ea543a7..a67265a 100644 --- a/metrics/metricstest/local.go +++ b/metrics/metricstest/local.go @@ -30,28 +30,31 @@ import ( // A Backend is a metrics provider which aggregates data in-vm, and // allows exporting snapshots to shove the data into a remote collector type Backend struct { - cm sync.Mutex - gm sync.Mutex - tm sync.Mutex - counters map[string]*int64 - gauges map[string]*int64 - timers map[string]*localBackendTimer - stop chan struct{} - wg sync.WaitGroup - TagsSep string - TagKVSep string + cm sync.Mutex + gm sync.Mutex + tm sync.Mutex + hm sync.Mutex + counters map[string]*int64 + gauges map[string]*int64 + timers map[string]*localBackendTimer + histograms map[string]*localBackendHistogram + stop chan struct{} + wg sync.WaitGroup + TagsSep string + TagKVSep string } // NewBackend returns a new Backend. The collectionInterval is the histogram // time window for each timer. func NewBackend(collectionInterval time.Duration) *Backend { b := &Backend{ - counters: make(map[string]*int64), - gauges: make(map[string]*int64), - timers: make(map[string]*localBackendTimer), - stop: make(chan struct{}), - TagsSep: "|", - TagKVSep: "=", + counters: make(map[string]*int64), + gauges: make(map[string]*int64), + timers: make(map[string]*localBackendTimer), + histograms: make(map[string]*localBackendHistogram), + stop: make(chan struct{}), + TagsSep: "|", + TagKVSep: "=", } if collectionInterval == 0 { // Use one histogram time window for all timers @@ -70,9 +73,12 @@ func (b *Backend) Clear() { defer b.gm.Unlock() b.tm.Lock() defer b.tm.Unlock() + b.hm.Lock() + defer b.hm.Unlock() b.counters = make(map[string]*int64) b.gauges = make(map[string]*int64) b.timers = make(map[string]*localBackendTimer) + b.histograms = make(map[string]*localBackendHistogram) } func (b *Backend) runLoop(collectionInterval time.Duration) { @@ -128,6 +134,34 @@ func (b *Backend) UpdateGauge(name string, tags map[string]string, value int64) atomic.StoreInt64(gauge, value) } +// RecordHistogram records a timing duration +func (b *Backend) RecordHistogram(name string, tags map[string]string, v float64) { + name = metrics.GetKey(name, tags, b.TagsSep, b.TagKVSep) + histogram := b.findOrCreateHistogram(name) + histogram.Lock() + histogram.hist.Current.RecordValue(int64(v)) + histogram.Unlock() +} + +func (b *Backend) findOrCreateHistogram(name string) *localBackendHistogram { + b.hm.Lock() + defer b.hm.Unlock() + if t, ok := b.histograms[name]; ok { + return t + } + + t := &localBackendHistogram{ + hist: hdrhistogram.NewWindowed(5, 0, int64((5*time.Minute)/time.Millisecond), 1), + } + b.histograms[name] = t + return t +} + +type localBackendHistogram struct { + sync.Mutex + hist *hdrhistogram.WindowedHistogram +} + // RecordTimer records a timing duration func (b *Backend) RecordTimer(name string, tags map[string]string, d time.Duration) { name = metrics.GetKey(name, tags, b.TagsSep, b.TagKVSep) @@ -201,6 +235,22 @@ func (b *Backend) Snapshot() (counters, gauges map[string]int64) { } } + b.hm.Lock() + histograms := make(map[string]*localBackendHistogram) + for histogramName, histogram := range b.histograms { + histograms[histogramName] = histogram + } + b.hm.Unlock() + + for histogramName, histogram := range histograms { + histogram.Lock() + hist := histogram.hist.Merge() + histogram.Unlock() + for name, q := range percentiles { + gauges[histogramName+"."+name] = hist.ValueAtQuantile(q) + } + } + return } @@ -211,9 +261,11 @@ func (b *Backend) Stop() { } type stats struct { - name string - tags map[string]string - localBackend *Backend + name string + tags map[string]string + buckets []float64 + durationBuckets []time.Duration + localBackend *Backend } type localTimer struct { @@ -224,6 +276,14 @@ func (l *localTimer) Record(d time.Duration) { l.localBackend.RecordTimer(l.name, l.tags, d) } +type localHistogram struct { + stats +} + +func (l *localHistogram) Record(v float64) { + l.localBackend.RecordHistogram(l.name, l.tags, v) +} + type localCounter struct { stats } @@ -290,12 +350,13 @@ func (l *Factory) Counter(options metrics.Options) metrics.Counter { } // Timer returns a local stats timer. -func (l *Factory) Timer(options metrics.Options) metrics.Timer { +func (l *Factory) Timer(options metrics.TimerOptions) metrics.Timer { return &localTimer{ stats{ - name: l.newNamespace(options.Name), - tags: l.appendTags(options.Tags), - localBackend: l.Backend, + name: l.newNamespace(options.Name), + tags: l.appendTags(options.Tags), + durationBuckets: options.Buckets, + localBackend: l.Backend, }, } } @@ -311,6 +372,18 @@ func (l *Factory) Gauge(options metrics.Options) metrics.Gauge { } } +// Histogram returns a local stats histogram. +func (l *Factory) Histogram(options metrics.HistogramOptions) metrics.Histogram { + return &localHistogram{ + stats{ + name: l.newNamespace(options.Name), + tags: l.appendTags(options.Tags), + buckets: options.Buckets, + localBackend: l.Backend, + }, + } +} + // Namespace returns a new namespace. func (l *Factory) Namespace(scope metrics.NSOptions) metrics.Factory { return &Factory{ diff --git a/metrics/metricstest/local_test.go b/metrics/metricstest/local_test.go index 9aa73ef..d93fbd4 100644 --- a/metrics/metricstest/local_test.go +++ b/metrics/metricstest/local_test.go @@ -68,12 +68,18 @@ func TestLocalMetrics(t *testing.T) { for metric, timing := range timings { for _, d := range timing { - f.Timer(metrics.Options{ + f.Timer(metrics.TimerOptions{ Name: metric, }).Record(d) } } + histogram := f.Histogram(metrics.HistogramOptions{ + Name: "my-histo", + }) + histogram.Record(321) + histogram.Record(42) + c, g := f.Snapshot() require.NotNil(t, c) require.NotNil(t, g) @@ -100,6 +106,12 @@ func TestLocalMetrics(t *testing.T) { "foo-latency.P99": 36863, "foo-latency.P999": 36863, "my-gauge": 43, + "my-histo.P50": 43, + "my-histo.P75": 335, + "my-histo.P90": 335, + "my-histo.P95": 335, + "my-histo.P99": 335, + "my-histo.P999": 335, "other-gauge": 74, }, g) @@ -118,7 +130,7 @@ func TestLocalMetricsInterval(t *testing.T) { f := NewFactory(refreshInterval) defer f.Stop() - f.Timer(metrics.Options{ + f.Timer(metrics.TimerOptions{ Name: "timer", }).Record(1) diff --git a/metrics/multi/multi.go b/metrics/multi/multi.go index aa84d29..ead14e3 100644 --- a/metrics/multi/multi.go +++ b/metrics/multi/multi.go @@ -64,7 +64,7 @@ func (t *timer) Record(delta time.Duration) { } // Timer implements metrics.Factory interface -func (f *Factory) Timer(options metrics.Options) metrics.Timer { +func (f *Factory) Timer(options metrics.TimerOptions) metrics.Timer { timer := &timer{ timers: make([]metrics.Timer, len(f.factories)), } @@ -74,6 +74,27 @@ func (f *Factory) Timer(options metrics.Options) metrics.Timer { return timer } +type histogram struct { + histograms []metrics.Histogram +} + +func (h *histogram) Record(value float64) { + for _, histogram := range h.histograms { + histogram.Record(value) + } +} + +// Histogram implements metrics.Factory interface +func (f *Factory) Histogram(options metrics.HistogramOptions) metrics.Histogram { + histogram := &histogram{ + histograms: make([]metrics.Histogram, len(f.factories)), + } + for i, factory := range f.factories { + histogram.histograms[i] = factory.Histogram(options) + } + return histogram +} + type gauge struct { gauges []metrics.Gauge } diff --git a/metrics/multi/multi_test.go b/metrics/multi/multi_test.go index 923fc9d..81a9a7a 100644 --- a/metrics/multi/multi_test.go +++ b/metrics/multi/multi_test.go @@ -27,10 +27,14 @@ func TestMultiFactory(t *testing.T) { Name: "gauge", Tags: tags, }).Update(42) - multi2.Timer(metrics.Options{ + multi2.Timer(metrics.TimerOptions{ Name: "timer", Tags: tags, }).Record(42 * time.Millisecond) + multi2.Histogram(metrics.HistogramOptions{ + Name: "histogram", + Tags: tags, + }).Record(42) for _, f := range []*metricstest.Factory{f1, f2} { f.AssertCounterMetrics(t, @@ -39,5 +43,6 @@ func TestMultiFactory(t *testing.T) { metricstest.ExpectedMetric{Name: "ns2.gauge", Tags: tags, Value: 42}) _, g := f.Snapshot() assert.EqualValues(t, 43, g["ns2.timer|x=y.P99"]) + assert.EqualValues(t, 43, g["ns2.histogram|x=y.P99"]) } } diff --git a/metrics/prometheus/factory.go b/metrics/prometheus/factory.go index 7641361..44205bc 100644 --- a/metrics/prometheus/factory.go +++ b/metrics/prometheus/factory.go @@ -163,7 +163,7 @@ func (f *Factory) Gauge(options metrics.Options) metrics.Gauge { } // Timer implements Timer of metrics.Factory. -func (f *Factory) Timer(options metrics.Options) metrics.Timer { +func (f *Factory) Timer(options metrics.TimerOptions) metrics.Timer { help := strings.TrimSpace(options.Help) if len(help) == 0 { help = options.Name @@ -174,7 +174,7 @@ func (f *Factory) Timer(options metrics.Options) metrics.Timer { opts := prometheus.HistogramOpts{ Name: name, Help: help, - Buckets: f.buckets, + Buckets: asFloatBuckets(options.Buckets), } hv := f.cache.getOrMakeHistogramVec(opts, labelNames) return &timer{ @@ -182,6 +182,34 @@ func (f *Factory) Timer(options metrics.Options) metrics.Timer { } } +func asFloatBuckets(buckets []time.Duration) []float64 { + data := make([]float64, len(buckets)) + for i := range data { + data[i] = float64(buckets[i]) / float64(time.Second) + } + return data +} + +// Histogram implements Histogram of metrics.Factory. +func (f *Factory) Histogram(options metrics.HistogramOptions) metrics.Histogram { + help := strings.TrimSpace(options.Help) + if len(help) == 0 { + help = options.Name + } + name := f.subScope(options.Name) + tags := f.mergeTags(options.Tags) + labelNames := f.tagNames(tags) + opts := prometheus.HistogramOpts{ + Name: name, + Help: help, + Buckets: options.Buckets, + } + hv := f.cache.getOrMakeHistogramVec(opts, labelNames) + return &histogram{ + histogram: hv.WithLabelValues(f.tagsAsLabelValues(labelNames, tags)...), + } +} + // Namespace implements Namespace of metrics.Factory. func (f *Factory) Namespace(scope metrics.NSOptions) metrics.Factory { return newFactory(f, f.subScope(scope.Name), f.mergeTags(scope.Tags)) @@ -215,6 +243,14 @@ func (t *timer) Record(v time.Duration) { t.histogram.Observe(float64(v.Nanoseconds()) / float64(time.Second/time.Nanosecond)) } +type histogram struct { + histogram observer +} + +func (h *histogram) Record(v float64) { + h.histogram.Observe(v) +} + func (f *Factory) subScope(name string) string { if f.scope == "" { return f.normalize(name) diff --git a/metrics/prometheus/factory_test.go b/metrics/prometheus/factory_test.go index 9615f1a..cb03fe6 100644 --- a/metrics/prometheus/factory_test.go +++ b/metrics/prometheus/factory_test.go @@ -175,17 +175,17 @@ func TestTimer(t *testing.T) { f3 := f2.Namespace(metrics.NSOptions{ Tags: map[string]string{"a": "b"}, }) // essentially same as f2 - t1 := f2.Timer(metrics.Options{ + t1 := f2.Timer(metrics.TimerOptions{ Name: "rodriguez", Tags: map[string]string{"x": "y"}, Help: "Help message", }) - t2 := f2.Timer(metrics.Options{ + t2 := f2.Timer(metrics.TimerOptions{ Name: "rodriguez", Tags: map[string]string{"x": "z"}, Help: "Help message", }) - t3 := f3.Timer(metrics.Options{ + t3 := f3.Timer(metrics.TimerOptions{ Name: "rodriguez", Tags: map[string]string{"x": "z"}, Help: "Help message", @@ -230,7 +230,7 @@ func TestTimer(t *testing.T) { func TestTimerDefaultHelp(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := New(WithRegisterer(registry)) - t1 := f1.Timer(metrics.Options{ + t1 := f1.Timer(metrics.TimerOptions{ Name: "rodriguez", Tags: map[string]string{"x": "y"}, }) @@ -246,9 +246,10 @@ func TestTimerCustomBuckets(t *testing.T) { registry := prometheus.NewPedanticRegistry() f1 := New(WithRegisterer(registry), WithBuckets([]float64{1.5})) // dot and dash in the metric name will be replaced with underscore - t1 := f1.Timer(metrics.Options{ - Name: "bender.bending-rodriguez", - Tags: map[string]string{"x": "y"}, + t1 := f1.Timer(metrics.TimerOptions{ + Name: "bender.bending-rodriguez", + Tags: map[string]string{"x": "y"}, + Buckets: []time.Duration{time.Nanosecond, 5 * time.Nanosecond}, }) t1.Record(1 * time.Second) t1.Record(2 * time.Second) @@ -256,6 +257,104 @@ func TestTimerCustomBuckets(t *testing.T) { snapshot, err := registry.Gather() require.NoError(t, err) + m1 := findMetric(t, snapshot, "bender_bending_rodriguez", map[string]string{"x": "y"}) + assert.EqualValues(t, 2, m1.GetHistogram().GetSampleCount(), "%+v", m1) + assert.EqualValues(t, 3, m1.GetHistogram().GetSampleSum(), "%+v", m1) + assert.Len(t, m1.GetHistogram().GetBucket(), 2) +} + +func TestHistogram(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry)) + f2 := f1.Namespace(metrics.NSOptions{ + Name: "bender", + Tags: map[string]string{"a": "b"}, + }) + f3 := f2.Namespace(metrics.NSOptions{ + Tags: map[string]string{"a": "b"}, + }) // essentially same as f2 + t1 := f2.Histogram(metrics.HistogramOptions{ + Name: "rodriguez", + Tags: map[string]string{"x": "y"}, + Help: "Help message", + }) + t2 := f2.Histogram(metrics.HistogramOptions{ + Name: "rodriguez", + Tags: map[string]string{"x": "z"}, + Help: "Help message", + }) + t3 := f3.Histogram(metrics.HistogramOptions{ + Name: "rodriguez", + Tags: map[string]string{"x": "z"}, + Help: "Help message", + }) // same as t2, but from f3 + t1.Record(1) + t1.Record(2) + t2.Record(3) + t3.Record(4) + + snapshot, err := registry.Gather() + require.NoError(t, err) + + assert.EqualValues(t, "Help message", snapshot[0].GetHelp()) + + m1 := findMetric(t, snapshot, "bender_rodriguez", map[string]string{"a": "b", "x": "y"}) + assert.EqualValues(t, 2, m1.GetHistogram().GetSampleCount(), "%+v", m1) + assert.EqualValues(t, 3, m1.GetHistogram().GetSampleSum(), "%+v", m1) + for _, bucket := range m1.GetHistogram().GetBucket() { + if bucket.GetUpperBound() < 1 { + assert.EqualValues(t, 0, bucket.GetCumulativeCount()) + } else if bucket.GetUpperBound() < 2 { + assert.EqualValues(t, 1, bucket.GetCumulativeCount()) + } else { + assert.EqualValues(t, 2, bucket.GetCumulativeCount()) + } + } + + m2 := findMetric(t, snapshot, "bender_rodriguez", map[string]string{"a": "b", "x": "z"}) + assert.EqualValues(t, 2, m2.GetHistogram().GetSampleCount(), "%+v", m2) + assert.EqualValues(t, 7, m2.GetHistogram().GetSampleSum(), "%+v", m2) + for _, bucket := range m2.GetHistogram().GetBucket() { + if bucket.GetUpperBound() < 3 { + assert.EqualValues(t, 0, bucket.GetCumulativeCount()) + } else if bucket.GetUpperBound() < 4 { + assert.EqualValues(t, 1, bucket.GetCumulativeCount()) + } else { + assert.EqualValues(t, 2, bucket.GetCumulativeCount()) + } + } +} + +func TestHistogramDefaultHelp(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry)) + t1 := f1.Histogram(metrics.HistogramOptions{ + Name: "rodriguez", + Tags: map[string]string{"x": "y"}, + }) + t1.Record(1) + + snapshot, err := registry.Gather() + require.NoError(t, err) + + assert.EqualValues(t, "rodriguez", snapshot[0].GetHelp()) +} + +func TestHistogramCustomBuckets(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + f1 := New(WithRegisterer(registry)) + // dot and dash in the metric name will be replaced with underscore + t1 := f1.Histogram(metrics.HistogramOptions{ + Name: "bender.bending-rodriguez", + Tags: map[string]string{"x": "y"}, + Buckets: []float64{1.5}, + }) + t1.Record(1) + t1.Record(2) + + snapshot, err := registry.Gather() + require.NoError(t, err) + m1 := findMetric(t, snapshot, "bender_bending_rodriguez", map[string]string{"x": "y"}) assert.EqualValues(t, 2, m1.GetHistogram().GetSampleCount(), "%+v", m1) assert.EqualValues(t, 3, m1.GetHistogram().GetSampleSum(), "%+v", m1) diff --git a/metrics/tally/factory.go b/metrics/tally/factory.go index b66f987..4397381 100644 --- a/metrics/tally/factory.go +++ b/metrics/tally/factory.go @@ -48,14 +48,23 @@ func (f *factory) Gauge(options metrics.Options) metrics.Gauge { return NewGauge(scope.Gauge(options.Name)) } -func (f *factory) Timer(options metrics.Options) metrics.Timer { +func (f *factory) Timer(options metrics.TimerOptions) metrics.Timer { scope := f.tally if len(options.Tags) > 0 { scope = scope.Tagged(options.Tags) } + // TODO: Determine whether buckets can be used return NewTimer(scope.Timer(options.Name)) } +func (f *factory) Histogram(options metrics.HistogramOptions) metrics.Histogram { + scope := f.tally + if len(options.Tags) > 0 { + scope = scope.Tagged(options.Tags) + } + return NewHistogram(scope.Histogram(options.Name, tally.ValueBuckets(options.Buckets))) +} + func (f *factory) Namespace(scope metrics.NSOptions) metrics.Factory { return &factory{ tally: f.tally.SubScope(scope.Name).Tagged(scope.Tags), diff --git a/metrics/tally/factory_test.go b/metrics/tally/factory_test.go index b21b8fc..a2474eb 100644 --- a/metrics/tally/factory_test.go +++ b/metrics/tally/factory_test.go @@ -26,11 +26,17 @@ func TestFactory(t *testing.T) { Tags: map[string]string{"x": "y"}, }) gauge.Update(42) - timer := factory.Timer(metrics.Options{ + timer := factory.Timer(metrics.TimerOptions{ Name: "timer", Tags: map[string]string{"x": "y"}, }) timer.Record(42 * time.Millisecond) + histogram := factory.Histogram(metrics.HistogramOptions{ + Name: "histogram", + Tags: map[string]string{"x": "y"}, + Buckets: []float64{0, 100, 200}, + }) + histogram.Record(42) snapshot := testScope.Snapshot() // tally v3 includes tags in the name, so look @@ -50,6 +56,11 @@ func TestFactory(t *testing.T) { h = snapshot.Timers()["pre.fix.timer+a=b,c=d,x=y"] } + hs := snapshot.Histograms()["pre.fix.histogram"] + if hs == nil { + hs = snapshot.Histograms()["pre.fix.histogram+a=b,c=d,x=y"] + } + expectedTags := map[string]string{"a": "b", "c": "d", "x": "y"} assert.EqualValues(t, 42, c.Value()) assert.EqualValues(t, expectedTags, c.Tags()) @@ -57,4 +68,9 @@ func TestFactory(t *testing.T) { assert.EqualValues(t, expectedTags, g.Tags()) assert.Equal(t, []time.Duration{42 * time.Millisecond}, h.Values()) assert.EqualValues(t, expectedTags, h.Tags()) + assert.Len(t, hs.Values(), 4) + assert.Equal(t, int64(0), hs.Values()[0]) + assert.Equal(t, int64(1), hs.Values()[100]) + assert.Equal(t, int64(0), hs.Values()[200]) + assert.EqualValues(t, expectedTags, hs.Tags()) } diff --git a/metrics/tally/metrics.go b/metrics/tally/metrics.go index f8621c6..5069212 100644 --- a/metrics/tally/metrics.go +++ b/metrics/tally/metrics.go @@ -64,3 +64,18 @@ func NewTimer(timer tally.Timer) *Timer { func (t *Timer) Record(delta time.Duration) { t.timer.Record(delta) } + +// Histogram is an adapter from go-tally Histogram to jaeger-lib Histogram +type Histogram struct { + histogram tally.Histogram +} + +// NewHistogram creates a new Histogram +func NewHistogram(histogram tally.Histogram) *Histogram { + return &Histogram{histogram: histogram} +} + +// Record saves the value passed in. +func (h *Histogram) Record(value float64) { + h.histogram.RecordValue(value) +}