diff --git a/key_gen.go b/key_gen.go new file mode 100644 index 00000000..264d2e9f --- /dev/null +++ b/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/pool.go b/pool.go new file mode 100644 index 00000000..8b90ada7 --- /dev/null +++ b/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/prometheus/README.md b/prometheus/README.md index 70a730c1..494506f4 100644 --- a/prometheus/README.md +++ b/prometheus/README.md @@ -1,57 +1,117 @@ -# Example Usage +# A buffered Prometheus reporter -`main.go`: +See `examples/prometheus_main.go` for an end to end example. +## Options + +You can use a specific Prometheus registry, and you can use +either histograms or summaries 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) +} ``` -package main -import ( - "fmt" - "math/rand" - "net/http" - "time" +The timer types are: + +```go +// TimerType describes a type of timer +type TimerType int - "github.com/uber-go/tally" - "github.com/uber-go/tally/prometheus" +const ( + // HistogramTimerType is a timer type that reports into a histogram + HistogramTimerType TimerType = iota + // SummaryTimerType is a timer type that reports into a summary + SummaryTimerType ) +``` + +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. -func main() { - r := prometheus.NewReporter(nil) - // note the "_" separator. Prometheus doesnt like metrics with "." in them. - scope, finisher := tally.NewRootScope("prefix", map[string]string{}, r, 1*time.Second, "_") - defer finisher.Close() - - counter := scope.Counter("test_counter") - gauge := scope.Gauge("test_gauge") - histogram := scope.Timer("test_histogram") - - go func() { - for { - counter.Inc(1) - time.Sleep(1000000) - } - }() - - go func() { - for { - gauge.Update(rand.Float64() * 1000) - time.Sleep(1000000) - } - }() - - go func() { - for { - sw := histogram.Start() - time.Sleep(time.Duration(rand.Float64() * 1000 * 1000)) - sw.Stop() - time.Sleep(1000000) - } - }() - - http.Handle("/metrics", r.HTTPHandler()) - fmt.Printf("Serving :8080/metrics\n") - fmt.Printf("%v\n", http.ListenAndServe(":8080", nil)) - select {} +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/prometheus/example/prometheus_main.go b/prometheus/example/prometheus_main.go new file mode 100644 index 00000000..b000c2ba --- /dev/null +++ b/prometheus/example/prometheus_main.go @@ -0,0 +1,80 @@ +// 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 main + +import ( + "fmt" + "math/rand" + "net/http" + "time" + + "github.com/uber-go/tally" + promreporter "github.com/uber-go/tally/prometheus" +) + +func main() { + r := promreporter.NewReporter(promreporter.Options{}) + + // Note: `promreporter.DefaultSeparator` is "_". + // Prometheus doesnt like metrics with "." or "-" in them. + scope, closer := tally.NewCachedRootScope("my_service", map[string]string{}, + r, 1*time.Second, promreporter.DefaultSeparator) + defer closer.Close() + + counter := scope.Tagged(map[string]string{ + "foo": "bar", + }).Counter("test_counter") + + gauge := scope.Tagged(map[string]string{ + "foo": "baz", + }).Gauge("test_gauge") + + timer := scope.Tagged(map[string]string{ + "foo": "qux", + }).Timer("test_timer_histogram") + + go func() { + for { + counter.Inc(1) + time.Sleep(time.Second) + } + }() + + go func() { + for { + gauge.Update(rand.Float64() * 1000) + time.Sleep(time.Second) + } + }() + + go func() { + for { + sw := timer.Start() + time.Sleep(time.Duration(rand.Float64() * float64(time.Second))) + sw.Stop() + } + }() + + http.Handle("/metrics", r.HTTPHandler()) + fmt.Printf("Serving :8080/metrics\n") + fmt.Printf("%v\n", http.ListenAndServe(":8080", nil)) + select {} +} diff --git a/prometheus/reporter.go b/prometheus/reporter.go index f1638bb6..78afe85a 100644 --- a/prometheus/reporter.go +++ b/prometheus/reporter.go @@ -1,219 +1,509 @@ +// 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" - "sort" "sync" "time" prom "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/uber-go/tally" ) +const ( + // DefaultSeparator is the default separator that should be used with + // a tally scope for a prometheus reporter. + DefaultSeparator = "_" +) + var ( - errorAlreadyRegistered = errors.New("metric already registered") - // DefaultHistogramObjectives is the default objectives used when creating a new Summary histogram - // in the prometheus registry. - // See https://godoc.org/github.com/prometheus/client_golang/prometheus#SummaryOpts - DefaultHistogramObjectives = map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001, 0.999: 0.0001} + errUnknownTimerType = errors.New("unknown metric timer type") + ms = float64(time.Millisecond) / float64(time.Second) ) -type metricID string +// 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, + } +} -// Reporter is a prometheus backed tally reporter +// 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.StatsReporter + 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 { - objectives map[float64]float64 - counters map[metricID]*prom.CounterVec - gauges map[metricID]*prom.GaugeVec - summaries map[metricID]*prom.SummaryVec 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 +} + +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)) } -// HTTPHandler returns the prometheus HTTP handler for serving metrics +func (m *cachedMetric) reportTimerSummary(interval time.Duration) { + m.summary.Observe(float64(interval) / float64(time.Second)) +} + +type noopMetric struct{} + +func (m noopMetric) ReportCount(value int64) {} +func (m noopMetric) ReportGauge(value float64) {} +func (m noopMetric) ReportTimer(interval time.Duration) {} + func (r *reporter) HTTPHandler() http.Handler { return promhttp.Handler() } +// TimerType describes a type of timer +type TimerType int + +const ( + // HistogramTimerType is a timer type that reports into a histogram + HistogramTimerType TimerType = iota + // SummaryTimerType is a timer type that reports into a summary + SummaryTimerType +) + +// 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 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(objectives map[float64]float64) Reporter { - counters := map[metricID]*prom.CounterVec{} - gauges := map[metricID]*prom.GaugeVec{} - summaries := map[metricID]*prom.SummaryVec{} - obj := DefaultHistogramObjectives - if objectives != nil { - obj = objectives +// 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() } - reporter := reporter{ - counters: counters, - gauges: gauges, - summaries: summaries, - objectives: obj, + 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), + } +} - return &reporter +func (r *reporter) RegisterCounter( + name string, + tagKeys []string, + desc string, +) (*prom.CounterVec, error) { + return r.counterVec(name, tagKeys, desc) } -// 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 -func (r *reporter) RegisterCounter(name string, tags map[string]string, desc string) (*prom.CounterVec, error) { - ctr := &prom.CounterVec{} - id := canonicalMetricID(name, tags) - exists := r.hasCounter(id) - if exists { - return ctr, errorAlreadyRegistered +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 } - labelKeys := keysFromMap(tags) - ctr = prom.NewCounterVec( + + ctr := prom.NewCounterVec( prom.CounterOpts{ Name: name, Help: desc, }, - labelKeys, + tagKeys, ) - err := prom.Register(ctr) - if err != nil { - return ctr, err + + if err := r.registerer.Register(ctr); err != nil { + return nil, err } - r.Lock() - defer r.Unlock() + r.counters[id] = ctr return ctr, nil } -// ReportCounter reports a counter value -func (r *reporter) ReportCounter(name string, tags map[string]string, value int64) { - id := canonicalMetricID(name, tags) - - r.RLock() - ctr, ok := r.counters[id] - r.RUnlock() - - if !ok { - var err error - ctr, err = r.RegisterCounter(name, tags, name+" counter") - if err != nil { - panic(err) - } +// 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{} } - ctr.With(tags).Add(float64(value)) + return &cachedMetric{counter: counterVec.With(tags)} } -// 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 -func (r *reporter) RegisterGauge(name string, tags map[string]string, desc string) (*prom.GaugeVec, error) { - g := &prom.GaugeVec{} - id := canonicalMetricID(name, tags) - exists := r.hasGauge(id) - if exists { - return g, errorAlreadyRegistered +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 } - labelKeys := keysFromMap(tags) - g = prom.NewGaugeVec( + g := prom.NewGaugeVec( prom.GaugeOpts{ Name: name, Help: desc, }, - labelKeys, + tagKeys, ) - err := prom.Register(g) - if err != nil { - return g, err + + if err := r.registerer.Register(g); err != nil { + return nil, err } - r.Lock() - defer r.Unlock() + r.gauges[id] = g return g, nil } -// ReportGauge reports a gauge value -func (r *reporter) ReportGauge(name string, tags map[string]string, value float64) { - id := canonicalMetricID(name, tags) +// 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)} +} - r.RLock() - g, ok := r.gauges[id] - r.RUnlock() +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 +} - if !ok { - var err error - g, err = r.RegisterGauge(name, tags, name+" gauge") - if err != nil { - panic(err) +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 } } - g.With(tags).Set(value) + return } -// RegisterTimer is a helper method to initialize a Timer 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 -func (r *reporter) RegisterTimer(name string, tags map[string]string, desc string, objectives map[float64]float64) (*prom.SummaryVec, error) { - h := &prom.SummaryVec{} - id := canonicalMetricID(name, tags) - exists := r.hasSummary(id) - if exists { - return h, errorAlreadyRegistered - } - labelKeys := keysFromMap(tags) +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 objectives == nil { - objectives = r.objectives + if s, ok := r.timers[id]; ok { + return s.summary, nil } - h = prom.NewSummaryVec( + + s := prom.NewSummaryVec( prom.SummaryOpts{ Name: name, Help: desc, Objectives: objectives, }, - labelKeys, + tagKeys, ) - err := prom.Register(h) - if err != nil { - return h, err + + 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() - r.summaries[id] = h - return h, nil -} -// ReportTimer reports a timer value into the Summary histogram -func (r *reporter) ReportTimer(name string, tags map[string]string, interval time.Duration) { - id := canonicalMetricID(name, tags) + if h, ok := r.timers[id]; ok { + return h.histogram, nil + } - r.RLock() - h, ok := r.summaries[id] - r.RUnlock() + h := prom.NewHistogramVec( + prom.HistogramOpts{ + Name: name, + Help: desc, + Buckets: buckets, + }, + tagKeys, + ) - if !ok { - var err error - h, err = r.RegisterTimer(name, tags, name+" histogram in seconds", nil) - if err != nil { - panic(err) + 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+" timer 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+" timer 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{} } - h.With(tags).Observe(float64(interval)) + return timer } -// Capabilities ... func (r *reporter) Capabilities() tally.Capabilities { return r } -// Reporting indicates it can report outside of the process func (r *reporter) Reporting() bool { return true } -// Tagging indicates prometheus supports tagged metrics func (r *reporter) Tagging() bool { return true } @@ -221,39 +511,17 @@ func (r *reporter) Tagging() bool { // Flush does nothing for prometheus func (r *reporter) Flush() {} -// 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, tags map[string]string) metricID { - canonicalRep := name + "{" - ts := keysFromMap(tags) - sort.Strings(ts) - for _, k := range ts { - canonicalRep = canonicalRep + k + "," - } - canonicalRep = canonicalRep + "}" - - return metricID(canonicalRep) -} +var metricIDKeyValue = "1" -func (r *reporter) hasCounter(id metricID) (exists bool) { - r.RLock() - defer r.RUnlock() - _, exists = r.counters[id] - return -} - -func (r *reporter) hasGauge(id metricID) (exists bool) { - r.RLock() - defer r.RUnlock() - _, exists = r.gauges[id] - return -} - -func (r *reporter) hasSummary(id metricID) (exists bool) { - r.RLock() - defer r.RUnlock() - _, exists = r.summaries[id] - return +// 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 { diff --git a/prometheus/reporter_test.go b/prometheus/reporter_test.go index 5342cb74..1fb9e470 100644 --- a/prometheus/reporter_test.go +++ b/prometheus/reporter_test.go @@ -1,13 +1,40 @@ +// 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 ( "fmt" "testing" "time" + + prom "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCounter(t *testing.T) { - r := NewReporter(nil) + registry := prom.NewRegistry() + r := NewReporter(Options{Registerer: registry}) name := "test_counter" tags := map[string]string{ "foo": "bar", @@ -15,32 +42,150 @@ func TestCounter(t *testing.T) { } tags2 := map[string]string{ "foo": "baz", - "test": "*", + "test": "something", } - r.ReportCounter(name, tags, 1) - r.ReportCounter(name, tags, 1) - r.ReportCounter(name, tags, 1) - for i := 0; i < 5; i++ { - fmt.Printf("%d adding 1\n", i) - r.ReportCounter(name, tags, 1) - } - r.ReportCounter(name, tags2, 1) - r.ReportCounter(name, tags2, 1) + + count := r.AllocateCounter(name, tags) + count.ReportCount(1) + count.ReportCount(2) + + count = r.AllocateCounter(name, tags2) + count.ReportCount(2) + + assertMetric(t, gather(t, registry), metric{ + name: name, + mtype: dto.MetricType_COUNTER, + instances: []instance{ + { + labels: tags, + counter: counterValue(3), + }, + { + labels: tags2, + counter: counterValue(2), + }, + }, + }) } func TestGauge(t *testing.T) { - r := NewReporter(nil) + registry := prom.NewRegistry() + r := NewReporter(Options{Registerer: registry}) name := "test_gauge" tags := map[string]string{ "foo": "bar", "test": "everything", } - r.ReportGauge(name, tags, 15) - r.ReportGauge(name, tags, 30) + + gauge := r.AllocateGauge(name, tags) + gauge.ReportGauge(15) + gauge.ReportGauge(30) + + assertMetric(t, gather(t, registry), metric{ + name: name, + mtype: dto.MetricType_GAUGE, + instances: []instance{ + { + labels: tags, + gauge: gaugeValue(30), + }, + }, + }) +} + +func TestTimerHistogram(t *testing.T) { + registry := prom.NewRegistry() + r := NewReporter(Options{ + Registerer: registry, + DefaultTimerType: HistogramTimerType, + DefaultHistogramBuckets: []float64{ + 50 * ms, + 250 * ms, + 1000 * ms, + 2500 * ms, + 10000 * ms, + }, + }) + + name := "test_timer" + tags := map[string]string{ + "foo": "bar", + "test": "everything", + } + tags2 := map[string]string{ + "foo": "baz", + "test": "something", + } + vals := []time.Duration{ + 23 * time.Millisecond, + 223 * time.Millisecond, + 320 * time.Millisecond, + } + vals2 := []time.Duration{ + 1742 * time.Millisecond, + 3232 * time.Millisecond, + } + + timer := r.AllocateTimer(name, tags) + for _, v := range vals { + timer.ReportTimer(v) + } + + timer = r.AllocateTimer(name, tags2) + for _, v := range vals2 { + timer.ReportTimer(v) + } + + assertMetric(t, gather(t, registry), metric{ + name: name, + mtype: dto.MetricType_HISTOGRAM, + instances: []instance{ + { + labels: tags, + histogram: histogramValue(histogramVal{ + sampleCount: uint64(len(vals)), + sampleSum: durationFloatSum(vals), + buckets: []histogramValBucket{ + {upperBound: 0.05, count: 1}, + {upperBound: 0.25, count: 2}, + {upperBound: 1.00, count: 3}, + {upperBound: 2.50, count: 3}, + {upperBound: 10.00, count: 3}, + }, + }), + }, + { + labels: tags2, + histogram: histogramValue(histogramVal{ + sampleCount: uint64(len(vals2)), + sampleSum: durationFloatSum(vals2), + buckets: []histogramValBucket{ + {upperBound: 0.05, count: 0}, + {upperBound: 0.25, count: 0}, + {upperBound: 1.00, count: 0}, + {upperBound: 2.50, count: 1}, + {upperBound: 10.00, count: 2}, + }, + }), + }, + }, + }) } -func TestTimer(t *testing.T) { - r := NewReporter(nil) +func TestTimerSummary(t *testing.T) { + registry := prom.NewRegistry() + r := NewReporter(Options{ + Registerer: registry, + DefaultTimerType: SummaryTimerType, + DefaultSummaryObjectives: map[float64]float64{ + 0.5: 0.01, + 0.75: 0.001, + 0.95: 0.001, + 0.99: 0.001, + 0.999: 0.0001, + }, + }) + name := "test_timer" tags := map[string]string{ "foo": "bar", @@ -48,10 +193,254 @@ func TestTimer(t *testing.T) { } tags2 := map[string]string{ "foo": "baz", - "test": "*", + "test": "something", + } + vals := []time.Duration{ + 23 * time.Millisecond, + 223 * time.Millisecond, + 320 * time.Millisecond, + } + vals2 := []time.Duration{ + 1742 * time.Millisecond, + 3232 * time.Millisecond, + } + + timer := r.AllocateTimer(name, tags) + for _, v := range vals { + timer.ReportTimer(v) + } + + timer = r.AllocateTimer(name, tags2) + for _, v := range vals2 { + timer.ReportTimer(v) + } + + assertMetric(t, gather(t, registry), metric{ + name: name, + mtype: dto.MetricType_SUMMARY, + instances: []instance{ + { + labels: tags, + summary: summaryValue(summaryVal{ + sampleCount: uint64(len(vals)), + sampleSum: durationFloatSum(vals), + quantiles: []summaryValQuantile{ + {quantile: 0.50, value: 0.223}, + {quantile: 0.75, value: 0.32}, + {quantile: 0.95, value: 0.32}, + {quantile: 0.99, value: 0.32}, + {quantile: 0.999, value: 0.32}, + }, + }), + }, + { + labels: tags2, + summary: summaryValue(summaryVal{ + sampleCount: uint64(len(vals2)), + sampleSum: durationFloatSum(vals2), + quantiles: []summaryValQuantile{ + {quantile: 0.50, value: 1.742}, + {quantile: 0.75, value: 3.232}, + {quantile: 0.95, value: 3.232}, + {quantile: 0.99, value: 3.232}, + {quantile: 0.999, value: 3.232}, + }, + }), + }, + }, + }) +} + +func TestOnRegisterError(t *testing.T) { + var captured []error + + registry := prom.NewRegistry() + r := NewReporter(Options{ + Registerer: registry, + OnRegisterError: func(err error) { + captured = append(captured, err) + }, + }) + + c := r.AllocateCounter("bad-name", nil) + c.ReportCount(2) + c.ReportCount(4) + c = r.AllocateCounter("bad.name", nil) + c.ReportCount(42) + c.ReportCount(84) + + assert.Equal(t, 2, len(captured)) +} + +func gather(t *testing.T, r prom.Gatherer) []*dto.MetricFamily { + metrics, err := r.Gather() + require.NoError(t, err) + return metrics +} + +func counterValue(v float64) *dto.Counter { + return &dto.Counter{Value: &v} +} + +func gaugeValue(v float64) *dto.Gauge { + return &dto.Gauge{Value: &v} +} + +type histogramVal struct { + sampleCount uint64 + sampleSum float64 + buckets []histogramValBucket +} + +type histogramValBucket struct { + count uint64 + upperBound float64 +} + +func histogramValue(v histogramVal) *dto.Histogram { + r := &dto.Histogram{ + SampleCount: &v.sampleCount, + SampleSum: &v.sampleSum, + } + for _, b := range v.buckets { + b := b // or else the addresses we take will be static + r.Bucket = append(r.Bucket, &dto.Bucket{ + CumulativeCount: &b.count, + UpperBound: &b.upperBound, + }) + } + return r +} + +type summaryVal struct { + sampleCount uint64 + sampleSum float64 + quantiles []summaryValQuantile +} + +type summaryValQuantile struct { + quantile float64 + value float64 +} + +func summaryValue(v summaryVal) *dto.Summary { + r := &dto.Summary{ + SampleCount: &v.sampleCount, + SampleSum: &v.sampleSum, + } + for _, q := range v.quantiles { + q := q // or else the addresses we take will be static + r.Quantile = append(r.Quantile, &dto.Quantile{ + Quantile: &q.quantile, + Value: &q.value, + }) + } + return r +} + +func durationFloatSum(v []time.Duration) float64 { + var sum float64 + for _, d := range v { + sum += durationFloat(d) + } + return sum +} + +func durationFloat(d time.Duration) float64 { + return float64(d) / float64(time.Second) +} + +type metric struct { + name string + mtype dto.MetricType + instances []instance +} + +type instance struct { + labels map[string]string + counter *dto.Counter + gauge *dto.Gauge + histogram *dto.Histogram + summary *dto.Summary +} + +func assertMetric( + t *testing.T, + metrics []*dto.MetricFamily, + query metric, +) { + q := query + msgFmt := func(msg string, v ...interface{}) string { + prefix := fmt.Sprintf("assert fail for metric name=%s, type=%s: ", + q.name, q.mtype.String()) + return fmt.Sprintf(prefix+msg, v...) + } + for _, m := range metrics { + if m.GetName() != q.name || m.GetType() != q.mtype { + continue + } + if len(q.instances) == 0 { + require.Fail(t, msgFmt("no instances to assert")) + } + for _, i := range q.instances { + found := false + for _, j := range m.GetMetric() { + if len(i.labels) != len(j.GetLabel()) { + continue + } + + notMatched := make(map[string]string, len(i.labels)) + for k, v := range i.labels { + notMatched[k] = v + } + + for _, pair := range j.GetLabel() { + notMatchedValue, matches := notMatched[pair.GetName()] + if matches && pair.GetValue() == notMatchedValue { + delete(notMatched, pair.GetName()) + } + } + + if len(notMatched) != 0 { + continue + } + + found = true + + switch { + case i.counter != nil: + require.NotNil(t, j.GetCounter()) + assert.Equal(t, i.counter.GetValue(), j.GetCounter().GetValue()) + case i.gauge != nil: + require.NotNil(t, j.GetGauge()) + assert.Equal(t, i.gauge.GetValue(), j.GetGauge().GetValue()) + case i.histogram != nil: + require.NotNil(t, j.GetHistogram()) + assert.Equal(t, i.histogram.GetSampleCount(), j.GetHistogram().GetSampleCount()) + assert.Equal(t, i.histogram.GetSampleSum(), j.GetHistogram().GetSampleSum()) + require.Equal(t, len(i.histogram.GetBucket()), len(j.GetHistogram().GetBucket())) + for idx, b := range i.histogram.GetBucket() { + actual := j.GetHistogram().GetBucket()[idx] + assert.Equal(t, b.GetCumulativeCount(), actual.GetCumulativeCount()) + assert.Equal(t, b.GetUpperBound(), actual.GetUpperBound()) + } + case i.summary != nil: + require.NotNil(t, j.GetSummary()) + assert.Equal(t, i.summary.GetSampleCount(), j.GetSummary().GetSampleCount()) + assert.Equal(t, i.summary.GetSampleSum(), j.GetSummary().GetSampleSum()) + require.Equal(t, len(i.summary.GetQuantile()), len(j.GetSummary().GetQuantile())) + for idx, q := range i.summary.GetQuantile() { + actual := j.GetSummary().GetQuantile()[idx] + assert.Equal(t, q.GetQuantile(), actual.GetQuantile()) + assert.Equal(t, q.GetValue(), actual.GetValue()) + } + } + } + if !found { + require.Fail(t, msgFmt("instance not found labels=%v", i.labels)) + } + } + return } - r.ReportTimer(name, tags, 10*time.Second) - r.ReportTimer(name, tags, 320*time.Millisecond) - r.ReportTimer(name, tags2, 223*time.Millisecond) - r.ReportTimer(name, tags, 4*time.Second) + require.Fail(t, msgFmt("metric not found")) } diff --git a/scope.go b/scope.go index 0eca833f..846fa657 100644 --- a/scope.go +++ b/scope.go @@ -38,17 +38,6 @@ var ( globalClock = clock.New() ) -type scopeRegistry struct { - sm sync.RWMutex - subscopes []*scope -} - -func (r *scopeRegistry) add(subscope *scope) { - r.sm.Lock() - r.subscopes = append(r.subscopes, subscope) - r.sm.Unlock() -} - type scope struct { separator string prefix string @@ -69,6 +58,13 @@ type scope struct { timers map[string]*timer } +type scopeRegistry struct { + sync.RWMutex + subscopes map[string]*scope +} + +var scopeRegistryKey = KeyForPrefixedStringMap + // NewRootScope creates a new Scope around a given stats reporter with the // given prefix func NewRootScope( @@ -129,22 +125,27 @@ func newRootScope( } s := &scope{ - separator: sep, - prefix: prefix, - tags: tags, + separator: sep, + prefix: prefix, + // NB(r): Take a copy of the tags on creation + // so that it cannot be modified after set. + tags: copyStringMap(tags), reporter: reporter, cachedReporter: cachedReporter, baseReporter: baseReporter, - registry: &scopeRegistry{}, - quit: make(chan struct{}), + registry: &scopeRegistry{ + subscopes: make(map[string]*scope), + }, + quit: make(chan struct{}), counters: make(map[string]*counter), gauges: make(map[string]*gauge), timers: make(map[string]*timer), } - s.registry.add(s) + // Register the root scope + s.registry.subscopes[scopeRegistryKey(s.prefix, s.tags)] = s if interval > 0 { go s.reportLoop(interval) @@ -196,7 +197,7 @@ func (s *scope) reportLoop(interval time.Duration) { for { select { case <-ticker.C: - s.registry.sm.Lock() + s.registry.RLock() if s.reporter != nil { for _, ss := range s.registry.subscopes { ss.report(s.reporter) @@ -207,7 +208,7 @@ func (s *scope) reportLoop(interval time.Duration) { } } - s.registry.sm.Unlock() + s.registry.RUnlock() case <-s.quit: return } @@ -288,29 +289,43 @@ func (s *scope) Timer(name string) Timer { } func (s *scope) Tagged(tags map[string]string) Scope { - subscope := &scope{ - separator: s.separator, - prefix: s.prefix, - tags: mergeRightTags(s.tags, tags), - reporter: s.reporter, - cachedReporter: s.cachedReporter, - baseReporter: s.baseReporter, - registry: s.registry, + return s.subscope(s.prefix, tags) +} - counters: make(map[string]*counter), - gauges: make(map[string]*gauge), - timers: make(map[string]*timer), +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 { + if len(tags) == 0 { + tags = s.tags + } else { + tags = mergeRightTags(s.tags, tags) } + key := scopeRegistryKey(prefix, tags) - subscope.registry.add(subscope) - return subscope -} + 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 + } -func (s *scope) SubScope(prefix string) Scope { subscope := &scope{ - separator: s.separator, - prefix: s.fullyQualifiedName(prefix), - tags: s.tags, + 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, @@ -321,7 +336,7 @@ func (s *scope) SubScope(prefix string) Scope { timers: make(map[string]*timer), } - subscope.registry.add(subscope) + s.registry.subscopes[key] = subscope return subscope } @@ -329,14 +344,13 @@ func (s *scope) Capabilities() Capabilities { if s.baseReporter == nil { return capabilitiesNone } - return s.baseReporter.Capabilities() } func (s *scope) Snapshot() Snapshot { snap := newSnapshot() - s.registry.sm.RLock() + 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)) @@ -375,7 +389,7 @@ func (s *scope) Snapshot() Snapshot { } ss.tm.RUnlock() } - s.registry.sm.RUnlock() + s.registry.RUnlock() return snap } @@ -389,12 +403,9 @@ func (s *scope) Close() error { } func (s *scope) fullyQualifiedName(name string) string { - // TODO(mmihic): Consider maintaining a map[string]string for common names so we - // avoid the cost of continual allocations if len(s.prefix) == 0 { return name } - return fmt.Sprintf("%s%s%s", s.prefix, s.separator, name) } @@ -461,6 +472,12 @@ 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 { @@ -472,6 +489,14 @@ func mergeRightTags(tagsLeft, tagsRight map[string]string) map[string]string { 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