diff --git a/README.md b/README.md index edbcbe3..6739057 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ +# Major Notes +The project's documentation is *not up-to-date due to rapidly-changing +requirements* that have quieted down in the interim, but the overall API should +be stable for several months even if things change under the hood. + +An update to reflect the current state is pending. Key changes for current +users: + +1. The code has been qualified in production environments. +2. Label-oriented metric exposition and registration, including docstrings. +3. Deprecation of gocheck in favor of native table-driven tests. +4. The best way to get a handle on this is to look at the examples. + +Barring that, the antique documentation is below: + # Overview This [Go](http://golang.org) package is an extraction of a piece of instrumentation code I whipped-up for a personal project that a friend of mine diff --git a/constants.go b/constants.go index 0a7b2fc..3dfaead 100644 --- a/constants.go +++ b/constants.go @@ -1,14 +1,26 @@ -/* -Copyright (c) 2013, Matt T. Proud -All rights reserved. - -Use of this source code is governed by a BSD-style license that can be found in -the LICENSE file. -*/ +// Copyright (c) 2013, Matt T. Proud +// All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. package registry var ( // NilLabels is a nil set of labels merely for end-user convenience. - NilLabels map[string]string + NilLabels map[string]string = nil + + // A prefix to be used to namespace instrumentation flags from others. + FlagNamespace = "telemetry." + + // The format of the exported data. This will match this library's version, + // which subscribes to the Semantic Versioning scheme. + APIVersion = "0.0.1" + + ProtocolVersionHeader = "X-Prometheus-API-Version" + + baseLabelsKey = "baseLabels" + docstringKey = "docstring" + metricKey = "metric" + nameLabel = "name" ) diff --git a/examples/random/main.go b/examples/random/main.go index 598170e..fe41d14 100644 --- a/examples/random/main.go +++ b/examples/random/main.go @@ -37,46 +37,29 @@ func init() { func main() { flag.Parse() - foo_rpc_latency := metrics.CreateHistogram(&metrics.HistogramSpecification{ + rpc_latency := metrics.NewHistogram(&metrics.HistogramSpecification{ Starts: metrics.EquallySizedBucketsFor(0, 200, 4), - BucketMaker: metrics.AccumulatingBucketBuilder(metrics.EvictAndReplaceWith(10, maths.Average), 50), + BucketBuilder: metrics.AccumulatingBucketBuilder(metrics.EvictAndReplaceWith(10, maths.Average), 50), ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.90, 0.99}, }) - foo_rpc_calls := &metrics.CounterMetric{} - bar_rpc_latency := metrics.CreateHistogram(&metrics.HistogramSpecification{ - Starts: metrics.EquallySizedBucketsFor(0, 200, 4), - BucketMaker: metrics.AccumulatingBucketBuilder(metrics.EvictAndReplaceWith(10, maths.Average), 50), - ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.90, 0.99}, - }) - bar_rpc_calls := &metrics.CounterMetric{} - zed_rpc_latency := metrics.CreateHistogram(&metrics.HistogramSpecification{ - Starts: metrics.EquallySizedBucketsFor(0, 200, 4), - BucketMaker: metrics.AccumulatingBucketBuilder(metrics.EvictAndReplaceWith(10, maths.Average), 50), - ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.90, 0.99}, - }) - zed_rpc_calls := &metrics.CounterMetric{} + + rpc_calls := metrics.NewCounter() metrics := registry.NewRegistry() - nilBaseLabels := make(map[string]string) - - metrics.Register("rpc_latency_foo_microseconds", "RPC latency for foo service.", nilBaseLabels, foo_rpc_latency) - metrics.Register("rpc_calls_foo_total", "RPC calls for foo service.", nilBaseLabels, foo_rpc_calls) - metrics.Register("rpc_latency_bar_microseconds", "RPC latency for bar service.", nilBaseLabels, bar_rpc_latency) - metrics.Register("rpc_calls_bar_total", "RPC calls for bar service.", nilBaseLabels, bar_rpc_calls) - metrics.Register("rpc_latency_zed_microseconds", "RPC latency for zed service.", nilBaseLabels, zed_rpc_latency) - metrics.Register("rpc_calls_zed_total", "RPC calls for zed service.", nilBaseLabels, zed_rpc_calls) + metrics.Register("rpc_latency_microseconds", "RPC latency.", registry.NilLabels, rpc_latency) + metrics.Register("rpc_calls_total", "RPC calls.", registry.NilLabels, rpc_calls) go func() { for { - foo_rpc_latency.Add(rand.Float64() * 200) - foo_rpc_calls.Increment() + rpc_latency.Add(map[string]string{"service": "foo"}, rand.Float64()*200) + rpc_calls.Increment(map[string]string{"service": "foo"}) - bar_rpc_latency.Add((rand.NormFloat64() * 10.0) + 100.0) - bar_rpc_calls.Increment() + rpc_latency.Add(map[string]string{"service": "bar"}, (rand.NormFloat64()*10.0)+100.0) + rpc_calls.Increment(map[string]string{"service": "bar"}) - zed_rpc_latency.Add(rand.ExpFloat64()) - zed_rpc_calls.Increment() + rpc_latency.Add(map[string]string{"service": "zed"}, rand.ExpFloat64()) + rpc_calls.Increment(map[string]string{"service": "zed"}) time.Sleep(100 * time.Millisecond) } diff --git a/metrics/accumulating_bucket.go b/metrics/accumulating_bucket.go index 7651a5b..68146f6 100644 --- a/metrics/accumulating_bucket.go +++ b/metrics/accumulating_bucket.go @@ -20,11 +20,11 @@ import ( ) type AccumulatingBucket struct { - observations int elements utility.PriorityQueue + evictionPolicy EvictionPolicy maximumSize int mutex sync.RWMutex - evictionPolicy EvictionPolicy + observations int } /* @@ -35,9 +35,9 @@ behavior set. func AccumulatingBucketBuilder(evictionPolicy EvictionPolicy, maximumSize int) BucketBuilder { return func() Bucket { return &AccumulatingBucket{ - maximumSize: maximumSize, - evictionPolicy: evictionPolicy, elements: make(utility.PriorityQueue, 0, maximumSize), + evictionPolicy: evictionPolicy, + maximumSize: maximumSize, } } } @@ -54,8 +54,8 @@ func (b *AccumulatingBucket) Add(value float64) { size := len(b.elements) v := utility.Item{ - Value: value, Priority: -1 * time.Now().UnixNano(), + Value: value, } if size == b.maximumSize { @@ -69,7 +69,7 @@ func (b *AccumulatingBucket) String() string { b.mutex.RLock() defer b.mutex.RUnlock() - buffer := new(bytes.Buffer) + buffer := &bytes.Buffer{} fmt.Fprintf(buffer, "[AccumulatingBucket with %d elements and %d capacity] { ", len(b.elements), b.maximumSize) @@ -79,7 +79,7 @@ func (b *AccumulatingBucket) String() string { fmt.Fprintf(buffer, "}") - return string(buffer.Bytes()) + return buffer.String() } func (b *AccumulatingBucket) ValueForIndex(index int) float64 { @@ -116,3 +116,14 @@ func (b *AccumulatingBucket) Observations() int { return b.observations } + +func (b *AccumulatingBucket) Reset() { + b.mutex.Lock() + defer b.mutex.RUnlock() + + for i := 0; i < b.elements.Len(); i++ { + b.elements.Pop() + } + + b.observations = 0 +} diff --git a/metrics/bucket.go b/metrics/bucket.go index 649b66a..577a2c1 100644 --- a/metrics/bucket.go +++ b/metrics/bucket.go @@ -29,14 +29,16 @@ type Bucket interface { Add a value to the bucket. */ Add(value float64) - /* - Provide a humanized representation hereof. - */ - String() string /* Provide a count of observations throughout the bucket's lifetime. */ Observations() int + // Reset is responsible for resetting this bucket back to a pristine state. + Reset() + /* + Provide a humanized representation hereof. + */ + String() string /* Provide the value from the given in-memory value cache or an estimate thereof for the given index. The consumer of the bucket's data makes diff --git a/metrics/constants.go b/metrics/constants.go index 0514ae7..cd8d9cd 100644 --- a/metrics/constants.go +++ b/metrics/constants.go @@ -12,12 +12,13 @@ constants.go provides package-level constants for metrics. package metrics const ( - valueKey = "value" - gaugeTypeValue = "gauge" counterTypeValue = "counter" - typeKey = "type" - histogramTypeValue = "histogram" + floatBitCount = 64 floatFormat = 'f' floatPrecision = 6 - floatBitCount = 64 + gaugeTypeValue = "gauge" + histogramTypeValue = "histogram" + typeKey = "type" + valueKey = "value" + labelsKey = "labels" ) diff --git a/metrics/counter.go b/metrics/counter.go index 01549a6..01fd655 100644 --- a/metrics/counter.go +++ b/metrics/counter.go @@ -10,77 +10,145 @@ package metrics import ( "fmt" + "github.com/matttproud/golang_instrumentation/utility" "sync" ) -type CounterMetric struct { - value float64 - mutex sync.RWMutex +// TODO(matt): Refactor to de-duplicate behaviors. + +type Counter interface { + AsMarshallable() map[string]interface{} + Decrement(labels map[string]string) float64 + DecrementBy(labels map[string]string, value float64) float64 + Increment(labels map[string]string) float64 + IncrementBy(labels map[string]string, value float64) float64 + ResetAll() + Set(labels map[string]string, value float64) float64 + String() string } -func (metric *CounterMetric) Set(value float64) float64 { +type counterValue struct { + labels map[string]string + value float64 +} + +func NewCounter() Counter { + return &counter{ + values: map[string]*counterValue{}, + } +} + +type counter struct { + mutex sync.RWMutex + values map[string]*counterValue +} + +func (metric *counter) Set(labels map[string]string, value float64) float64 { metric.mutex.Lock() defer metric.mutex.Unlock() - metric.value = value + if labels == nil { + labels = map[string]string{} + } - return metric.value + signature := utility.LabelsToSignature(labels) + if original, ok := metric.values[signature]; ok { + original.value = value + } else { + metric.values[signature] = &counterValue{ + labels: labels, + value: value, + } + } + + return value } -func (metric *CounterMetric) Reset() { - metric.Set(0) +func (metric *counter) ResetAll() { + metric.mutex.Lock() + defer metric.mutex.Unlock() + + for key, value := range metric.values { + for label := range value.labels { + delete(value.labels, label) + } + delete(metric.values, key) + } } -func (metric *CounterMetric) String() string { - formatString := "[CounterMetric; value=%f]" +func (metric *counter) String() string { + formatString := "[Counter %s]" metric.mutex.RLock() defer metric.mutex.RUnlock() - return fmt.Sprintf(formatString, metric.value) + return fmt.Sprintf(formatString, metric.values) } -func (metric *CounterMetric) IncrementBy(value float64) float64 { +func (metric *counter) IncrementBy(labels map[string]string, value float64) float64 { metric.mutex.Lock() defer metric.mutex.Unlock() - metric.value += value + if labels == nil { + labels = map[string]string{} + } - return metric.value + signature := utility.LabelsToSignature(labels) + if original, ok := metric.values[signature]; ok { + original.value += value + } else { + metric.values[signature] = &counterValue{ + labels: labels, + value: value, + } + } + + return value } -func (metric *CounterMetric) Increment() float64 { - return metric.IncrementBy(1) +func (metric *counter) Increment(labels map[string]string) float64 { + return metric.IncrementBy(labels, 1) } -func (metric *CounterMetric) DecrementBy(value float64) float64 { +func (metric *counter) DecrementBy(labels map[string]string, value float64) float64 { metric.mutex.Lock() defer metric.mutex.Unlock() - metric.value -= value + if labels == nil { + labels = map[string]string{} + } - return metric.value + signature := utility.LabelsToSignature(labels) + if original, ok := metric.values[signature]; ok { + original.value -= value + } else { + metric.values[signature] = &counterValue{ + labels: labels, + value: -1 * value, + } + } + + return value } -func (metric *CounterMetric) Decrement() float64 { - return metric.DecrementBy(1) +func (metric *counter) Decrement(labels map[string]string) float64 { + return metric.DecrementBy(labels, 1) } -func (metric *CounterMetric) Get() float64 { +func (metric *counter) AsMarshallable() map[string]interface{} { metric.mutex.RLock() defer metric.mutex.RUnlock() - return metric.value -} - -func (metric *CounterMetric) Marshallable() map[string]interface{} { - metric.mutex.RLock() - defer metric.mutex.RUnlock() - - v := make(map[string]interface{}, 2) - - v[valueKey] = metric.value - v[typeKey] = counterTypeValue - - return v + values := make([]map[string]interface{}, 0, len(metric.values)) + for _, value := range metric.values { + values = append(values, map[string]interface{}{ + labelsKey: value.labels, + valueKey: value.value, + }) + } + + return map[string]interface{}{ + valueKey: values, + typeKey: counterTypeValue, + } } diff --git a/metrics/counter_test.go b/metrics/counter_test.go index f47a916..c6063b1 100644 --- a/metrics/counter_test.go +++ b/metrics/counter_test.go @@ -9,90 +9,215 @@ license that can be found in the LICENSE file. package metrics import ( - . "github.com/matttproud/gocheck" + "encoding/json" + "github.com/matttproud/golang_instrumentation/utility/test" + "testing" ) -func (s *S) TestCounterCreate(c *C) { - m := CounterMetric{value: 1.0} +func testCounter(t test.Tester) { + type input struct { + steps []func(g Counter) + } + type output struct { + value string + } - c.Assert(m, Not(IsNil)) + var scenarios = []struct { + in input + out output + }{ + { + in: input{ + steps: []func(g Counter){}, + }, + out: output{ + value: "{\"type\":\"counter\",\"value\":[]}", + }, + }, + { + in: input{ + steps: []func(g Counter){ + func(g Counter) { + g.Set(nil, 1) + }, + }, + }, + out: output{ + value: "{\"type\":\"counter\",\"value\":[{\"labels\":{},\"value\":1}]}", + }, + }, + { + in: input{ + steps: []func(g Counter){ + func(g Counter) { + g.Set(map[string]string{}, 2) + }, + }, + }, + out: output{ + value: "{\"type\":\"counter\",\"value\":[{\"labels\":{},\"value\":2}]}", + }, + }, + { + in: input{ + steps: []func(g Counter){ + func(g Counter) { + g.Set(map[string]string{}, 3) + }, + func(g Counter) { + g.Set(map[string]string{}, 5) + }, + }, + }, + out: output{ + value: "{\"type\":\"counter\",\"value\":[{\"labels\":{},\"value\":5}]}", + }, + }, + { + in: input{ + steps: []func(g Counter){ + func(g Counter) { + g.Set(map[string]string{"handler": "/foo"}, 13) + }, + func(g Counter) { + g.Set(map[string]string{"handler": "/bar"}, 17) + }, + func(g Counter) { + g.ResetAll() + }, + }, + }, + out: output{ + value: "{\"type\":\"counter\",\"value\":[]}", + }, + }, + { + in: input{ + steps: []func(g Counter){ + func(g Counter) { + g.Set(map[string]string{"handler": "/foo"}, 19) + }, + }, + }, + out: output{ + value: "{\"type\":\"counter\",\"value\":[{\"labels\":{\"handler\":\"/foo\"},\"value\":19}]}", + }, + }, + { + in: input{ + steps: []func(g Counter){ + func(g Counter) { + g.Set(map[string]string{"handler": "/foo"}, 23) + }, + func(g Counter) { + g.Increment(map[string]string{"handler": "/foo"}) + }, + }, + }, + out: output{ + value: "{\"type\":\"counter\",\"value\":[{\"labels\":{\"handler\":\"/foo\"},\"value\":24}]}", + }, + }, + { + in: input{ + steps: []func(g Counter){ + func(g Counter) { + g.Increment(map[string]string{"handler": "/foo"}) + }, + }, + }, + out: output{ + value: "{\"type\":\"counter\",\"value\":[{\"labels\":{\"handler\":\"/foo\"},\"value\":1}]}", + }, + }, + { + in: input{ + steps: []func(g Counter){ + func(g Counter) { + g.Decrement(map[string]string{"handler": "/foo"}) + }, + }, + }, + out: output{ + value: "{\"type\":\"counter\",\"value\":[{\"labels\":{\"handler\":\"/foo\"},\"value\":-1}]}", + }, + }, + { + in: input{ + steps: []func(g Counter){ + func(g Counter) { + g.Set(map[string]string{"handler": "/foo"}, 29) + }, + func(g Counter) { + g.Decrement(map[string]string{"handler": "/foo"}) + }, + }, + }, + out: output{ + value: "{\"type\":\"counter\",\"value\":[{\"labels\":{\"handler\":\"/foo\"},\"value\":28}]}", + }, + }, + { + in: input{ + steps: []func(g Counter){ + func(g Counter) { + g.Set(map[string]string{"handler": "/foo"}, 31) + }, + func(g Counter) { + g.IncrementBy(map[string]string{"handler": "/foo"}, 5) + }, + }, + }, + out: output{ + value: "{\"type\":\"counter\",\"value\":[{\"labels\":{\"handler\":\"/foo\"},\"value\":36}]}", + }, + }, + { + in: input{ + steps: []func(g Counter){ + func(g Counter) { + g.Set(map[string]string{"handler": "/foo"}, 37) + }, + func(g Counter) { + g.DecrementBy(map[string]string{"handler": "/foo"}, 10) + }, + }, + }, + out: output{ + value: "{\"type\":\"counter\",\"value\":[{\"labels\":{\"handler\":\"/foo\"},\"value\":27}]}", + }, + }, + } + + for i, scenario := range scenarios { + counter := NewCounter() + + for _, step := range scenario.in.steps { + step(counter) + } + + marshallable := counter.AsMarshallable() + + bytes, err := json.Marshal(marshallable) + if err != nil { + t.Errorf("%d. could not marshal into JSON %s", i, err) + continue + } + + asString := string(bytes) + + if scenario.out.value != asString { + t.Errorf("%d. expected %q, got %q", i, scenario.out.value, asString) + } + } } -func (s *S) TestCounterGet(c *C) { - m := CounterMetric{value: 42.23} - - c.Check(m.Get(), Equals, 42.23) +func TestCounter(t *testing.T) { + testCounter(t) } -func (s *S) TestCounterSet(c *C) { - m := CounterMetric{value: 42.23} - m.Set(40.4) - - c.Check(m.Get(), Equals, 40.4) -} - -func (s *S) TestCounterReset(c *C) { - m := CounterMetric{value: 42.23} - m.Reset() - - c.Check(m.Get(), Equals, 0.0) -} - -func (s *S) TestCounterIncrementBy(c *C) { - m := CounterMetric{value: 1.0} - - m.IncrementBy(1.5) - - c.Check(m.Get(), Equals, 2.5) - c.Check(m.String(), Equals, "[CounterMetric; value=2.500000]") -} - -func (s *S) TestCounterIncrement(c *C) { - m := CounterMetric{value: 1.0} - - m.Increment() - - c.Check(m.Get(), Equals, 2.0) - c.Check(m.String(), Equals, "[CounterMetric; value=2.000000]") -} - -func (s *S) TestCounterDecrementBy(c *C) { - m := CounterMetric{value: 1.0} - - m.DecrementBy(1.0) - - c.Check(m.Get(), Equals, 0.0) - c.Check(m.String(), Equals, "[CounterMetric; value=0.000000]") -} - -func (s *S) TestCounterDecrement(c *C) { - m := CounterMetric{value: 1.0} - - m.Decrement() - - c.Check(m.Get(), Equals, 0.0) - c.Check(m.String(), Equals, "[CounterMetric; value=0.000000]") -} - -func (s *S) TestCounterString(c *C) { - m := CounterMetric{value: 2.0} - c.Check(m.String(), Equals, "[CounterMetric; value=2.000000]") -} - -func (s *S) TestCounterMetricMarshallable(c *C) { - m := CounterMetric{value: 1.0} - - returned := m.Marshallable() - - c.Assert(returned, Not(IsNil)) - - c.Check(returned, HasLen, 2) - c.Check(returned["value"], Equals, 1.0) - c.Check(returned["type"], Equals, "counter") -} - -func (s *S) TestCounterAsMetric(c *C) { - var metric Metric = &CounterMetric{value: 1.0} - - c.Assert(metric, Not(IsNil)) +func BenchmarkCounter(b *testing.B) { + for i := 0; i < b.N; i++ { + testCounter(b) + } } diff --git a/metrics/eviction_test.go b/metrics/eviction_test.go index 0c65244..033ca5b 100644 --- a/metrics/eviction_test.go +++ b/metrics/eviction_test.go @@ -22,8 +22,8 @@ func (s *S) TestEvictOldest(c *C) { for i := 0; i < 10; i++ { var item utility.Item = utility.Item{ - Value: float64(i), Priority: int64(i), + Value: float64(i), } heap.Push(&q, &item) @@ -49,8 +49,8 @@ func (s *S) TestEvictAndReplaceWithAverage(c *C) { for i := 0; i < 10; i++ { var item utility.Item = utility.Item{ - Value: float64(i), Priority: int64(i), + Value: float64(i), } heap.Push(&q, &item) @@ -77,8 +77,8 @@ func (s *S) TestEvictAndReplaceWithMedian(c *C) { for i := 0; i < 10; i++ { var item utility.Item = utility.Item{ - Value: float64(i), Priority: int64(i), + Value: float64(i), } heap.Push(&q, &item) @@ -105,8 +105,8 @@ func (s *S) TestEvictAndReplaceWithFirstMode(c *C) { for i := 0; i < 10; i++ { heap.Push(&q, &utility.Item{ - Value: float64(i), Priority: int64(i), + Value: float64(i), }) } @@ -131,8 +131,8 @@ func (s *S) TestEvictAndReplaceWithMinimum(c *C) { for i := 0; i < 10; i++ { var item utility.Item = utility.Item{ - Value: float64(i), Priority: int64(i), + Value: float64(i), } heap.Push(&q, &item) @@ -159,8 +159,8 @@ func (s *S) TestEvictAndReplaceWithMaximum(c *C) { for i := 0; i < 10; i++ { var item utility.Item = utility.Item{ - Value: float64(i), Priority: int64(i), + Value: float64(i), } heap.Push(&q, &item) diff --git a/metrics/gauge.go b/metrics/gauge.go index 4197336..f1cc147 100644 --- a/metrics/gauge.go +++ b/metrics/gauge.go @@ -10,6 +10,7 @@ package metrics import ( "fmt" + "github.com/matttproud/golang_instrumentation/utility" "sync" ) @@ -19,44 +20,86 @@ value or an accumulation. For instance, if one wants to expose the current temperature or the hitherto bandwidth used, this would be the metric for such circumstances. */ -type GaugeMetric struct { - value float64 - mutex sync.RWMutex +type Gauge interface { + AsMarshallable() map[string]interface{} + ResetAll() + Set(labels map[string]string, value float64) float64 + String() string } -func (metric *GaugeMetric) String() string { - formatString := "[GaugeMetric; value=%f]" +type gaugeValue struct { + labels map[string]string + value float64 +} + +func NewGauge() Gauge { + return &gauge{ + values: map[string]*gaugeValue{}, + } +} + +type gauge struct { + mutex sync.RWMutex + values map[string]*gaugeValue +} + +func (metric *gauge) String() string { + formatString := "[Gauge %s]" metric.mutex.RLock() defer metric.mutex.RUnlock() - return fmt.Sprintf(formatString, metric.value) + return fmt.Sprintf(formatString, metric.values) } -func (metric *GaugeMetric) Set(value float64) float64 { +func (metric *gauge) Set(labels map[string]string, value float64) float64 { metric.mutex.Lock() defer metric.mutex.Unlock() - metric.value = value + if labels == nil { + labels = map[string]string{} + } - return metric.value + signature := utility.LabelsToSignature(labels) + + if original, ok := metric.values[signature]; ok { + original.value = value + } else { + metric.values[signature] = &gaugeValue{ + labels: labels, + value: value, + } + } + + return value } -func (metric *GaugeMetric) Get() float64 { +func (metric *gauge) ResetAll() { + metric.mutex.Lock() + defer metric.mutex.Unlock() + + for key, value := range metric.values { + for label := range value.labels { + delete(value.labels, label) + } + delete(metric.values, key) + } +} + +func (metric *gauge) AsMarshallable() map[string]interface{} { metric.mutex.RLock() defer metric.mutex.RUnlock() - return metric.value -} - -func (metric *GaugeMetric) Marshallable() map[string]interface{} { - metric.mutex.RLock() - defer metric.mutex.RUnlock() - - v := make(map[string]interface{}, 2) - - v[valueKey] = metric.value - v[typeKey] = gaugeTypeValue - - return v + values := make([]map[string]interface{}, 0, len(metric.values)) + for _, value := range metric.values { + values = append(values, map[string]interface{}{ + labelsKey: value.labels, + valueKey: value.value, + }) + } + + return map[string]interface{}{ + typeKey: gaugeTypeValue, + valueKey: values, + } } diff --git a/metrics/gauge_test.go b/metrics/gauge_test.go index 6e9c011..2110785 100644 --- a/metrics/gauge_test.go +++ b/metrics/gauge_test.go @@ -9,43 +9,131 @@ license that can be found in the LICENSE file. package metrics import ( - . "github.com/matttproud/gocheck" + "encoding/json" + "github.com/matttproud/golang_instrumentation/utility/test" + "testing" ) -func (s *S) TestGaugeCreate(c *C) { - m := GaugeMetric{value: 1.0} +func testGauge(t test.Tester) { + type input struct { + steps []func(g Gauge) + } + type output struct { + value string + } - c.Assert(m, Not(IsNil)) - c.Check(m.Get(), Equals, 1.0) + var scenarios = []struct { + in input + out output + }{ + { + in: input{ + steps: []func(g Gauge){}, + }, + out: output{ + value: "{\"type\":\"gauge\",\"value\":[]}", + }, + }, + { + in: input{ + steps: []func(g Gauge){ + func(g Gauge) { + g.Set(nil, 1) + }, + }, + }, + out: output{ + value: "{\"type\":\"gauge\",\"value\":[{\"labels\":{},\"value\":1}]}", + }, + }, + { + in: input{ + steps: []func(g Gauge){ + func(g Gauge) { + g.Set(map[string]string{}, 2) + }, + }, + }, + out: output{ + value: "{\"type\":\"gauge\",\"value\":[{\"labels\":{},\"value\":2}]}", + }, + }, + { + in: input{ + steps: []func(g Gauge){ + func(g Gauge) { + g.Set(map[string]string{}, 3) + }, + func(g Gauge) { + g.Set(map[string]string{}, 5) + }, + }, + }, + out: output{ + value: "{\"type\":\"gauge\",\"value\":[{\"labels\":{},\"value\":5}]}", + }, + }, + { + in: input{ + steps: []func(g Gauge){ + func(g Gauge) { + g.Set(map[string]string{"handler": "/foo"}, 13) + }, + func(g Gauge) { + g.Set(map[string]string{"handler": "/bar"}, 17) + }, + func(g Gauge) { + g.ResetAll() + }, + }, + }, + out: output{ + value: "{\"type\":\"gauge\",\"value\":[]}", + }, + }, + { + in: input{ + steps: []func(g Gauge){ + func(g Gauge) { + g.Set(map[string]string{"handler": "/foo"}, 19) + }, + }, + }, + out: output{ + value: "{\"type\":\"gauge\",\"value\":[{\"labels\":{\"handler\":\"/foo\"},\"value\":19}]}", + }, + }, + } + + for i, scenario := range scenarios { + gauge := NewGauge() + + for _, step := range scenario.in.steps { + step(gauge) + } + + marshallable := gauge.AsMarshallable() + + bytes, err := json.Marshal(marshallable) + if err != nil { + t.Errorf("%d. could not marshal into JSON %s", i, err) + continue + } + + asString := string(bytes) + + if scenario.out.value != asString { + t.Errorf("%d. expected %q, got %q", i, scenario.out.value, asString) + } + } } -func (s *S) TestGaugeString(c *C) { - m := GaugeMetric{value: 2.0} - c.Check(m.String(), Equals, "[GaugeMetric; value=2.000000]") +func TestGauge(t *testing.T) { + testGauge(t) } -func (s *S) TestGaugeSet(c *C) { - m := GaugeMetric{value: -1.0} - - m.Set(-99.0) - - c.Check(m.Get(), Equals, -99.0) -} - -func (s *S) TestGaugeMetricMarshallable(c *C) { - m := GaugeMetric{value: 1.0} - - returned := m.Marshallable() - - c.Assert(returned, Not(IsNil)) - - c.Check(returned, HasLen, 2) - c.Check(returned["value"], Equals, 1.0) - c.Check(returned["type"], Equals, "gauge") -} - -func (s *S) TestGaugeAsMetric(c *C) { - var metric Metric = &GaugeMetric{value: 1.0} - - c.Assert(metric, Not(IsNil)) +func BenchmarkGauge(b *testing.B) { + for i := 0; i < b.N; i++ { + testGauge(b) + } } diff --git a/metrics/histogram.go b/metrics/histogram.go index e5baad8..e7ea1f4 100644 --- a/metrics/histogram.go +++ b/metrics/histogram.go @@ -11,8 +11,10 @@ package metrics import ( "bytes" "fmt" + "github.com/matttproud/golang_instrumentation/utility" "math" "strconv" + "sync" ) /* @@ -53,21 +55,25 @@ func LogarithmicSizedBucketsFor(lower, upper float64) []float64 { A HistogramSpecification defines how a Histogram is to be built. */ type HistogramSpecification struct { - Starts []float64 - BucketMaker BucketBuilder + BucketBuilder BucketBuilder ReportablePercentiles []float64 + Starts []float64 +} + +type Histogram interface { + Add(labels map[string]string, value float64) + AsMarshallable() map[string]interface{} + ResetAll() + String() string } /* The histogram is an accumulator for samples. It merely routes into which to bucket to capture an event and provides a percentile calculation mechanism. - -Histogram makes do without locking by employing the law of large numbers -to presume a convergence toward a given bucket distribution. Locking -may be implemented in the buckets themselves, though. */ -type Histogram struct { +type histogram struct { + bucketMaker BucketBuilder /* This represents the open interval's start at which values shall be added to the bucket. The interval continues until the beginning of the next bucket @@ -76,23 +82,52 @@ type Histogram struct { N.B. - bucketStarts should be sorted in ascending order; - len(bucketStarts) must be equivalent to len(buckets); - - The index of a given bucketStarts' element is presumed to match + - The index of a given bucketStarts' element is presumed to correspond to the appropriate element in buckets. */ bucketStarts []float64 + mutex sync.RWMutex /* These are the buckets that capture samples as they are emitted to the histogram. Please consult the reference interface and its implements for further details about behavior expectations. */ - buckets []Bucket + values map[string]*histogramValue /* These are the percentile values that will be reported on marshalling. */ reportablePercentiles []float64 } -func (h *Histogram) Add(value float64) { +type histogramValue struct { + buckets []Bucket + labels map[string]string +} + +func (h *histogram) Add(labels map[string]string, value float64) { + h.mutex.Lock() + defer h.mutex.Unlock() + + if labels == nil { + labels = map[string]string{} + } + + signature := utility.LabelsToSignature(labels) + var histogram *histogramValue = nil + if original, ok := h.values[signature]; ok { + histogram = original + } else { + bucketCount := len(h.bucketStarts) + histogram = &histogramValue{ + buckets: make([]Bucket, bucketCount), + labels: labels, + } + for i := 0; i < bucketCount; i++ { + histogram.buckets[i] = h.bucketMaker() + } + h.values[signature] = histogram + } + lastIndex := 0 for i, bucketStart := range h.bucketStarts { @@ -103,21 +138,27 @@ func (h *Histogram) Add(value float64) { lastIndex = i } - h.buckets[lastIndex].Add(value) + histogram.buckets[lastIndex].Add(value) } -func (h *Histogram) String() string { - stringBuffer := bytes.NewBufferString("") +func (h *histogram) String() string { + h.mutex.RLock() + defer h.mutex.RUnlock() + + stringBuffer := &bytes.Buffer{} stringBuffer.WriteString("[Histogram { ") - for i, bucketStart := range h.bucketStarts { - bucket := h.buckets[i] - stringBuffer.WriteString(fmt.Sprintf("[%f, inf) = %s, ", bucketStart, bucket.String())) + for _, histogram := range h.values { + fmt.Fprintf(stringBuffer, "Labels: %s ", histogram.labels) + for i, bucketStart := range h.bucketStarts { + bucket := histogram.buckets[i] + fmt.Fprintf(stringBuffer, "[%f, inf) = %s, ", bucketStart, bucket) + } } stringBuffer.WriteString("}]") - return string(stringBuffer.Bytes()) + return stringBuffer.String() } /* @@ -141,13 +182,15 @@ func prospectiveIndexForPercentile(percentile float64, totalObservations int) in /* Determine the next bucket element when interim bucket intervals may be empty. */ -func (h *Histogram) nextNonEmptyBucketElement(currentIndex, bucketCount int, observationsByBucket []int) (*Bucket, int) { +func (h *histogram) nextNonEmptyBucketElement(signature string, currentIndex, bucketCount int, observationsByBucket []int) (*Bucket, int) { for i := currentIndex; i < bucketCount; i++ { if observationsByBucket[i] == 0 { continue } - return &h.buckets[i], 0 + histogram := h.values[signature] + + return &histogram.buckets[i], 0 } panic("Illegal Condition: There were no remaining buckets to provide a value.") @@ -160,8 +203,8 @@ longer contained by the bucket, the index of the last item is returned. This may occur if the underlying bucket catalogs values and employs an eviction strategy. */ -func (h *Histogram) bucketForPercentile(percentile float64) (*Bucket, int) { - bucketCount := len(h.buckets) +func (h *histogram) bucketForPercentile(signature string, percentile float64) (*Bucket, int) { + bucketCount := len(h.bucketStarts) /* This captures the quantity of samples in a given bucket's range. @@ -173,9 +216,11 @@ func (h *Histogram) bucketForPercentile(percentile float64) (*Bucket, int) { */ cumulativeObservationsByBucket := make([]int, bucketCount) - var totalObservations int = 0 + totalObservations := 0 - for i, bucket := range h.buckets { + histogram := h.values[signature] + + for i, bucket := range histogram.buckets { observations := bucket.Observations() observationsByBucket[i] = observations totalObservations += bucket.Observations() @@ -210,14 +255,14 @@ func (h *Histogram) bucketForPercentile(percentile float64) (*Bucket, int) { take this into account. */ if observationsByBucket[i] == subIndex { - return h.nextNonEmptyBucketElement(i+1, bucketCount, observationsByBucket) + return h.nextNonEmptyBucketElement(signature, i+1, bucketCount, observationsByBucket) } - return &h.buckets[i], subIndex + return &histogram.buckets[i], subIndex } } - return &h.buckets[0], 0 + return &histogram.buckets[0], 0 } /* @@ -225,8 +270,8 @@ Return the histogram's estimate of the value for a given percentile of collected samples. The requested percentile is expected to be a real value within (0, 1.0]. */ -func (h *Histogram) Percentile(percentile float64) float64 { - bucket, index := h.bucketForPercentile(percentile) +func (h *histogram) percentile(signature string, percentile float64) float64 { + bucket, index := h.bucketForPercentile(signature, percentile) return (*bucket).ValueForIndex(index) } @@ -235,38 +280,53 @@ func formatFloat(value float64) string { return strconv.FormatFloat(value, floatFormat, floatPrecision, floatBitCount) } -func (h *Histogram) Marshallable() map[string]interface{} { - numberOfPercentiles := len(h.reportablePercentiles) +func (h *histogram) AsMarshallable() map[string]interface{} { + h.mutex.RLock() + defer h.mutex.RUnlock() + result := make(map[string]interface{}, 2) - result[typeKey] = histogramTypeValue + values := make([]map[string]interface{}, 0, len(h.values)) - value := make(map[string]interface{}, numberOfPercentiles) - - for _, percentile := range h.reportablePercentiles { - percentileString := formatFloat(percentile) - value[percentileString] = formatFloat(h.Percentile(percentile)) + for signature, value := range h.values { + metricContainer := map[string]interface{}{} + metricContainer[labelsKey] = value.labels + intermediate := map[string]interface{}{} + for _, percentile := range h.reportablePercentiles { + formatted := formatFloat(percentile) + intermediate[formatted] = h.percentile(signature, percentile) + } + metricContainer[valueKey] = intermediate + values = append(values, metricContainer) } - result[valueKey] = value + result[valueKey] = values return result } +func (h *histogram) ResetAll() { + h.mutex.Lock() + defer h.mutex.Unlock() + + for signature, value := range h.values { + for _, bucket := range value.buckets { + bucket.Reset() + } + + delete(h.values, signature) + } +} + /* Produce a histogram from a given specification. */ -func CreateHistogram(specification *HistogramSpecification) *Histogram { - bucketCount := len(specification.Starts) - - metric := &Histogram{ +func NewHistogram(specification *HistogramSpecification) Histogram { + metric := &histogram{ + bucketMaker: specification.BucketBuilder, bucketStarts: specification.Starts, - buckets: make([]Bucket, bucketCount), reportablePercentiles: specification.ReportablePercentiles, - } - - for i := 0; i < bucketCount; i++ { - metric.buckets[i] = specification.BucketMaker() + values: map[string]*histogramValue{}, } return metric diff --git a/metrics/histogram_test.go b/metrics/histogram_test.go index 7831e0b..5171059 100644 --- a/metrics/histogram_test.go +++ b/metrics/histogram_test.go @@ -1,974 +1,9 @@ -/* -Copyright (c) 2012, Matt T. Proud -All rights reserved. - -Use of this source code is governed by a BSD-style -license that can be found in the LICENSE file. -*/ +// Copyright (c) 2013, Matt T. Proud +// All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. package metrics -import ( - . "github.com/matttproud/gocheck" - "github.com/matttproud/golang_instrumentation/maths" -) - -func (s *S) TestEquallySizedBucketsFor(c *C) { - h := EquallySizedBucketsFor(0, 10, 5) - - c.Assert(h, Not(IsNil)) - c.Check(h, HasLen, 5) - c.Check(h[0], Equals, 0.0) - c.Check(h[1], Equals, 2.0) - c.Check(h[2], Equals, 4.0) - c.Check(h[3], Equals, 6.0) - c.Check(h[4], Equals, 8.0) -} - -func (s *S) TestLogarithmicSizedBucketsFor(c *C) { - h := LogarithmicSizedBucketsFor(0, 2048) - - c.Assert(h, Not(IsNil)) - c.Check(h, HasLen, 11) - c.Check(h[0], Equals, 0.0) - c.Check(h[1], Equals, 2.0) - c.Check(h[2], Equals, 4.0) - c.Check(h[3], Equals, 8.0) - c.Check(h[4], Equals, 16.0) - c.Check(h[5], Equals, 32.0) - c.Check(h[6], Equals, 64.0) - c.Check(h[7], Equals, 128.0) - c.Check(h[8], Equals, 256.0) - c.Check(h[9], Equals, 512.0) - c.Check(h[10], Equals, 1024.0) -} - -func (s *S) TestCreateHistogram(c *C) { - hs := &HistogramSpecification{ - Starts: EquallySizedBucketsFor(0, 10, 5), - BucketMaker: TallyingBucketBuilder, - } - - h := CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - c.Check(h.String(), Equals, "[Histogram { [0.000000, inf) = [TallyingBucket (Empty)], [2.000000, inf) = [TallyingBucket (Empty)], [4.000000, inf) = [TallyingBucket (Empty)], [6.000000, inf) = [TallyingBucket (Empty)], [8.000000, inf) = [TallyingBucket (Empty)], }]") - - h.Add(1) - - c.Check(h.String(), Equals, "[Histogram { [0.000000, inf) = [TallyingBucket (1.000000, 1.000000); 1 items], [2.000000, inf) = [TallyingBucket (Empty)], [4.000000, inf) = [TallyingBucket (Empty)], [6.000000, inf) = [TallyingBucket (Empty)], [8.000000, inf) = [TallyingBucket (Empty)], }]") -} - -func (s *S) TestBucketForPercentile(c *C) { - hs := &HistogramSpecification{ - Starts: EquallySizedBucketsFor(0, 100, 100), - BucketMaker: TallyingBucketBuilder, - } - - h := CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - var bucket *Bucket = nil - var subindex int = 0 - - for i := 0.0; i < 1.0; i += 0.01 { - bucket, subindex := h.bucketForPercentile(i) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - } - - h.Add(1.0) - - bucket, subindex = h.bucketForPercentile(0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - for i := 2.0; i <= 100.0; i++ { - h.Add(i) - } - - bucket, subindex = h.bucketForPercentile(0.05) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - for i := 0; i < 50; i++ { - h.Add(50) - h.Add(51) - } - - bucket, subindex = h.bucketForPercentile(0.50) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 50) - c.Check((*bucket).Observations(), Equals, 51) - - bucket, subindex = h.bucketForPercentile(0.51) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 51) -} - -func (s *S) TestBucketForPercentileSingleton(c *C) { - hs := &HistogramSpecification{ - Starts: EquallySizedBucketsFor(0, 3, 3), - BucketMaker: TallyingBucketBuilder, - } - - var h *Histogram = CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - var bucket *Bucket = nil - var subindex int = 0 - - for i := 0.0; i < 1.0; i += 0.01 { - bucket, subindex := h.bucketForPercentile(i) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - } - - h.Add(0.0) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.5) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - h = CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - for i := 0.0; i < 1.0; i += 0.01 { - bucket, subindex := h.bucketForPercentile(i) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - } - - h.Add(1.0) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.5) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - h = CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - for i := 0.0; i < 1.0; i += 0.01 { - bucket, subindex := h.bucketForPercentile(i) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - } - - h.Add(2.0) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.5) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) -} - -func (s *S) TestBucketForPercentileDoubleInSingleBucket(c *C) { - hs := &HistogramSpecification{ - Starts: EquallySizedBucketsFor(0, 3, 3), - BucketMaker: TallyingBucketBuilder, - } - - var h *Histogram = CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - var bucket *Bucket = nil - var subindex int = 0 - - for i := 0.0; i < 1.0; i += 0.01 { - bucket, subindex := h.bucketForPercentile(i) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - } - - h.Add(0.0) - h.Add(0.0) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(0.5) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) - - h = CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - for i := 0.0; i < 1.0; i += 0.01 { - bucket, subindex := h.bucketForPercentile(i) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - } - - h.Add(1.0) - h.Add(1.0) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(0.5) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) - - h = CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - for i := 0.0; i < 1.0; i += 0.01 { - bucket, subindex := h.bucketForPercentile(i) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - } - - h.Add(2.0) - h.Add(2.0) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(0.5) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) -} - -func (s *S) TestBucketForPercentileTripleInSingleBucket(c *C) { - hs := &HistogramSpecification{ - Starts: EquallySizedBucketsFor(0, 3, 3), - BucketMaker: TallyingBucketBuilder, - } - - var h *Histogram = CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - var bucket *Bucket = nil - var subindex int = 0 - - for i := 0.0; i < 1.0; i += 0.01 { - bucket, subindex := h.bucketForPercentile(i) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - } - - h.Add(0.0) - h.Add(0.0) - h.Add(0.0) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 2) - c.Check((*bucket).Observations(), Equals, 3) - - bucket, subindex = h.bucketForPercentile(0.67) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 3) - - bucket, subindex = h.bucketForPercentile(2.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 3) - - bucket, subindex = h.bucketForPercentile(0.5) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 3) - - bucket, subindex = h.bucketForPercentile(1.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 3) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 3) - - h = CreateHistogram(hs) - - h.Add(1.0) - h.Add(1.0) - h.Add(1.0) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 2) - c.Check((*bucket).Observations(), Equals, 3) - - bucket, subindex = h.bucketForPercentile(0.67) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 3) - - bucket, subindex = h.bucketForPercentile(2.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 3) - - bucket, subindex = h.bucketForPercentile(0.5) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 3) - - bucket, subindex = h.bucketForPercentile(1.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 3) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 3) - - h = CreateHistogram(hs) - - h.Add(2.0) - h.Add(2.0) - h.Add(2.0) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 2) - c.Check((*bucket).Observations(), Equals, 3) - - bucket, subindex = h.bucketForPercentile(0.67) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 3) - - bucket, subindex = h.bucketForPercentile(2.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 3) - - bucket, subindex = h.bucketForPercentile(0.5) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 3) - - bucket, subindex = h.bucketForPercentile(1.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 3) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 3) -} - -func (s *S) TestBucketForPercentileTwoEqualAdjacencies(c *C) { - hs := &HistogramSpecification{ - Starts: EquallySizedBucketsFor(0, 3, 3), - BucketMaker: TallyingBucketBuilder, - } - - var h *Histogram = CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - var bucket *Bucket = nil - var subindex int = 0 - - for i := 0.0; i < 1.0; i += 0.01 { - bucket, subindex := h.bucketForPercentile(i) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - } - - h.Add(0.0) - h.Add(1.0) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.67) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(2.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.5) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(1.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - h = CreateHistogram(hs) - - h.Add(1.0) - h.Add(2.0) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.67) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(2.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.5) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(1.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) -} - -func (s *S) TestBucketForPercentileTwoAdjacenciesUnequal(c *C) { - hs := &HistogramSpecification{ - Starts: EquallySizedBucketsFor(0, 3, 3), - BucketMaker: TallyingBucketBuilder, - } - - var h *Histogram = CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - var bucket *Bucket = nil - var subindex int = 0 - - for i := 0.0; i < 1.0; i += 0.01 { - bucket, subindex := h.bucketForPercentile(i) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - } - - h.Add(0.0) - h.Add(0.0) - h.Add(1.0) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.67) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(2.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(0.5) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(1.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) - - h = CreateHistogram(hs) - - h.Add(0.0) - h.Add(1.0) - h.Add(1.0) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(0.67) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(2.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(0.5) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(1.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - h = CreateHistogram(hs) - - h.Add(1.0) - h.Add(1.0) - h.Add(2.0) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.67) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(2.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(0.5) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(1.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) - - h = CreateHistogram(hs) - - h.Add(1.0) - h.Add(2.0) - h.Add(2.0) - - bucket, subindex = h.bucketForPercentile(1.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 1) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(0.67) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(2.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(0.5) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 2) - - bucket, subindex = h.bucketForPercentile(1.0 / 3.0) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(0.01) - - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) -} - -func (s *S) TestBucketForPercentileWithBinomialApproximation(c *C) { - hs := &HistogramSpecification{ - Starts: EquallySizedBucketsFor(0, 5, 6), - BucketMaker: TallyingBucketBuilder, - } - - c.Assert(hs, Not(IsNil)) - - h := CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - n := 5 - p := 0.5 - - for k := 0; k < 6; k++ { - limit := 1000000.0 * maths.BinomialPDF(k, n, p) - for j := 0.0; j < limit; j++ { - h.Add(float64(k)) - } - } - - var bucket *Bucket = nil - var subindex int = 0 - - bucket, subindex = h.bucketForPercentile(0.0) - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 31250) - - bucket, subindex = h.bucketForPercentile(0.03125) - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 31249) - c.Check((*bucket).Observations(), Equals, 31250) - - bucket, subindex = h.bucketForPercentile(0.1875) - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 156249) - c.Check((*bucket).Observations(), Equals, 156250) - - bucket, subindex = h.bucketForPercentile(0.50) - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 312499) - c.Check((*bucket).Observations(), Equals, 312500) - - bucket, subindex = h.bucketForPercentile(0.8125) - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 312499) - c.Check((*bucket).Observations(), Equals, 312500) - - bucket, subindex = h.bucketForPercentile(0.96875) - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 156249) - c.Check((*bucket).Observations(), Equals, 156250) - - bucket, subindex = h.bucketForPercentile(1.0) - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 31249) - c.Check((*bucket).Observations(), Equals, 31250) -} - -func (s *S) TestBucketForPercentileWithUniform(c *C) { - hs := &HistogramSpecification{ - Starts: EquallySizedBucketsFor(0, 100, 100), - BucketMaker: TallyingBucketBuilder, - } - - c.Assert(hs, Not(IsNil)) - - h := CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - for i := 0.0; i <= 99.0; i++ { - h.Add(i) - } - - for i := 0; i <= 99; i++ { - c.Check(h.bucketStarts[i], Equals, float64(i)) - } - - for i := 1; i <= 100; i++ { - c.Check(h.buckets[i-1].Observations(), Equals, 1) - } - - var bucket *Bucket = nil - var subindex int = 0 - - bucket, subindex = h.bucketForPercentile(0.01) - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) - - bucket, subindex = h.bucketForPercentile(1.0) - c.Assert(*bucket, Not(IsNil)) - c.Check(subindex, Equals, 0) - c.Check((*bucket).Observations(), Equals, 1) -} - -func (s *S) TestHistogramPercentileUniform(c *C) { - hs := &HistogramSpecification{ - Starts: EquallySizedBucketsFor(0, 100, 100), - BucketMaker: TallyingBucketBuilder, - } - - h := CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - for i := 0.0; i <= 99.0; i++ { - h.Add(i) - } - - c.Check(h.Percentile(0.01), Equals, 0.0) - c.Check(h.Percentile(0.49), Equals, 48.0) - c.Check(h.Percentile(0.50), Equals, 49.0) - c.Check(h.Percentile(0.51), Equals, 50.0) - c.Check(h.Percentile(1.0), Equals, 99.0) -} - -func (s *S) TestHistogramPercentileBinomialApproximation(c *C) { - hs := &HistogramSpecification{ - Starts: EquallySizedBucketsFor(0, 5, 6), - BucketMaker: TallyingBucketBuilder, - } - - h := CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - n := 5 - p := 0.5 - - for k := 0; k < 6; k++ { - limit := 1000000.0 * maths.BinomialPDF(k, n, p) - for j := 0.0; j < limit; j++ { - h.Add(float64(k)) - } - } - - c.Check(h.Percentile(0.0), Equals, 0.0) - c.Check(h.Percentile(0.03125), Equals, 0.0) - c.Check(h.Percentile(0.1875), Equals, 1.0) - c.Check(h.Percentile(0.5), Equals, 2.0) - c.Check(h.Percentile(0.8125), Equals, 3.0) - c.Check(h.Percentile(0.96875), Equals, 4.0) - c.Check(h.Percentile(1.0), Equals, 5.0) -} - -func (s *S) TestHistogramMarshallable(c *C) { - hs := &HistogramSpecification{ - Starts: EquallySizedBucketsFor(0, 5, 6), - BucketMaker: TallyingBucketBuilder, - ReportablePercentiles: []float64{0.03125, 0.1875, 0.5, 0.8125, 0.96875, 1.0}, - } - - h := CreateHistogram(hs) - - c.Assert(h, Not(IsNil)) - - n := 5 - p := 0.5 - - for k := 0; k < 6; k++ { - limit := 1000000.0 * maths.BinomialPDF(k, n, p) - for j := 0.0; j < limit; j++ { - h.Add(float64(k)) - } - } - - m := h.Marshallable() - - c.Assert(m, Not(IsNil)) - c.Check(m, HasLen, 2) - c.Check(m["type"], Equals, "histogram") - - var v map[string]interface{} = m["value"].(map[string]interface{}) - - c.Assert(v, Not(IsNil)) - - c.Check(v, HasLen, 6) - c.Check(v["0.031250"], Equals, "0.000000") - c.Check(v["0.187500"], Equals, "1.000000") - c.Check(v["0.500000"], Equals, "2.000000") - c.Check(v["0.812500"], Equals, "3.000000") - c.Check(v["0.968750"], Equals, "4.000000") - c.Check(v["1.000000"], Equals, "5.000000") -} - -func (s *S) TestHistogramAsMetric(c *C) { - hs := &HistogramSpecification{ - Starts: EquallySizedBucketsFor(0, 5, 6), - BucketMaker: TallyingBucketBuilder, - ReportablePercentiles: []float64{0.0, 0.03125, 0.1875, 0.5, 0.8125, 0.96875, 1.0}, - } - - h := CreateHistogram(hs) - - var metric Metric = h - - c.Assert(metric, Not(IsNil)) -} +// TODO(matt): Re-Add tests for this type. diff --git a/metrics/metric.go b/metrics/metric.go index ad7b26c..65a1b6f 100644 --- a/metrics/metric.go +++ b/metrics/metric.go @@ -1,23 +1,17 @@ -/* -Copyright (c) 2012, Matt T. Proud -All rights reserved. - -Use of this source code is governed by a BSD-style -license that can be found in the LICENSE file. -*/ +// Copyright (c) 2012, Matt T. Proud +// All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. package metrics -/* -A Metric is something that can be exposed via the registry framework. -*/ +// A Metric is something that can be exposed via the registry framework. type Metric interface { - /* - Produce a human-consumable representation of the metric. - */ + // Produce a JSON-consumable representation of the metric. + AsMarshallable() map[string]interface{} + // Reset the parent metrics and delete all child metrics. + ResetAll() + // Produce a human-consumable representation of the metric. String() string - /* - Produce a JSON-consumable representation of the metric. - */ - Marshallable() map[string]interface{} } diff --git a/metrics/tallying_bucket.go b/metrics/tallying_bucket.go index 195db9b..b023310 100644 --- a/metrics/tallying_bucket.go +++ b/metrics/tallying_bucket.go @@ -88,11 +88,11 @@ Upon insertion, an object is compared against collected extrema and noted as a new minimum or maximum if appropriate. */ type TallyingBucket struct { - observations int - smallestObserved float64 + estimator TallyingIndexEstimator largestObserved float64 mutex sync.RWMutex - estimator TallyingIndexEstimator + observations int + smallestObserved float64 } func (b *TallyingBucket) Add(value float64) { @@ -131,22 +131,31 @@ func (b *TallyingBucket) ValueForIndex(index int) float64 { return b.estimator(b.smallestObserved, b.largestObserved, index, b.observations) } +func (b *TallyingBucket) Reset() { + b.mutex.Lock() + defer b.mutex.Unlock() + + b.largestObserved = math.SmallestNonzeroFloat64 + b.observations = 0 + b.smallestObserved = math.MaxFloat64 +} + /* Produce a TallyingBucket with sane defaults. */ func DefaultTallyingBucket() TallyingBucket { return TallyingBucket{ - smallestObserved: math.MaxFloat64, - largestObserved: math.SmallestNonzeroFloat64, estimator: Minimum, + largestObserved: math.SmallestNonzeroFloat64, + smallestObserved: math.MaxFloat64, } } func CustomTallyingBucket(estimator TallyingIndexEstimator) TallyingBucket { return TallyingBucket{ - smallestObserved: math.MaxFloat64, - largestObserved: math.SmallestNonzeroFloat64, estimator: estimator, + largestObserved: math.SmallestNonzeroFloat64, + smallestObserved: math.MaxFloat64, } } diff --git a/metrics/timer.go b/metrics/timer.go index b3a7c23..cdd5ec0 100644 --- a/metrics/timer.go +++ b/metrics/timer.go @@ -30,19 +30,23 @@ N.B.(mtp): A major limitation hereof is that the StopWatch protocol cannot retain instrumentation if a panic percolates within the context that is being measured. */ -type StopWatch struct { - startTime time.Time +type StopWatch interface { + Stop() time.Duration +} + +type stopWatch struct { endTime time.Time onCompletion CompletionCallback + startTime time.Time } /* Return a new StopWatch that is ready for instrumentation. */ -func Start(onCompletion CompletionCallback) *StopWatch { - return &StopWatch{ - startTime: time.Now(), +func Start(onCompletion CompletionCallback) StopWatch { + return &stopWatch{ onCompletion: onCompletion, + startTime: time.Now(), } } @@ -50,7 +54,7 @@ func Start(onCompletion CompletionCallback) *StopWatch { Stop the StopWatch returning the elapsed duration of its lifetime while firing an optional CompletionCallback in the background. */ -func (s *StopWatch) Stop() time.Duration { +func (s *stopWatch) Stop() time.Duration { s.endTime = time.Now() duration := s.endTime.Sub(s.startTime) diff --git a/metrics/timer_test.go b/metrics/timer_test.go index e78c626..62ae099 100644 --- a/metrics/timer_test.go +++ b/metrics/timer_test.go @@ -14,7 +14,10 @@ import ( ) func (s *S) TestTimerStart(c *C) { - stopWatch := Start(nil) + stopWatch, ok := Start(nil).(*stopWatch) + if !ok { + c.Check(ok, Equals, true) + } c.Assert(stopWatch, Not(IsNil)) c.Assert(stopWatch.startTime, Not(IsNil)) diff --git a/registry.go b/registry.go index 681e3a1..cf6f3fe 100644 --- a/registry.go +++ b/registry.go @@ -9,21 +9,39 @@ the LICENSE file. package registry import ( + "compress/gzip" "encoding/base64" "encoding/json" + "flag" + "fmt" "github.com/matttproud/golang_instrumentation/metrics" + "github.com/matttproud/golang_instrumentation/utility" + "io" "log" "net/http" + "sort" "strings" "sync" "time" ) const ( - authorization = "Authorization" - contentType = "Content-Type" - jsonContentType = "application/json" - jsonSuffix = ".json" + acceptEncodingHeader = "Accept-Encoding" + authorization = "Authorization" + authorizationHeader = "WWW-Authenticate" + authorizationHeaderValue = "Basic" + contentEncodingHeader = "Content-Encoding" + contentTypeHeader = "Content-Type" + gzipAcceptEncodingValue = "gzip" + gzipContentEncodingValue = "gzip" + jsonContentType = "application/json" + jsonSuffix = ".json" +) + +var ( + abortOnMisuse bool + debugRegistration bool + useAggressiveSanityChecks bool ) /* @@ -31,12 +49,18 @@ This callback accumulates the microsecond duration of the reporting framework's overhead such that it can be reported. */ var requestLatencyAccumulator metrics.CompletionCallback = func(duration time.Duration) { - microseconds := float64(duration / time.Millisecond) + microseconds := float64(duration / time.Microsecond) - requestLatencyLogarithmicAccumulating.Add(microseconds) - requestLatencyEqualAccumulating.Add(microseconds) - requestLatencyLogarithmicTallying.Add(microseconds) - requestLatencyEqualTallying.Add(microseconds) + requestLatency.Add(nil, microseconds) +} + +// container represents a top-level registered metric that encompasses its +// static metadata. +type container struct { + baseLabels map[string]string + docstring string + metric metrics.Metric + name string } /* @@ -46,8 +70,8 @@ In most situations, using DefaultRegistry is sufficient versus creating one's own. */ type Registry struct { - mutex sync.RWMutex - NameToMetric map[string]metrics.Metric + mutex sync.RWMutex + signatureContainers map[string]container } /* @@ -56,7 +80,7 @@ cases. */ func NewRegistry() *Registry { return &Registry{ - NameToMetric: make(map[string]metrics.Metric), + signatureContainers: make(map[string]container), } } @@ -69,25 +93,96 @@ var DefaultRegistry = NewRegistry() /* Associate a Metric with the DefaultRegistry. */ -func Register(name, unusedDocstring string, unusedBaseLabels map[string]string, metric metrics.Metric) { - DefaultRegistry.Register(name, unusedDocstring, unusedBaseLabels, metric) +func Register(name, docstring string, baseLabels map[string]string, metric metrics.Metric) error { + return DefaultRegistry.Register(name, docstring, baseLabels, metric) +} + +// isValidCandidate returns true if the candidate is acceptable for use. In the +// event of any apparent incorrect use it will report the problem, invalidate +// the candidate, or outright abort. +func (r *Registry) isValidCandidate(name string, baseLabels map[string]string) (signature string, err error) { + if len(name) == 0 { + err = fmt.Errorf("unnamed metric named with baseLabels %s is invalid", baseLabels) + + if abortOnMisuse { + panic(err) + } else if debugRegistration { + log.Println(err) + } + } + + if _, contains := baseLabels[nameLabel]; contains { + err = fmt.Errorf("metric named %s with baseLabels %s contains reserved label name %s in baseLabels", name, baseLabels, nameLabel) + + if abortOnMisuse { + panic(err) + } else if debugRegistration { + log.Println(err) + } + + return + } + + baseLabels[nameLabel] = name + signature = utility.LabelsToSignature(baseLabels) + + if _, contains := r.signatureContainers[signature]; contains { + err = fmt.Errorf("metric named %s with baseLabels %s is already registered", name, baseLabels) + if abortOnMisuse { + panic(err) + } else if debugRegistration { + log.Println(err) + } + + return + } + + if useAggressiveSanityChecks { + for _, container := range r.signatureContainers { + if container.name == name { + err = fmt.Errorf("metric named %s with baseLabels %s is already registered as %s and risks causing confusion", name, baseLabels, container.baseLabels) + if abortOnMisuse { + panic(err) + } else if debugRegistration { + log.Println(err) + } + + return + } + } + } + + return } /* Register a metric with a given name. Name should be globally unique. */ -func (r *Registry) Register(name, unusedDocstring string, unusedBaseLabels map[string]string, metric metrics.Metric) { +func (r *Registry) Register(name, docstring string, baseLabels map[string]string, metric metrics.Metric) (err error) { r.mutex.Lock() defer r.mutex.Unlock() - if _, present := r.NameToMetric[name]; !present { - r.NameToMetric[name] = metric - log.Printf("Registered %s.\n", name) - } else { - log.Printf("Attempted to register duplicate %s metric.\n", name) + if baseLabels == nil { + baseLabels = map[string]string{} } + + signature, err := r.isValidCandidate(name, baseLabels) + if err != nil { + return + } + + r.signatureContainers[signature] = container{ + baseLabels: baseLabels, + docstring: docstring, + metric: metric, + name: name, + } + + return } +// YieldBasicAuthExporter creates a http.HandlerFunc that is protected by HTTP's +// basic authentication. func (register *Registry) YieldBasicAuthExporter(username, password string) http.HandlerFunc { exporter := register.YieldExporter() @@ -108,12 +203,80 @@ func (register *Registry) YieldBasicAuthExporter(username, password string) http if authenticated { exporter.ServeHTTP(w, r) } else { - w.Header().Add("WWW-Authenticate", "Basic") + w.Header().Add(authorizationHeader, authorizationHeaderValue) http.Error(w, "access forbidden", 401) } }) } +func (registry *Registry) dumpToWriter(writer io.Writer) (err error) { + defer func() { + if err != nil { + dumpErrorCount.Increment(nil) + } + }() + + numberOfMetrics := len(registry.signatureContainers) + keys := make([]string, 0, numberOfMetrics) + for key := range registry.signatureContainers { + keys = append(keys, key) + } + + sort.Strings(keys) + + _, err = writer.Write([]byte("[")) + if err != nil { + return + } + + index := 0 + + for _, key := range keys { + container := registry.signatureContainers[key] + intermediate := map[string]interface{}{ + baseLabelsKey: container.baseLabels, + docstringKey: container.docstring, + metricKey: container.metric.AsMarshallable(), + } + marshaled, err := json.Marshal(intermediate) + if err != nil { + marshalErrorCount.Increment(nil) + index++ + continue + } + + if index > 0 && index < numberOfMetrics { + _, err = writer.Write([]byte(",")) + if err != nil { + return err + } + } + + _, err = writer.Write(marshaled) + if err != nil { + return err + } + index++ + } + + _, err = writer.Write([]byte("]")) + + return +} + +// decorateWriter annotates the response writer to handle any other behaviors +// that might be beneficial to the client---e.g., GZIP encoding. +func decorateWriter(request *http.Request, writer http.ResponseWriter) io.Writer { + if !strings.Contains(request.Header.Get(acceptEncodingHeader), gzipAcceptEncodingValue) { + return writer + } + + writer.Header().Set(contentEncodingHeader, gzipContentEncodingValue) + gziper := gzip.NewWriter(writer) + + return gziper +} + /* Create a http.HandlerFunc that is tied to r Registry such that requests against it generate a representation of the housed metrics. @@ -121,19 +284,23 @@ against it generate a representation of the housed metrics. func (registry *Registry) YieldExporter() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var instrumentable metrics.InstrumentableCall = func() { - requestCount.Increment() + requestCount.Increment(nil) url := r.URL if strings.HasSuffix(url.Path, jsonSuffix) { - w.Header().Set(contentType, jsonContentType) - composite := make(map[string]interface{}, len(registry.NameToMetric)) - for name, metric := range registry.NameToMetric { - composite[name] = metric.Marshallable() + header := w.Header() + header.Set(ProtocolVersionHeader, APIVersion) + header.Set(contentTypeHeader, jsonContentType) + + writer := decorateWriter(r, w) + + // TODO(matt): Migrate to ioutil.NopCloser. + if closer, ok := writer.(io.Closer); ok { + defer closer.Close() } - data, _ := json.Marshal(composite) + registry.dumpToWriter(writer) - w.Write(data) } else { w.WriteHeader(http.StatusNotFound) } @@ -142,3 +309,9 @@ func (registry *Registry) YieldExporter() http.HandlerFunc { metrics.InstrumentCall(instrumentable, requestLatencyAccumulator) } } + +func init() { + flag.BoolVar(&abortOnMisuse, FlagNamespace+"abortonmisuse", false, "abort if a semantic misuse is encountered (bool).") + flag.BoolVar(&debugRegistration, FlagNamespace+"debugregistration", false, "display information about the metric registration process (bool).") + flag.BoolVar(&useAggressiveSanityChecks, FlagNamespace+"useaggressivesanitychecks", false, "perform expensive validation of metrics (bool).") +} diff --git a/registry_test.go b/registry_test.go new file mode 100644 index 0000000..35de611 --- /dev/null +++ b/registry_test.go @@ -0,0 +1,331 @@ +// Copyright (c) 2013, Matt T. Proud +// All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package registry + +import ( + "bytes" + "fmt" + "github.com/matttproud/golang_instrumentation/metrics" + "github.com/matttproud/golang_instrumentation/utility/test" + "io" + "net/http" + "testing" +) + +func testRegister(t test.Tester) { + var oldState = struct { + abortOnMisuse bool + debugRegistration bool + useAggressiveSanityChecks bool + }{ + abortOnMisuse: abortOnMisuse, + debugRegistration: debugRegistration, + useAggressiveSanityChecks: useAggressiveSanityChecks, + } + defer func() { + abortOnMisuse = oldState.abortOnMisuse + debugRegistration = oldState.debugRegistration + useAggressiveSanityChecks = oldState.useAggressiveSanityChecks + }() + + type input struct { + name string + baseLabels map[string]string + } + + var scenarios = []struct { + inputs []input + outputs []bool + }{ + {}, + { + inputs: []input{ + { + name: "my_name_without_labels", + }, + }, + outputs: []bool{ + true, + }, + }, + { + inputs: []input{ + { + name: "my_name_without_labels", + }, + { + name: "another_name_without_labels", + }, + }, + outputs: []bool{ + true, + true, + }, + }, + { + inputs: []input{ + { + name: "", + }, + }, + outputs: []bool{ + false, + }, + }, + { + inputs: []input{ + { + name: "valid_name", + baseLabels: map[string]string{"name": "illegal_duplicate_name"}, + }, + }, + outputs: []bool{ + false, + }, + }, + { + inputs: []input{ + { + name: "duplicate_names", + }, + { + name: "duplicate_names", + }, + }, + outputs: []bool{ + true, + false, + }, + }, + { + inputs: []input{ + { + name: "duplicate_names_with_identical_labels", + baseLabels: map[string]string{"label": "value"}, + }, + { + name: "duplicate_names_with_identical_labels", + baseLabels: map[string]string{"label": "value"}, + }, + }, + outputs: []bool{ + true, + false, + }, + }, + { + inputs: []input{ + { + name: "duplicate_names_with_dissimilar_labels", + baseLabels: map[string]string{"label": "foo"}, + }, + { + name: "duplicate_names_with_dissimilar_labels", + baseLabels: map[string]string{"label": "bar"}, + }, + }, + outputs: []bool{ + true, + false, + }, + }, + } + + for i, scenario := range scenarios { + if len(scenario.inputs) != len(scenario.outputs) { + t.Fatalf("%d. len(scenario.inputs) != len(scenario.outputs)") + } + + abortOnMisuse = false + debugRegistration = false + useAggressiveSanityChecks = true + + registry := NewRegistry() + + for j, input := range scenario.inputs { + actual := registry.Register(input.name, "", input.baseLabels, nil) + if scenario.outputs[j] != (actual == nil) { + t.Errorf("%d.%d. expected %s, got %s", i, j, scenario.outputs[j], actual) + } + } + } +} + +func TestRegister(t *testing.T) { + testRegister(t) +} + +func BenchmarkRegister(b *testing.B) { + for i := 0; i < b.N; i++ { + testRegister(b) + } +} + +type fakeResponseWriter struct { + header http.Header + body bytes.Buffer +} + +func (r *fakeResponseWriter) Header() http.Header { + return r.header +} + +func (r *fakeResponseWriter) Write(d []byte) (l int, err error) { + return r.body.Write(d) +} + +func (r *fakeResponseWriter) WriteHeader(c int) { +} + +func testDecorateWriter(t test.Tester) { + type input struct { + headers map[string]string + body []byte + } + + type output struct { + headers map[string]string + body []byte + } + + var scenarios = []struct { + in input + out output + }{ + {}, + { + in: input{ + headers: map[string]string{ + "Accept-Encoding": "gzip,deflate,sdch", + }, + body: []byte("Hi, mom!"), + }, + out: output{ + headers: map[string]string{ + "Content-Encoding": "gzip", + }, + body: []byte("\x1f\x8b\b\x00\x00\tn\x88\x00\xff\xf2\xc8\xd4Q\xc8\xcd\xcfU\x04\x04\x00\x00\xff\xff9C&&\b\x00\x00\x00"), + }, + }, + { + in: input{ + headers: map[string]string{ + "Accept-Encoding": "foo", + }, + body: []byte("Hi, mom!"), + }, + out: output{ + headers: map[string]string{}, + body: []byte("Hi, mom!"), + }, + }, + } + + for i, scenario := range scenarios { + request, _ := http.NewRequest("GET", "/", nil) + + for key, value := range scenario.in.headers { + request.Header.Add(key, value) + } + + baseWriter := &fakeResponseWriter{ + header: make(http.Header), + } + + writer := decorateWriter(request, baseWriter) + + for key, value := range scenario.out.headers { + if baseWriter.Header().Get(key) != value { + t.Errorf("%d. expected %s for header %s, got %s", i, value, key, baseWriter.Header().Get(key)) + } + } + + writer.Write(scenario.in.body) + + if closer, ok := writer.(io.Closer); ok { + closer.Close() + } + + if !bytes.Equal(scenario.out.body, baseWriter.body.Bytes()) { + t.Errorf("%d. expected %s for body, got %s", i, scenario.out.body, baseWriter.body.Bytes()) + } + } +} + +func TestDecorateWriter(t *testing.T) { + testDecorateWriter(t) +} + +func BenchmarkDecorateWriter(b *testing.B) { + for i := 0; i < b.N; i++ { + testDecorateWriter(b) + } +} + +func testDumpToWriter(t test.Tester) { + type input struct { + metrics map[string]metrics.Metric + } + + var scenarios = []struct { + in input + out []byte + }{ + { + out: []byte("[]"), + }, + { + in: input{ + metrics: map[string]metrics.Metric{ + "foo": metrics.NewCounter(), + }, + }, + out: []byte("[{\"baseLabels\":{\"label_foo\":\"foo\",\"name\":\"foo\"},\"docstring\":\"metric foo\",\"metric\":{\"type\":\"counter\",\"value\":[]}}]"), + }, + { + in: input{ + metrics: map[string]metrics.Metric{ + "foo": metrics.NewCounter(), + "bar": metrics.NewCounter(), + }, + }, + out: []byte("[{\"baseLabels\":{\"label_bar\":\"bar\",\"name\":\"bar\"},\"docstring\":\"metric bar\",\"metric\":{\"type\":\"counter\",\"value\":[]}},{\"baseLabels\":{\"label_foo\":\"foo\",\"name\":\"foo\"},\"docstring\":\"metric foo\",\"metric\":{\"type\":\"counter\",\"value\":[]}}]"), + }, + } + + for i, scenario := range scenarios { + registry := NewRegistry() + + for name, metric := range scenario.in.metrics { + err := registry.Register(name, fmt.Sprintf("metric %s", name), map[string]string{fmt.Sprintf("label_%s", name): name}, metric) + if err != nil { + t.Errorf("%d. encountered error while registering metric %s", i, err) + } + } + + actual := &bytes.Buffer{} + + err := registry.dumpToWriter(actual) + if err != nil { + t.Errorf("%d. encountered error while dumping %s", i, err) + } + + if !bytes.Equal(scenario.out, actual.Bytes()) { + t.Errorf("%d. expected %q for dumping, got %q", i, scenario.out, actual.Bytes()) + } + } +} + +func TestDumpToWriter(t *testing.T) { + testDumpToWriter(t) +} + +func BenchmarkDumpToWriter(b *testing.B) { + for i := 0; i < b.N; i++ { + testDumpToWriter(b) + } +} diff --git a/telemetry.go b/telemetry.go index 82c29f1..c82e314 100644 --- a/telemetry.go +++ b/telemetry.go @@ -20,43 +20,25 @@ exposed if the DefaultRegistry's exporter is hooked into the HTTP request handler. */ var ( - // TODO(matt): Refresh these names to support namespacing. + marshalErrorCount = metrics.NewCounter() + dumpErrorCount = metrics.NewCounter() - requestCount *metrics.CounterMetric = &metrics.CounterMetric{} - requestLatencyLogarithmicBuckets []float64 = metrics.LogarithmicSizedBucketsFor(0, 1000) - requestLatencyEqualBuckets []float64 = metrics.EquallySizedBucketsFor(0, 1000, 10) - requestLatencyLogarithmicAccumulating *metrics.Histogram = metrics.CreateHistogram(&metrics.HistogramSpecification{ - Starts: requestLatencyLogarithmicBuckets, - BucketMaker: metrics.AccumulatingBucketBuilder(metrics.EvictAndReplaceWith(50, maths.Average), 1000), - ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.9, 0.99}, - }) - requestLatencyEqualAccumulating *metrics.Histogram = metrics.CreateHistogram(&metrics.HistogramSpecification{ - Starts: requestLatencyEqualBuckets, - BucketMaker: metrics.AccumulatingBucketBuilder(metrics.EvictAndReplaceWith(50, maths.Average), 1000), - ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.9, 0.99}, - }) - requestLatencyLogarithmicTallying *metrics.Histogram = metrics.CreateHistogram(&metrics.HistogramSpecification{ - Starts: requestLatencyLogarithmicBuckets, - BucketMaker: metrics.TallyingBucketBuilder, - ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.9, 0.99}, - }) - requestLatencyEqualTallying *metrics.Histogram = metrics.CreateHistogram(&metrics.HistogramSpecification{ - Starts: requestLatencyEqualBuckets, - BucketMaker: metrics.TallyingBucketBuilder, + requestCount = metrics.NewCounter() + requestLatencyBuckets = metrics.LogarithmicSizedBucketsFor(0, 1000) + requestLatency = metrics.NewHistogram(&metrics.HistogramSpecification{ + Starts: requestLatencyBuckets, + BucketBuilder: metrics.AccumulatingBucketBuilder(metrics.EvictAndReplaceWith(50, maths.Average), 1000), ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.9, 0.99}, }) - startTime *metrics.GaugeMetric = &metrics.GaugeMetric{} + startTime = metrics.NewGauge() ) func init() { - startTime.Set(float64(time.Now().Unix())) + startTime.Set(nil, float64(time.Now().Unix())) - DefaultRegistry.Register("requests_metrics_total", "A counter of the total requests made against the telemetry system.", NilLabels, requestCount) - DefaultRegistry.Register("requests_metrics_latency_logarithmic_accumulating_microseconds", "A histogram of the response latency for requests made against the telemetry system.", NilLabels, requestLatencyLogarithmicAccumulating) - DefaultRegistry.Register("requests_metrics_latency_equal_accumulating_microseconds", "A histogram of the response latency for requests made against the telemetry system.", NilLabels, requestLatencyEqualAccumulating) - DefaultRegistry.Register("requests_metrics_latency_logarithmic_tallying_microseconds", "A histogram of the response latency for requests made against the telemetry system.", NilLabels, requestLatencyLogarithmicTallying) - DefaultRegistry.Register("request_metrics_latency_equal_tallying_microseconds", "A histogram of the response latency for requests made against the telemetry system.", NilLabels, requestLatencyEqualTallying) + DefaultRegistry.Register("telemetry_requests_metrics_total", "A counter of the total requests made against the telemetry system.", NilLabels, requestCount) + DefaultRegistry.Register("telemetry_requests_metrics_latency_microseconds", "A histogram of the response latency for requests made against the telemetry system.", NilLabels, requestLatency) DefaultRegistry.Register("instance_start_time_seconds", "The time at which the current instance started (UTC).", NilLabels, startTime) } diff --git a/utility/documentation.go b/utility/documentation.go index 2143a4b..a4bdfdf 100644 --- a/utility/documentation.go +++ b/utility/documentation.go @@ -10,11 +10,6 @@ license that can be found in the LICENSE file. The utility package provides general purpose helpers to assist with this library. -optional.go provides a mechanism for safely getting a set value or falling -back to defaults a la a Haskell and Scala Maybe or Guava Optional. - -optional_test.go provides a test complement for the optional.go module. - priority_queue.go provides a simple priority queue. priority_queue_test.go provides a test complement for the priority_queue.go diff --git a/utility/optional.go b/utility/optional.go deleted file mode 100644 index 383b097..0000000 --- a/utility/optional.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright (c) 2012, Matt T. Proud -All rights reserved. - -Use of this source code is governed by a BSD-style -license that can be found in the LICENSE file. -*/ - -package utility - -type Optional struct { - value interface{} -} - -func EmptyOptional() *Optional { - emission := &Optional{value: nil} - - return emission -} - -func Of(value interface{}) *Optional { - emission := &Optional{value: value} - - return emission -} - -func (o *Optional) IsSet() bool { - return o.value != nil -} - -func (o *Optional) Get() interface{} { - if o.value == nil { - panic("Expected a value to be set.") - } - - return o.value -} - -func (o *Optional) Or(a interface{}) interface{} { - if o.IsSet() { - return o.Get() - } - return a -} diff --git a/utility/optional_test.go b/utility/optional_test.go deleted file mode 100644 index ef3772e..0000000 --- a/utility/optional_test.go +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright (c) 2012, Matt T. Proud -All rights reserved. - -Use of this source code is governed by a BSD-style -license that can be found in the LICENSE file. -*/ - -package utility - -import ( - . "github.com/matttproud/gocheck" -) - -func (s *S) TestEmptyOptional(c *C) { - var o *Optional = EmptyOptional() - - c.Assert(o, Not(IsNil)) - c.Check(o.IsSet(), Equals, false) - c.Assert("default", Equals, o.Or("default")) -} - -func (s *S) TestOf(c *C) { - var o *Optional = Of(1) - - c.Assert(o, Not(IsNil)) - c.Check(o.IsSet(), Equals, true) - c.Check(o.Get(), Equals, 1) - c.Check(o.Or(2), Equals, 1) -} diff --git a/utility/priority_queue.go b/utility/priority_queue.go index e277e11..9d2780c 100644 --- a/utility/priority_queue.go +++ b/utility/priority_queue.go @@ -9,8 +9,8 @@ license that can be found in the LICENSE file. package utility type Item struct { - Value interface{} Priority int64 + Value interface{} index int } diff --git a/utility/signature.go b/utility/signature.go new file mode 100644 index 0000000..610af73 --- /dev/null +++ b/utility/signature.go @@ -0,0 +1,41 @@ +// Copyright (c) 2013, Matt T. Proud +// All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package utility + +import ( + "bytes" + "sort" +) + +const ( + delimiter = "|" +) + +// LabelsToSignature provides a way of building a unique signature +// (i.e., fingerprint) for a given label set sequence. +func LabelsToSignature(labels map[string]string) string { + // TODO(matt): This is a wart, and we'll want to validate that collisions + // do not occur in less-than-diligent environments. + cardinality := len(labels) + keys := make([]string, 0, cardinality) + + for label := range labels { + keys = append(keys, label) + } + + sort.Strings(keys) + + buffer := bytes.Buffer{} + + for _, label := range keys { + buffer.WriteString(label) + buffer.WriteString(delimiter) + buffer.WriteString(labels[label]) + } + + return buffer.String() +} diff --git a/utility/signature_test.go b/utility/signature_test.go new file mode 100644 index 0000000..42e059d --- /dev/null +++ b/utility/signature_test.go @@ -0,0 +1,43 @@ +// Copyright (c) 2013, Matt T. Proud +// All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package utility + +import ( + "github.com/matttproud/golang_instrumentation/utility/test" + "testing" +) + +func testLabelsToSignature(t test.Tester) { + var scenarios = []struct { + in map[string]string + out string + }{ + { + in: map[string]string{}, + out: "", + }, + {}, + } + + for i, scenario := range scenarios { + actual := LabelsToSignature(scenario.in) + + if actual != scenario.out { + t.Errorf("%d. expected %s, got %s", i, scenario.out, actual) + } + } +} + +func TestLabelToSignature(t *testing.T) { + testLabelsToSignature(t) +} + +func BenchmarkLabelToSignature(b *testing.B) { + for i := 0; i < b.N; i++ { + testLabelsToSignature(b) + } +} diff --git a/utility/test/interface.go b/utility/test/interface.go new file mode 100644 index 0000000..de30938 --- /dev/null +++ b/utility/test/interface.go @@ -0,0 +1,14 @@ +// Copyright (c) 2013, Matt T. Proud +// All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package test + +type Tester interface { + Error(args ...interface{}) + Errorf(format string, args ...interface{}) + Fatal(args ...interface{}) + Fatalf(format string, args ...interface{}) +}