Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exemplar support for const histogram #986

Merged
merged 7 commits into from Mar 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
112 changes: 110 additions & 2 deletions prometheus/examples_test.go
Expand Up @@ -24,9 +24,8 @@ import (

//nolint:staticcheck // Ignore SA1019. Need to keep deprecated package for compatibility.
"github.com/golang/protobuf/proto"
"github.com/prometheus/common/expfmt"

dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
Expand Down Expand Up @@ -599,6 +598,115 @@ func ExampleNewConstHistogram() {
// >
}

func ExampleNewConstHistogram_WithExemplar() {
desc := prometheus.NewDesc(
"http_request_duration_seconds",
"A histogram of the HTTP request durations.",
[]string{"code", "method"},
prometheus.Labels{"owner": "example"},
)

// Create a constant histogram from values we got from a 3rd party telemetry system.
h := prometheus.MustNewConstHistogram(
desc,
4711, 403.34,
map[float64]uint64{25: 121, 50: 2403, 100: 3221, 200: 4233},
"200", "get",
)

// Wrap const histogram with exemplars for each bucket.
exemplarTs, _ := time.Parse(time.RFC850, "Monday, 02-Jan-06 15:04:05 GMT")
exemplarLabels := prometheus.Labels{"testName": "testVal"}
h = prometheus.MustNewMetricWithExemplars(
h,
prometheus.Exemplar{Labels: exemplarLabels, Timestamp: exemplarTs, Value: 24.0},
prometheus.Exemplar{Labels: exemplarLabels, Timestamp: exemplarTs, Value: 42.0},
prometheus.Exemplar{Labels: exemplarLabels, Timestamp: exemplarTs, Value: 89.0},
prometheus.Exemplar{Labels: exemplarLabels, Timestamp: exemplarTs, Value: 157.0},
)

// Just for demonstration, let's check the state of the histogram by
// (ab)using its Write method (which is usually only used by Prometheus
// internally).
metric := &dto.Metric{}
h.Write(metric)
fmt.Println(proto.MarshalTextString(metric))

// Output:
// label: <
// name: "code"
// value: "200"
// >
// label: <
// name: "method"
// value: "get"
// >
// label: <
// name: "owner"
// value: "example"
// >
// histogram: <
// sample_count: 4711
// sample_sum: 403.34
// bucket: <
// cumulative_count: 121
// upper_bound: 25
// exemplar: <
// label: <
// name: "testName"
// value: "testVal"
// >
// value: 24
// timestamp: <
// seconds: 1136214245
// >
// >
// >
// bucket: <
// cumulative_count: 2403
// upper_bound: 50
// exemplar: <
// label: <
// name: "testName"
// value: "testVal"
// >
// value: 42
// timestamp: <
// seconds: 1136214245
// >
// >
// >
// bucket: <
// cumulative_count: 3221
// upper_bound: 100
// exemplar: <
// label: <
// name: "testName"
// value: "testVal"
// >
// value: 89
// timestamp: <
// seconds: 1136214245
// >
// >
// >
// bucket: <
// cumulative_count: 4233
// upper_bound: 200
// exemplar: <
// label: <
// name: "testName"
// value: "testVal"
// >
// value: 157
// timestamp: <
// seconds: 1136214245
// >
// >
// >
// >
}

func ExampleAlreadyRegisteredError() {
reqCounter := prometheus.NewCounter(prometheus.CounterOpts{
Name: "requests_total",
Expand Down
2 changes: 1 addition & 1 deletion prometheus/histogram.go
Expand Up @@ -581,11 +581,11 @@ func (h *constHistogram) Desc() *Desc {

func (h *constHistogram) Write(out *dto.Metric) error {
his := &dto.Histogram{}

buckets := make([]*dto.Bucket, 0, len(h.buckets))

his.SampleCount = proto.Uint64(h.count)
his.SampleSum = proto.Float64(h.sum)

for upperBound, count := range h.buckets {
buckets = append(buckets, &dto.Bucket{
CumulativeCount: proto.Uint64(count),
Expand Down
12 changes: 6 additions & 6 deletions prometheus/histogram_test.go
Expand Up @@ -424,24 +424,24 @@ func TestHistogramExemplar(t *testing.T) {
}
expectedExemplars := []*dto.Exemplar{
nil,
&dto.Exemplar{
{
Label: []*dto.LabelPair{
&dto.LabelPair{Name: proto.String("id"), Value: proto.String("2")},
{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")},
{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")},
{Name: proto.String("id"), Value: proto.String("4")},
},
Value: proto.Float64(4.5),
Timestamp: ts,
Expand Down
90 changes: 90 additions & 0 deletions prometheus/metric.go
Expand Up @@ -14,6 +14,8 @@
package prometheus

import (
"errors"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -158,3 +160,91 @@ func (m timestampedMetric) Write(pb *dto.Metric) error {
func NewMetricWithTimestamp(t time.Time, m Metric) Metric {
return timestampedMetric{Metric: m, t: t}
}

type withExemplarsMetric struct {
Metric

exemplars []*dto.Exemplar
}

func (m *withExemplarsMetric) Write(pb *dto.Metric) error {
if err := m.Metric.Write(pb); err != nil {
return err
}

switch {
case pb.Counter != nil:
pb.Counter.Exemplar = m.exemplars[len(m.exemplars)-1]
case pb.Histogram != nil:
for _, e := range m.exemplars {
// pb.Histogram.Bucket are sorted by UpperBound.
i := sort.Search(len(pb.Histogram.Bucket), func(i int) bool {
return pb.Histogram.Bucket[i].GetUpperBound() >= e.GetValue()
})
if i < len(pb.Histogram.Bucket) {
pb.Histogram.Bucket[i].Exemplar = e
} else {
// This is not possible as last bucket is Inf.
panic("no bucket was found for given exemplar value")
}
}
default:
// TODO(bwplotka): Implement Gauge?
return errors.New("cannot inject exemplar into Gauge, Summary or Untyped")
}

return nil
}

// Exemplar is easier to use, user-facing representation of *dto.Exemplar.
type Exemplar struct {
Value float64
Labels Labels
// Optional.
// Default value (time.Time{}) indicates its empty, which should be
// understood as time.Now() time at the moment of creation of metric.
Timestamp time.Time
}

// NewMetricWithExemplars returns a new Metric wrapping the provided Metric with given
// exemplars. Exemplars are validated.
//
// Only last applicable exemplar is injected from the list.
// For example for Counter it means last exemplar is injected.
// For Histogram, it means last applicable exemplar for each bucket is injected.
//
// NewMetricWithExemplars works best with MustNewConstMetric and
// MustNewConstHistogram, see example.
func NewMetricWithExemplars(m Metric, exemplars ...Exemplar) (Metric, error) {
if len(exemplars) == 0 {
return nil, errors.New("no exemplar was passed for NewMetricWithExemplars")
}

var (
now = time.Now()
exs = make([]*dto.Exemplar, len(exemplars))
err error
)
for i, e := range exemplars {
ts := e.Timestamp
if ts == (time.Time{}) {
ts = now
}
exs[i], err = newExemplar(e.Value, ts, e.Labels)
if err != nil {
return nil, err
}
}

return &withExemplarsMetric{Metric: m, exemplars: exs}, nil
}

// MustNewMetricWithExemplars is a version of NewMetricWithExemplars that panics where
// NewMetricWithExemplars would have returned an error.
func MustNewMetricWithExemplars(m Metric, exemplars ...Exemplar) Metric {
ret, err := NewMetricWithExemplars(m, exemplars...)
if err != nil {
panic(err)
}
return ret
}
45 changes: 44 additions & 1 deletion prometheus/metric_test.go
Expand Up @@ -13,7 +13,12 @@

package prometheus

import "testing"
import (
"testing"

"github.com/golang/protobuf/proto"
dto "github.com/prometheus/client_model/go"
)

func TestBuildFQName(t *testing.T) {
scenarios := []struct{ namespace, subsystem, name, result string }{
Expand All @@ -33,3 +38,41 @@ func TestBuildFQName(t *testing.T) {
}
}
}

func TestWithExemplarsMetric(t *testing.T) {
t.Run("histogram", func(t *testing.T) {
// Create a constant histogram from values we got from a 3rd party telemetry system.
h := MustNewConstHistogram(
NewDesc("http_request_duration_seconds", "A histogram of the HTTP request durations.", nil, nil),
4711, 403.34,
map[float64]uint64{25: 121, 50: 2403, 100: 3221, 200: 4233},
)

m := &withExemplarsMetric{Metric: h, exemplars: []*dto.Exemplar{
{Value: proto.Float64(24.0)},
{Value: proto.Float64(25.1)},
{Value: proto.Float64(42.0)},
{Value: proto.Float64(89.0)},
{Value: proto.Float64(100.0)},
{Value: proto.Float64(157.0)},
}}
metric := dto.Metric{}
if err := m.Write(&metric); err != nil {
t.Fatal(err)
}
if want, got := 4, len(metric.GetHistogram().Bucket); want != got {
t.Errorf("want %v, got %v", want, got)
}

expectedExemplarVals := []float64{24.0, 42.0, 100.0, 157.0}
for i, b := range metric.GetHistogram().Bucket {
if b.Exemplar == nil {
t.Errorf("Expected exemplar for bucket %v, got nil", i)
}
if want, got := expectedExemplarVals[i], *metric.GetHistogram().Bucket[i].Exemplar.Value; want != got {
t.Errorf("%v: want %v, got %v", i, want, got)
}
}
})

}