Merge pull request #13 from matttproud/feature/labels/base-exposed

Update client to reflect new API needs.
This commit is contained in:
Matt T. Proud 2013-01-23 04:09:17 -08:00
commit 5a29c27491
27 changed files with 1371 additions and 1413 deletions

View File

@ -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 # Overview
This [Go](http://golang.org) package is an extraction of a piece of 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 instrumentation code I whipped-up for a personal project that a friend of mine

View File

@ -1,14 +1,26 @@
/* // Copyright (c) 2013, Matt T. Proud
Copyright (c) 2013, Matt T. Proud // All rights reserved.
All rights reserved. //
// Use of this source code is governed by a BSD-style license that can be found in
Use of this source code is governed by a BSD-style license that can be found in // the LICENSE file.
the LICENSE file.
*/
package registry package registry
var ( var (
// NilLabels is a nil set of labels merely for end-user convenience. // 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"
) )

View File

@ -37,46 +37,29 @@ func init() {
func main() { func main() {
flag.Parse() flag.Parse()
foo_rpc_latency := metrics.CreateHistogram(&metrics.HistogramSpecification{ rpc_latency := metrics.NewHistogram(&metrics.HistogramSpecification{
Starts: metrics.EquallySizedBucketsFor(0, 200, 4), 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}, ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.90, 0.99},
}) })
foo_rpc_calls := &metrics.CounterMetric{}
bar_rpc_latency := metrics.CreateHistogram(&metrics.HistogramSpecification{ rpc_calls := metrics.NewCounter()
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{}
metrics := registry.NewRegistry() metrics := registry.NewRegistry()
nilBaseLabels := make(map[string]string) metrics.Register("rpc_latency_microseconds", "RPC latency.", registry.NilLabels, rpc_latency)
metrics.Register("rpc_calls_total", "RPC calls.", registry.NilLabels, rpc_calls)
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)
go func() { go func() {
for { for {
foo_rpc_latency.Add(rand.Float64() * 200) rpc_latency.Add(map[string]string{"service": "foo"}, rand.Float64()*200)
foo_rpc_calls.Increment() rpc_calls.Increment(map[string]string{"service": "foo"})
bar_rpc_latency.Add((rand.NormFloat64() * 10.0) + 100.0) rpc_latency.Add(map[string]string{"service": "bar"}, (rand.NormFloat64()*10.0)+100.0)
bar_rpc_calls.Increment() rpc_calls.Increment(map[string]string{"service": "bar"})
zed_rpc_latency.Add(rand.ExpFloat64()) rpc_latency.Add(map[string]string{"service": "zed"}, rand.ExpFloat64())
zed_rpc_calls.Increment() rpc_calls.Increment(map[string]string{"service": "zed"})
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
} }

View File

@ -20,11 +20,11 @@ import (
) )
type AccumulatingBucket struct { type AccumulatingBucket struct {
observations int
elements utility.PriorityQueue elements utility.PriorityQueue
evictionPolicy EvictionPolicy
maximumSize int maximumSize int
mutex sync.RWMutex mutex sync.RWMutex
evictionPolicy EvictionPolicy observations int
} }
/* /*
@ -35,9 +35,9 @@ behavior set.
func AccumulatingBucketBuilder(evictionPolicy EvictionPolicy, maximumSize int) BucketBuilder { func AccumulatingBucketBuilder(evictionPolicy EvictionPolicy, maximumSize int) BucketBuilder {
return func() Bucket { return func() Bucket {
return &AccumulatingBucket{ return &AccumulatingBucket{
maximumSize: maximumSize,
evictionPolicy: evictionPolicy,
elements: make(utility.PriorityQueue, 0, maximumSize), elements: make(utility.PriorityQueue, 0, maximumSize),
evictionPolicy: evictionPolicy,
maximumSize: maximumSize,
} }
} }
} }
@ -54,8 +54,8 @@ func (b *AccumulatingBucket) Add(value float64) {
size := len(b.elements) size := len(b.elements)
v := utility.Item{ v := utility.Item{
Value: value,
Priority: -1 * time.Now().UnixNano(), Priority: -1 * time.Now().UnixNano(),
Value: value,
} }
if size == b.maximumSize { if size == b.maximumSize {
@ -69,7 +69,7 @@ func (b *AccumulatingBucket) String() string {
b.mutex.RLock() b.mutex.RLock()
defer b.mutex.RUnlock() 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) 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, "}") fmt.Fprintf(buffer, "}")
return string(buffer.Bytes()) return buffer.String()
} }
func (b *AccumulatingBucket) ValueForIndex(index int) float64 { func (b *AccumulatingBucket) ValueForIndex(index int) float64 {
@ -116,3 +116,14 @@ func (b *AccumulatingBucket) Observations() int {
return b.observations 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
}

View File

@ -29,14 +29,16 @@ type Bucket interface {
Add a value to the bucket. Add a value to the bucket.
*/ */
Add(value float64) Add(value float64)
/*
Provide a humanized representation hereof.
*/
String() string
/* /*
Provide a count of observations throughout the bucket's lifetime. Provide a count of observations throughout the bucket's lifetime.
*/ */
Observations() int 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 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 thereof for the given index. The consumer of the bucket's data makes

View File

@ -12,12 +12,13 @@ constants.go provides package-level constants for metrics.
package metrics package metrics
const ( const (
valueKey = "value"
gaugeTypeValue = "gauge"
counterTypeValue = "counter" counterTypeValue = "counter"
typeKey = "type" floatBitCount = 64
histogramTypeValue = "histogram"
floatFormat = 'f' floatFormat = 'f'
floatPrecision = 6 floatPrecision = 6
floatBitCount = 64 gaugeTypeValue = "gauge"
histogramTypeValue = "histogram"
typeKey = "type"
valueKey = "value"
labelsKey = "labels"
) )

View File

@ -10,77 +10,145 @@ package metrics
import ( import (
"fmt" "fmt"
"github.com/matttproud/golang_instrumentation/utility"
"sync" "sync"
) )
type CounterMetric struct { // TODO(matt): Refactor to de-duplicate behaviors.
value float64
mutex sync.RWMutex 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() metric.mutex.Lock()
defer metric.mutex.Unlock() 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() { func (metric *counter) ResetAll() {
metric.Set(0) 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 { func (metric *counter) String() string {
formatString := "[CounterMetric; value=%f]" formatString := "[Counter %s]"
metric.mutex.RLock() metric.mutex.RLock()
defer metric.mutex.RUnlock() 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() metric.mutex.Lock()
defer metric.mutex.Unlock() 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 { func (metric *counter) Increment(labels map[string]string) float64 {
return metric.IncrementBy(1) 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() metric.mutex.Lock()
defer metric.mutex.Unlock() 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 { func (metric *counter) Decrement(labels map[string]string) float64 {
return metric.DecrementBy(1) return metric.DecrementBy(labels, 1)
} }
func (metric *CounterMetric) Get() float64 { func (metric *counter) AsMarshallable() map[string]interface{} {
metric.mutex.RLock() metric.mutex.RLock()
defer metric.mutex.RUnlock() defer metric.mutex.RUnlock()
return metric.value values := make([]map[string]interface{}, 0, len(metric.values))
} for _, value := range metric.values {
values = append(values, map[string]interface{}{
func (metric *CounterMetric) Marshallable() map[string]interface{} { labelsKey: value.labels,
metric.mutex.RLock() valueKey: value.value,
defer metric.mutex.RUnlock() })
}
v := make(map[string]interface{}, 2)
return map[string]interface{}{
v[valueKey] = metric.value valueKey: values,
v[typeKey] = counterTypeValue typeKey: counterTypeValue,
}
return v
} }

View File

@ -9,90 +9,215 @@ license that can be found in the LICENSE file.
package metrics package metrics
import ( import (
. "github.com/matttproud/gocheck" "encoding/json"
"github.com/matttproud/golang_instrumentation/utility/test"
"testing"
) )
func (s *S) TestCounterCreate(c *C) { func testCounter(t test.Tester) {
m := CounterMetric{value: 1.0} 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) { func TestCounter(t *testing.T) {
m := CounterMetric{value: 42.23} testCounter(t)
c.Check(m.Get(), Equals, 42.23)
} }
func (s *S) TestCounterSet(c *C) { func BenchmarkCounter(b *testing.B) {
m := CounterMetric{value: 42.23} for i := 0; i < b.N; i++ {
m.Set(40.4) testCounter(b)
}
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))
} }

View File

@ -22,8 +22,8 @@ func (s *S) TestEvictOldest(c *C) {
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
var item utility.Item = utility.Item{ var item utility.Item = utility.Item{
Value: float64(i),
Priority: int64(i), Priority: int64(i),
Value: float64(i),
} }
heap.Push(&q, &item) heap.Push(&q, &item)
@ -49,8 +49,8 @@ func (s *S) TestEvictAndReplaceWithAverage(c *C) {
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
var item utility.Item = utility.Item{ var item utility.Item = utility.Item{
Value: float64(i),
Priority: int64(i), Priority: int64(i),
Value: float64(i),
} }
heap.Push(&q, &item) heap.Push(&q, &item)
@ -77,8 +77,8 @@ func (s *S) TestEvictAndReplaceWithMedian(c *C) {
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
var item utility.Item = utility.Item{ var item utility.Item = utility.Item{
Value: float64(i),
Priority: int64(i), Priority: int64(i),
Value: float64(i),
} }
heap.Push(&q, &item) heap.Push(&q, &item)
@ -105,8 +105,8 @@ func (s *S) TestEvictAndReplaceWithFirstMode(c *C) {
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
heap.Push(&q, &utility.Item{ heap.Push(&q, &utility.Item{
Value: float64(i),
Priority: int64(i), Priority: int64(i),
Value: float64(i),
}) })
} }
@ -131,8 +131,8 @@ func (s *S) TestEvictAndReplaceWithMinimum(c *C) {
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
var item utility.Item = utility.Item{ var item utility.Item = utility.Item{
Value: float64(i),
Priority: int64(i), Priority: int64(i),
Value: float64(i),
} }
heap.Push(&q, &item) heap.Push(&q, &item)
@ -159,8 +159,8 @@ func (s *S) TestEvictAndReplaceWithMaximum(c *C) {
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
var item utility.Item = utility.Item{ var item utility.Item = utility.Item{
Value: float64(i),
Priority: int64(i), Priority: int64(i),
Value: float64(i),
} }
heap.Push(&q, &item) heap.Push(&q, &item)

View File

@ -10,6 +10,7 @@ package metrics
import ( import (
"fmt" "fmt"
"github.com/matttproud/golang_instrumentation/utility"
"sync" "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 temperature or the hitherto bandwidth used, this would be the metric for such
circumstances. circumstances.
*/ */
type GaugeMetric struct { type Gauge interface {
value float64 AsMarshallable() map[string]interface{}
mutex sync.RWMutex ResetAll()
Set(labels map[string]string, value float64) float64
String() string
} }
func (metric *GaugeMetric) String() string { type gaugeValue struct {
formatString := "[GaugeMetric; value=%f]" 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() metric.mutex.RLock()
defer metric.mutex.RUnlock() 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() metric.mutex.Lock()
defer metric.mutex.Unlock() 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() metric.mutex.RLock()
defer metric.mutex.RUnlock() defer metric.mutex.RUnlock()
return metric.value values := make([]map[string]interface{}, 0, len(metric.values))
} for _, value := range metric.values {
values = append(values, map[string]interface{}{
func (metric *GaugeMetric) Marshallable() map[string]interface{} { labelsKey: value.labels,
metric.mutex.RLock() valueKey: value.value,
defer metric.mutex.RUnlock() })
}
v := make(map[string]interface{}, 2)
return map[string]interface{}{
v[valueKey] = metric.value typeKey: gaugeTypeValue,
v[typeKey] = gaugeTypeValue valueKey: values,
}
return v
} }

View File

@ -9,43 +9,131 @@ license that can be found in the LICENSE file.
package metrics package metrics
import ( import (
. "github.com/matttproud/gocheck" "encoding/json"
"github.com/matttproud/golang_instrumentation/utility/test"
"testing"
) )
func (s *S) TestGaugeCreate(c *C) { func testGauge(t test.Tester) {
m := GaugeMetric{value: 1.0} type input struct {
steps []func(g Gauge)
}
type output struct {
value string
}
c.Assert(m, Not(IsNil)) var scenarios = []struct {
c.Check(m.Get(), Equals, 1.0) 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) { func TestGauge(t *testing.T) {
m := GaugeMetric{value: 2.0} testGauge(t)
c.Check(m.String(), Equals, "[GaugeMetric; value=2.000000]")
} }
func (s *S) TestGaugeSet(c *C) { func BenchmarkGauge(b *testing.B) {
m := GaugeMetric{value: -1.0} for i := 0; i < b.N; i++ {
testGauge(b)
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))
} }

View File

@ -11,8 +11,10 @@ package metrics
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/matttproud/golang_instrumentation/utility"
"math" "math"
"strconv" "strconv"
"sync"
) )
/* /*
@ -53,21 +55,25 @@ func LogarithmicSizedBucketsFor(lower, upper float64) []float64 {
A HistogramSpecification defines how a Histogram is to be built. A HistogramSpecification defines how a Histogram is to be built.
*/ */
type HistogramSpecification struct { type HistogramSpecification struct {
Starts []float64 BucketBuilder BucketBuilder
BucketMaker BucketBuilder
ReportablePercentiles []float64 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 The histogram is an accumulator for samples. It merely routes into which
to bucket to capture an event and provides a percentile calculation to bucket to capture an event and provides a percentile calculation
mechanism. 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 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 the bucket. The interval continues until the beginning of the next bucket
@ -76,23 +82,52 @@ type Histogram struct {
N.B. N.B.
- bucketStarts should be sorted in ascending order; - bucketStarts should be sorted in ascending order;
- len(bucketStarts) must be equivalent to len(buckets); - 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. correspond to the appropriate element in buckets.
*/ */
bucketStarts []float64 bucketStarts []float64
mutex sync.RWMutex
/* /*
These are the buckets that capture samples as they are emitted to the These are the buckets that capture samples as they are emitted to the
histogram. Please consult the reference interface and its implements for histogram. Please consult the reference interface and its implements for
further details about behavior expectations. further details about behavior expectations.
*/ */
buckets []Bucket values map[string]*histogramValue
/* /*
These are the percentile values that will be reported on marshalling. These are the percentile values that will be reported on marshalling.
*/ */
reportablePercentiles []float64 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 lastIndex := 0
for i, bucketStart := range h.bucketStarts { for i, bucketStart := range h.bucketStarts {
@ -103,21 +138,27 @@ func (h *Histogram) Add(value float64) {
lastIndex = i lastIndex = i
} }
h.buckets[lastIndex].Add(value) histogram.buckets[lastIndex].Add(value)
} }
func (h *Histogram) String() string { func (h *histogram) String() string {
stringBuffer := bytes.NewBufferString("") h.mutex.RLock()
defer h.mutex.RUnlock()
stringBuffer := &bytes.Buffer{}
stringBuffer.WriteString("[Histogram { ") stringBuffer.WriteString("[Histogram { ")
for i, bucketStart := range h.bucketStarts { for _, histogram := range h.values {
bucket := h.buckets[i] fmt.Fprintf(stringBuffer, "Labels: %s ", histogram.labels)
stringBuffer.WriteString(fmt.Sprintf("[%f, inf) = %s, ", bucketStart, bucket.String())) for i, bucketStart := range h.bucketStarts {
bucket := histogram.buckets[i]
fmt.Fprintf(stringBuffer, "[%f, inf) = %s, ", bucketStart, bucket)
}
} }
stringBuffer.WriteString("}]") 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. 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++ { for i := currentIndex; i < bucketCount; i++ {
if observationsByBucket[i] == 0 { if observationsByBucket[i] == 0 {
continue 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.") 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 may occur if the underlying bucket catalogs values and employs an eviction
strategy. strategy.
*/ */
func (h *Histogram) bucketForPercentile(percentile float64) (*Bucket, int) { func (h *histogram) bucketForPercentile(signature string, percentile float64) (*Bucket, int) {
bucketCount := len(h.buckets) bucketCount := len(h.bucketStarts)
/* /*
This captures the quantity of samples in a given bucket's range. 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) 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() observations := bucket.Observations()
observationsByBucket[i] = observations observationsByBucket[i] = observations
totalObservations += bucket.Observations() totalObservations += bucket.Observations()
@ -210,14 +255,14 @@ func (h *Histogram) bucketForPercentile(percentile float64) (*Bucket, int) {
take this into account. take this into account.
*/ */
if observationsByBucket[i] == subIndex { 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 collected samples. The requested percentile is expected to be a real
value within (0, 1.0]. value within (0, 1.0].
*/ */
func (h *Histogram) Percentile(percentile float64) float64 { func (h *histogram) percentile(signature string, percentile float64) float64 {
bucket, index := h.bucketForPercentile(percentile) bucket, index := h.bucketForPercentile(signature, percentile)
return (*bucket).ValueForIndex(index) return (*bucket).ValueForIndex(index)
} }
@ -235,38 +280,53 @@ func formatFloat(value float64) string {
return strconv.FormatFloat(value, floatFormat, floatPrecision, floatBitCount) return strconv.FormatFloat(value, floatFormat, floatPrecision, floatBitCount)
} }
func (h *Histogram) Marshallable() map[string]interface{} { func (h *histogram) AsMarshallable() map[string]interface{} {
numberOfPercentiles := len(h.reportablePercentiles) h.mutex.RLock()
defer h.mutex.RUnlock()
result := make(map[string]interface{}, 2) result := make(map[string]interface{}, 2)
result[typeKey] = histogramTypeValue result[typeKey] = histogramTypeValue
values := make([]map[string]interface{}, 0, len(h.values))
value := make(map[string]interface{}, numberOfPercentiles) for signature, value := range h.values {
metricContainer := map[string]interface{}{}
for _, percentile := range h.reportablePercentiles { metricContainer[labelsKey] = value.labels
percentileString := formatFloat(percentile) intermediate := map[string]interface{}{}
value[percentileString] = formatFloat(h.Percentile(percentile)) 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 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. Produce a histogram from a given specification.
*/ */
func CreateHistogram(specification *HistogramSpecification) *Histogram { func NewHistogram(specification *HistogramSpecification) Histogram {
bucketCount := len(specification.Starts) metric := &histogram{
bucketMaker: specification.BucketBuilder,
metric := &Histogram{
bucketStarts: specification.Starts, bucketStarts: specification.Starts,
buckets: make([]Bucket, bucketCount),
reportablePercentiles: specification.ReportablePercentiles, reportablePercentiles: specification.ReportablePercentiles,
} values: map[string]*histogramValue{},
for i := 0; i < bucketCount; i++ {
metric.buckets[i] = specification.BucketMaker()
} }
return metric return metric

View File

@ -1,974 +1,9 @@
/* // Copyright (c) 2013, Matt T. Proud
Copyright (c) 2012, Matt T. Proud // All rights reserved.
All rights reserved. //
// Use of this source code is governed by a BSD-style
Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file.
license that can be found in the LICENSE file.
*/
package metrics package metrics
import ( // TODO(matt): Re-Add tests for this type.
. "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))
}

View File

@ -1,23 +1,17 @@
/* // Copyright (c) 2012, Matt T. Proud
Copyright (c) 2012, Matt T. Proud // All rights reserved.
All rights reserved. //
// Use of this source code is governed by a BSD-style
Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file.
license that can be found in the LICENSE file.
*/
package metrics 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 { type Metric interface {
/* // Produce a JSON-consumable representation of the metric.
Produce a human-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 String() string
/*
Produce a JSON-consumable representation of the metric.
*/
Marshallable() map[string]interface{}
} }

View File

@ -88,11 +88,11 @@ Upon insertion, an object is compared against collected extrema and noted
as a new minimum or maximum if appropriate. as a new minimum or maximum if appropriate.
*/ */
type TallyingBucket struct { type TallyingBucket struct {
observations int estimator TallyingIndexEstimator
smallestObserved float64
largestObserved float64 largestObserved float64
mutex sync.RWMutex mutex sync.RWMutex
estimator TallyingIndexEstimator observations int
smallestObserved float64
} }
func (b *TallyingBucket) Add(value 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) 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. Produce a TallyingBucket with sane defaults.
*/ */
func DefaultTallyingBucket() TallyingBucket { func DefaultTallyingBucket() TallyingBucket {
return TallyingBucket{ return TallyingBucket{
smallestObserved: math.MaxFloat64,
largestObserved: math.SmallestNonzeroFloat64,
estimator: Minimum, estimator: Minimum,
largestObserved: math.SmallestNonzeroFloat64,
smallestObserved: math.MaxFloat64,
} }
} }
func CustomTallyingBucket(estimator TallyingIndexEstimator) TallyingBucket { func CustomTallyingBucket(estimator TallyingIndexEstimator) TallyingBucket {
return TallyingBucket{ return TallyingBucket{
smallestObserved: math.MaxFloat64,
largestObserved: math.SmallestNonzeroFloat64,
estimator: estimator, estimator: estimator,
largestObserved: math.SmallestNonzeroFloat64,
smallestObserved: math.MaxFloat64,
} }
} }

View File

@ -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 retain instrumentation if a panic percolates within the context that is
being measured. being measured.
*/ */
type StopWatch struct { type StopWatch interface {
startTime time.Time Stop() time.Duration
}
type stopWatch struct {
endTime time.Time endTime time.Time
onCompletion CompletionCallback onCompletion CompletionCallback
startTime time.Time
} }
/* /*
Return a new StopWatch that is ready for instrumentation. Return a new StopWatch that is ready for instrumentation.
*/ */
func Start(onCompletion CompletionCallback) *StopWatch { func Start(onCompletion CompletionCallback) StopWatch {
return &StopWatch{ return &stopWatch{
startTime: time.Now(),
onCompletion: onCompletion, 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 Stop the StopWatch returning the elapsed duration of its lifetime while
firing an optional CompletionCallback in the background. firing an optional CompletionCallback in the background.
*/ */
func (s *StopWatch) Stop() time.Duration { func (s *stopWatch) Stop() time.Duration {
s.endTime = time.Now() s.endTime = time.Now()
duration := s.endTime.Sub(s.startTime) duration := s.endTime.Sub(s.startTime)

View File

@ -14,7 +14,10 @@ import (
) )
func (s *S) TestTimerStart(c *C) { 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, Not(IsNil))
c.Assert(stopWatch.startTime, Not(IsNil)) c.Assert(stopWatch.startTime, Not(IsNil))

View File

@ -9,21 +9,39 @@ the LICENSE file.
package registry package registry
import ( import (
"compress/gzip"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"flag"
"fmt"
"github.com/matttproud/golang_instrumentation/metrics" "github.com/matttproud/golang_instrumentation/metrics"
"github.com/matttproud/golang_instrumentation/utility"
"io"
"log" "log"
"net/http" "net/http"
"sort"
"strings" "strings"
"sync" "sync"
"time" "time"
) )
const ( const (
authorization = "Authorization" acceptEncodingHeader = "Accept-Encoding"
contentType = "Content-Type" authorization = "Authorization"
jsonContentType = "application/json" authorizationHeader = "WWW-Authenticate"
jsonSuffix = ".json" 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. overhead such that it can be reported.
*/ */
var requestLatencyAccumulator metrics.CompletionCallback = func(duration time.Duration) { var requestLatencyAccumulator metrics.CompletionCallback = func(duration time.Duration) {
microseconds := float64(duration / time.Millisecond) microseconds := float64(duration / time.Microsecond)
requestLatencyLogarithmicAccumulating.Add(microseconds) requestLatency.Add(nil, microseconds)
requestLatencyEqualAccumulating.Add(microseconds) }
requestLatencyLogarithmicTallying.Add(microseconds)
requestLatencyEqualTallying.Add(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. own.
*/ */
type Registry struct { type Registry struct {
mutex sync.RWMutex mutex sync.RWMutex
NameToMetric map[string]metrics.Metric signatureContainers map[string]container
} }
/* /*
@ -56,7 +80,7 @@ cases.
*/ */
func NewRegistry() *Registry { func NewRegistry() *Registry {
return &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. Associate a Metric with the DefaultRegistry.
*/ */
func Register(name, unusedDocstring string, unusedBaseLabels map[string]string, metric metrics.Metric) { func Register(name, docstring string, baseLabels map[string]string, metric metrics.Metric) error {
DefaultRegistry.Register(name, unusedDocstring, unusedBaseLabels, metric) 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. 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() r.mutex.Lock()
defer r.mutex.Unlock() defer r.mutex.Unlock()
if _, present := r.NameToMetric[name]; !present { if baseLabels == nil {
r.NameToMetric[name] = metric baseLabels = map[string]string{}
log.Printf("Registered %s.\n", name)
} else {
log.Printf("Attempted to register duplicate %s metric.\n", name)
} }
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 { func (register *Registry) YieldBasicAuthExporter(username, password string) http.HandlerFunc {
exporter := register.YieldExporter() exporter := register.YieldExporter()
@ -108,12 +203,80 @@ func (register *Registry) YieldBasicAuthExporter(username, password string) http
if authenticated { if authenticated {
exporter.ServeHTTP(w, r) exporter.ServeHTTP(w, r)
} else { } else {
w.Header().Add("WWW-Authenticate", "Basic") w.Header().Add(authorizationHeader, authorizationHeaderValue)
http.Error(w, "access forbidden", 401) 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 Create a http.HandlerFunc that is tied to r Registry such that requests
against it generate a representation of the housed metrics. 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 { func (registry *Registry) YieldExporter() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var instrumentable metrics.InstrumentableCall = func() { var instrumentable metrics.InstrumentableCall = func() {
requestCount.Increment() requestCount.Increment(nil)
url := r.URL url := r.URL
if strings.HasSuffix(url.Path, jsonSuffix) { if strings.HasSuffix(url.Path, jsonSuffix) {
w.Header().Set(contentType, jsonContentType) header := w.Header()
composite := make(map[string]interface{}, len(registry.NameToMetric)) header.Set(ProtocolVersionHeader, APIVersion)
for name, metric := range registry.NameToMetric { header.Set(contentTypeHeader, jsonContentType)
composite[name] = metric.Marshallable()
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 { } else {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
} }
@ -142,3 +309,9 @@ func (registry *Registry) YieldExporter() http.HandlerFunc {
metrics.InstrumentCall(instrumentable, requestLatencyAccumulator) 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).")
}

331
registry_test.go Normal file
View File

@ -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)
}
}

View File

@ -20,43 +20,25 @@ exposed if the DefaultRegistry's exporter is hooked into the HTTP request
handler. handler.
*/ */
var ( var (
// TODO(matt): Refresh these names to support namespacing. marshalErrorCount = metrics.NewCounter()
dumpErrorCount = metrics.NewCounter()
requestCount *metrics.CounterMetric = &metrics.CounterMetric{} requestCount = metrics.NewCounter()
requestLatencyLogarithmicBuckets []float64 = metrics.LogarithmicSizedBucketsFor(0, 1000) requestLatencyBuckets = metrics.LogarithmicSizedBucketsFor(0, 1000)
requestLatencyEqualBuckets []float64 = metrics.EquallySizedBucketsFor(0, 1000, 10) requestLatency = metrics.NewHistogram(&metrics.HistogramSpecification{
requestLatencyLogarithmicAccumulating *metrics.Histogram = metrics.CreateHistogram(&metrics.HistogramSpecification{ Starts: requestLatencyBuckets,
Starts: requestLatencyLogarithmicBuckets, BucketBuilder: metrics.AccumulatingBucketBuilder(metrics.EvictAndReplaceWith(50, maths.Average), 1000),
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,
ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.9, 0.99}, ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.9, 0.99},
}) })
startTime *metrics.GaugeMetric = &metrics.GaugeMetric{} startTime = metrics.NewGauge()
) )
func init() { 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("telemetry_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("telemetry_requests_metrics_latency_microseconds", "A histogram of the response latency for requests made against the telemetry system.", NilLabels, requestLatency)
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("instance_start_time_seconds", "The time at which the current instance started (UTC).", NilLabels, startTime) DefaultRegistry.Register("instance_start_time_seconds", "The time at which the current instance started (UTC).", NilLabels, startTime)
} }

View File

@ -10,11 +10,6 @@ license that can be found in the LICENSE file.
The utility package provides general purpose helpers to assist with this The utility package provides general purpose helpers to assist with this
library. 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.go provides a simple priority queue.
priority_queue_test.go provides a test complement for the priority_queue.go priority_queue_test.go provides a test complement for the priority_queue.go

View File

@ -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
}

View File

@ -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)
}

View File

@ -9,8 +9,8 @@ license that can be found in the LICENSE file.
package utility package utility
type Item struct { type Item struct {
Value interface{}
Priority int64 Priority int64
Value interface{}
index int index int
} }

41
utility/signature.go Normal file
View File

@ -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()
}

43
utility/signature_test.go Normal file
View File

@ -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)
}
}

14
utility/test/interface.go Normal file
View File

@ -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{})
}