diff --git a/prometheus/counter.go b/prometheus/counter.go index 553f9b005..888a999ba 100644 --- a/prometheus/counter.go +++ b/prometheus/counter.go @@ -72,7 +72,7 @@ func NewCounter(opts CounterOpts) Counter { nil, opts.ConstLabels, ) - result := &counter{desc: desc, labelPairs: desc.constLabelPairs} + result := &counter{desc: desc, labelPairs: desc.constLabelPairs, now: time.Now} result.init(result) // Init self-collection. return result } @@ -90,6 +90,8 @@ type counter struct { labelPairs []*dto.LabelPair exemplar atomic.Value // *dto.Exemplar + + now func() time.Time // To mock out time.Now() for testing. } func (c *counter) Desc() *Desc { @@ -142,7 +144,7 @@ func (c *counter) updateExemplar(v float64, l Labels) { if l == nil { return } - e, err := newExemplar(v, time.Now(), l) + e, err := newExemplar(v, c.now(), l) if err != nil { panic(err) } diff --git a/prometheus/counter_test.go b/prometheus/counter_test.go index fd98fb1ce..4b3866734 100644 --- a/prometheus/counter_test.go +++ b/prometheus/counter_test.go @@ -17,7 +17,10 @@ import ( "fmt" "math" "testing" + "time" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes" dto "github.com/prometheus/client_model/go" ) @@ -210,3 +213,61 @@ func TestCounterAddSmall(t *testing.T) { t.Errorf("expected %q, got %q", expected, got) } } + +func TestCounterExemplar(t *testing.T) { + now := time.Now() + + counter := NewCounter(CounterOpts{ + Name: "test", + Help: "test help", + }).(*counter) + counter.now = func() time.Time { return now } + + ts, err := ptypes.TimestampProto(now) + if err != nil { + t.Fatal(err) + } + expectedExemplar := &dto.Exemplar{ + Label: []*dto.LabelPair{ + &dto.LabelPair{Name: proto.String("foo"), Value: proto.String("bar")}, + }, + Value: proto.Float64(42), + Timestamp: ts, + } + + counter.AddWithExemplar(42, Labels{"foo": "bar"}) + if expected, got := expectedExemplar.String(), counter.exemplar.Load().(*dto.Exemplar).String(); expected != got { + t.Errorf("expected exemplar %s, got %s.", expected, got) + } + + addExemplarWithInvalidLabel := func() (err error) { + defer func() { + if e := recover(); e != nil { + err = e.(error) + } + }() + // Should panic because of invalid label name. + counter.AddWithExemplar(42, Labels{":o)": "smile"}) + return nil + } + if addExemplarWithInvalidLabel() == nil { + t.Error("adding exemplar with invalid label succeeded") + } + + addExemplarWithOversizedLabels := func() (err error) { + defer func() { + if e := recover(); e != nil { + err = e.(error) + } + }() + // Should panic because of 65 runes. + counter.AddWithExemplar(42, Labels{ + "abcdefghijklmnopqrstuvwxyz": "26+16 characters", + "x1234567": "8+15 characters", + }) + return nil + } + if addExemplarWithOversizedLabels() == nil { + t.Error("adding exemplar with oversized labels succeeded") + } +} diff --git a/prometheus/histogram.go b/prometheus/histogram.go index e30de3bc8..7ea48c8a7 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -199,6 +199,7 @@ func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogr upperBounds: opts.Buckets, labelPairs: makeLabelPairs(desc, labelValues), counts: [2]*histogramCounts{{}, {}}, + now: time.Now, } for i, upperBound := range h.upperBounds { if i < len(h.upperBounds)-1 { @@ -267,6 +268,8 @@ type histogram struct { upperBounds []float64 labelPairs []*dto.LabelPair exemplars []atomic.Value // One more than buckets (to include +Inf), each a *dto.Exemplar. + + now func() time.Time // To mock out time.Now() for testing. } func (h *histogram) Desc() *Desc { @@ -398,7 +401,7 @@ func (h *histogram) updateExemplar(v float64, bucket int, l Labels) { if l == nil { return } - e, err := newExemplar(v, time.Now(), l) + e, err := newExemplar(v, h.now(), l) if err != nil { panic(err) } diff --git a/prometheus/histogram_test.go b/prometheus/histogram_test.go index 306eee9a1..f50827c69 100644 --- a/prometheus/histogram_test.go +++ b/prometheus/histogram_test.go @@ -22,7 +22,10 @@ import ( "sync" "testing" "testing/quick" + "time" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes" dto "github.com/prometheus/client_model/go" ) @@ -182,7 +185,11 @@ func TestHistogramConcurrency(t *testing.T) { go func(vals []float64) { start.Wait() for _, v := range vals { - sum.Observe(v) + if n%2 == 0 { + sum.Observe(v) + } else { + sum.ObserveWithExemplar(v, Labels{"foo": "bar"}) + } } end.Done() }(vals) @@ -201,9 +208,13 @@ func TestHistogramConcurrency(t *testing.T) { } wantCounts := getCumulativeCounts(allVars) + wantBuckets := len(testBuckets) + if !math.IsInf(m.Histogram.Bucket[len(m.Histogram.Bucket)-1].GetUpperBound(), +1) { + wantBuckets-- + } - if got, want := len(m.Histogram.Bucket), len(testBuckets)-1; got != want { - t.Errorf("got %d buckets in protobuf, want %d", got, want) + if got := len(m.Histogram.Bucket); got != wantBuckets { + t.Errorf("got %d buckets in protobuf, want %d", got, wantBuckets) } for i, wantBound := range testBuckets { if i == len(testBuckets)-1 { @@ -384,3 +395,62 @@ func TestHistogramAtomicObserve(t *testing.T) { runtime.Gosched() } } + +func TestHistogramExemplar(t *testing.T) { + now := time.Now() + + histogram := NewHistogram(HistogramOpts{ + Name: "test", + Help: "test help", + Buckets: []float64{1, 2, 3, 4}, + }).(*histogram) + histogram.now = func() time.Time { return now } + + ts, err := ptypes.TimestampProto(now) + if err != nil { + t.Fatal(err) + } + expectedExemplars := []*dto.Exemplar{ + nil, + &dto.Exemplar{ + Label: []*dto.LabelPair{ + &dto.LabelPair{Name: proto.String("id"), Value: proto.String("2")}, + }, + Value: proto.Float64(1.6), + Timestamp: ts, + }, + nil, + &dto.Exemplar{ + Label: []*dto.LabelPair{ + &dto.LabelPair{Name: proto.String("id"), Value: proto.String("3")}, + }, + Value: proto.Float64(4), + Timestamp: ts, + }, + &dto.Exemplar{ + Label: []*dto.LabelPair{ + &dto.LabelPair{Name: proto.String("id"), Value: proto.String("4")}, + }, + Value: proto.Float64(4.5), + Timestamp: ts, + }, + } + + histogram.ObserveWithExemplar(1.5, Labels{"id": "1"}) + histogram.ObserveWithExemplar(1.6, Labels{"id": "2"}) // To replace exemplar in bucket 0. + histogram.ObserveWithExemplar(4, Labels{"id": "3"}) + histogram.ObserveWithExemplar(4.5, Labels{"id": "4"}) // Should go to +Inf bucket. + + for i, ex := range histogram.exemplars { + var got, expected string + if val := ex.Load(); val != nil { + got = val.(*dto.Exemplar).String() + } + if expectedExemplars[i] != nil { + expected = expectedExemplars[i].String() + } + if got != expected { + t.Errorf("expected exemplar %s, got %s.", expected, got) + } + } +}