Skip to content

Commit ba79017

Browse files
authored
Merge pull request #706 from prometheus/beorn7/exemplars
Add exemplars to counter and histogram
2 parents 803ef2a + c32ffd1 commit ba79017

File tree

10 files changed

+369
-64
lines changed

10 files changed

+369
-64
lines changed

examples/random/main.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package main
1818

1919
import (
2020
"flag"
21+
"fmt"
2122
"log"
2223
"math"
2324
"math/rand"
@@ -89,7 +90,11 @@ func main() {
8990
for {
9091
v := (rand.NormFloat64() * *normDomain) + *normMean
9192
rpcDurations.WithLabelValues("normal").Observe(v)
92-
rpcDurationsHistogram.Observe(v)
93+
rpcDurationsHistogram.ObserveWithExemplar(
94+
// Demonstrate exemplar support with a dummy ID. This would be
95+
// something like a trace ID in a real application.
96+
v, prometheus.Labels{"dummyID": fmt.Sprint(rand.Intn(100000))},
97+
)
9398
time.Sleep(time.Duration(75*oscillationFactor()) * time.Millisecond)
9499
}
95100
}()
@@ -103,6 +108,12 @@ func main() {
103108
}()
104109

105110
// Expose the registered metrics via HTTP.
106-
http.Handle("/metrics", promhttp.Handler())
111+
http.Handle("/metrics", promhttp.HandlerFor(
112+
prometheus.DefaultGatherer,
113+
promhttp.HandlerOpts{
114+
// Opt into OpenMetrics to support exemplars.
115+
EnableOpenMetrics: true,
116+
},
117+
))
107118
log.Fatal(http.ListenAndServe(*addr, nil))
108119
}

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ require (
55
github.com/cespare/xxhash/v2 v2.1.1
66
github.com/golang/protobuf v1.3.2
77
github.com/json-iterator/go v1.1.8
8-
github.com/prometheus/client_model v0.1.0
9-
github.com/prometheus/common v0.7.0
8+
github.com/prometheus/client_model v0.2.0
9+
github.com/prometheus/common v0.9.0
1010
github.com/prometheus/procfs v0.0.8
1111
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f
1212
)

go.sum

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,12 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f
5858
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
5959
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
6060
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
61-
github.com/prometheus/client_model v0.1.0 h1:ElTg5tNp4DqfV7UQjDqv2+RJlNzsDtvNAWccbItceIE=
62-
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
61+
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
62+
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
6363
github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw=
6464
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
65-
github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
66-
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
65+
github.com/prometheus/common v0.9.0 h1:yg//x/8DqN+PxXTBFMwVCopGqDn3wSxmbF/3PCuu1bk=
66+
github.com/prometheus/common v0.9.0/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
6767
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
6868
github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=
6969
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@@ -97,4 +97,4 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
9797
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
9898
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
9999
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
100-
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
100+
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

prometheus/counter.go

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"errors"
1818
"math"
1919
"sync/atomic"
20+
"time"
2021

2122
dto "github.com/prometheus/client_model/go"
2223
)
@@ -40,6 +41,14 @@ type Counter interface {
4041
// Add adds the given value to the counter. It panics if the value is <
4142
// 0.
4243
Add(float64)
44+
// AddWithExemplar works like Add but also replaces the currently saved
45+
// exemplar (if any) with a new one, created from the provided value,
46+
// the current time as timestamp, and the provided labels. Empty Labels
47+
// will lead to a valid (label-less) exemplar. But if Labels is nil, the
48+
// current exemplar is left in place. This method panics if the value is
49+
// < 0, if any of the provided labels are invalid, or if the provided
50+
// labels contain more than 64 runes in total.
51+
AddWithExemplar(value float64, exemplar Labels)
4352
}
4453

4554
// CounterOpts is an alias for Opts. See there for doc comments.
@@ -61,7 +70,7 @@ func NewCounter(opts CounterOpts) Counter {
6170
nil,
6271
opts.ConstLabels,
6372
)
64-
result := &counter{desc: desc, labelPairs: desc.constLabelPairs}
73+
result := &counter{desc: desc, labelPairs: desc.constLabelPairs, now: time.Now}
6574
result.init(result) // Init self-collection.
6675
return result
6776
}
@@ -78,6 +87,9 @@ type counter struct {
7887
desc *Desc
7988

8089
labelPairs []*dto.LabelPair
90+
exemplar atomic.Value // Containing nil or a *dto.Exemplar.
91+
92+
now func() time.Time // To mock out time.Now() for testing.
8193
}
8294

8395
func (c *counter) Desc() *Desc {
@@ -88,6 +100,7 @@ func (c *counter) Add(v float64) {
88100
if v < 0 {
89101
panic(errors.New("counter cannot decrease in value"))
90102
}
103+
91104
ival := uint64(v)
92105
if float64(ival) == v {
93106
atomic.AddUint64(&c.valInt, ival)
@@ -103,6 +116,11 @@ func (c *counter) Add(v float64) {
103116
}
104117
}
105118

119+
func (c *counter) AddWithExemplar(v float64, e Labels) {
120+
c.Add(v)
121+
c.updateExemplar(v, e)
122+
}
123+
106124
func (c *counter) Inc() {
107125
atomic.AddUint64(&c.valInt, 1)
108126
}
@@ -112,7 +130,23 @@ func (c *counter) Write(out *dto.Metric) error {
112130
ival := atomic.LoadUint64(&c.valInt)
113131
val := fval + float64(ival)
114132

115-
return populateMetric(CounterValue, val, c.labelPairs, out)
133+
var exemplar *dto.Exemplar
134+
if e := c.exemplar.Load(); e != nil {
135+
exemplar = e.(*dto.Exemplar)
136+
}
137+
138+
return populateMetric(CounterValue, val, c.labelPairs, exemplar, out)
139+
}
140+
141+
func (c *counter) updateExemplar(v float64, l Labels) {
142+
if l == nil {
143+
return
144+
}
145+
e, err := newExemplar(v, c.now(), l)
146+
if err != nil {
147+
panic(err)
148+
}
149+
c.exemplar.Store(e)
116150
}
117151

118152
// CounterVec is a Collector that bundles a set of Counters that all share the

prometheus/counter_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import (
1717
"fmt"
1818
"math"
1919
"testing"
20+
"time"
21+
22+
"github.com/golang/protobuf/proto"
23+
"github.com/golang/protobuf/ptypes"
2024

2125
dto "github.com/prometheus/client_model/go"
2226
)
@@ -210,3 +214,61 @@ func TestCounterAddSmall(t *testing.T) {
210214
t.Errorf("expected %q, got %q", expected, got)
211215
}
212216
}
217+
218+
func TestCounterExemplar(t *testing.T) {
219+
now := time.Now()
220+
221+
counter := NewCounter(CounterOpts{
222+
Name: "test",
223+
Help: "test help",
224+
}).(*counter)
225+
counter.now = func() time.Time { return now }
226+
227+
ts, err := ptypes.TimestampProto(now)
228+
if err != nil {
229+
t.Fatal(err)
230+
}
231+
expectedExemplar := &dto.Exemplar{
232+
Label: []*dto.LabelPair{
233+
&dto.LabelPair{Name: proto.String("foo"), Value: proto.String("bar")},
234+
},
235+
Value: proto.Float64(42),
236+
Timestamp: ts,
237+
}
238+
239+
counter.AddWithExemplar(42, Labels{"foo": "bar"})
240+
if expected, got := expectedExemplar.String(), counter.exemplar.Load().(*dto.Exemplar).String(); expected != got {
241+
t.Errorf("expected exemplar %s, got %s.", expected, got)
242+
}
243+
244+
addExemplarWithInvalidLabel := func() (err error) {
245+
defer func() {
246+
if e := recover(); e != nil {
247+
err = e.(error)
248+
}
249+
}()
250+
// Should panic because of invalid label name.
251+
counter.AddWithExemplar(42, Labels{":o)": "smile"})
252+
return nil
253+
}
254+
if addExemplarWithInvalidLabel() == nil {
255+
t.Error("adding exemplar with invalid label succeeded")
256+
}
257+
258+
addExemplarWithOversizedLabels := func() (err error) {
259+
defer func() {
260+
if e := recover(); e != nil {
261+
err = e.(error)
262+
}
263+
}()
264+
// Should panic because of 65 runes.
265+
counter.AddWithExemplar(42, Labels{
266+
"abcdefghijklmnopqrstuvwxyz": "26+16 characters",
267+
"x1234567": "8+15 characters",
268+
})
269+
return nil
270+
}
271+
if addExemplarWithOversizedLabels() == nil {
272+
t.Error("adding exemplar with oversized labels succeeded")
273+
}
274+
}

prometheus/gauge.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func (g *gauge) Sub(val float64) {
123123

124124
func (g *gauge) Write(out *dto.Metric) error {
125125
val := math.Float64frombits(atomic.LoadUint64(&g.valBits))
126-
return populateMetric(GaugeValue, val, g.labelPairs, out)
126+
return populateMetric(GaugeValue, val, g.labelPairs, nil, out)
127127
}
128128

129129
// GaugeVec is a Collector that bundles a set of Gauges that all share the same

0 commit comments

Comments
 (0)