From 0f93b588cd8e60836ef97f0045f16b55b8e096c7 Mon Sep 17 00:00:00 2001 From: "Matt T. Proud" Date: Sat, 19 Jan 2013 14:48:30 +0100 Subject: [PATCH] Update client to reflect new API needs. 1. The output format is now versioned per the Semantic Versioning scheme. Mainline Prometheus will be adapted to use differing consumption methodologies as the generators' formats evolve to support legacy clients. 2. The telemetry outputter now supports GZIP output encoding. In sample runs, this cuts the output size in half. 3. Basic sanity tests are added for registration with varying levels of pedanticness. 4. We have support for base labels in the registration and emission phases. 5. We have label support for individual metric mutation operations. 6. A number of simplications have been made. --- README.md | 15 + constants.go | 28 +- examples/random/main.go | 41 +- metrics/accumulating_bucket.go | 25 +- metrics/bucket.go | 10 +- metrics/constants.go | 11 +- metrics/counter.go | 138 +++-- metrics/counter_test.go | 283 +++++++--- metrics/eviction_test.go | 12 +- metrics/gauge.go | 89 ++- metrics/gauge_test.go | 152 +++-- metrics/histogram.go | 152 +++-- metrics/histogram_test.go | 977 +-------------------------------- metrics/metric.go | 28 +- metrics/tallying_bucket.go | 23 +- metrics/timer.go | 16 +- metrics/timer_test.go | 5 +- registry.go | 229 +++++++- registry_test.go | 331 +++++++++++ telemetry.go | 40 +- utility/documentation.go | 5 - utility/optional.go | 44 -- utility/optional_test.go | 30 - utility/priority_queue.go | 2 +- utility/signature.go | 41 ++ utility/signature_test.go | 43 ++ utility/test/interface.go | 14 + 27 files changed, 1371 insertions(+), 1413 deletions(-) create mode 100644 registry_test.go delete mode 100644 utility/optional.go delete mode 100644 utility/optional_test.go create mode 100644 utility/signature.go create mode 100644 utility/signature_test.go create mode 100644 utility/test/interface.go 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{}) +}