Skip to content
This repository has been archived by the owner on May 7, 2024. It is now read-only.

Commit

Permalink
Added MustNewMetricWithExemplars that wraps metrics with exemplar
Browse files Browse the repository at this point in the history
Changes:
* Make sure to not "leak" dto.Metric
* Reused upper bounds we already have for histogram
* Common code for all types.

Signed-off-by: Bartlomiej Plotka <bwplotka@gmail.com>
  • Loading branch information
bwplotka committed Mar 16, 2022
1 parent daaf45b commit 6384207
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 87 deletions.
98 changes: 74 additions & 24 deletions prometheus/examples_test.go
Expand Up @@ -22,13 +22,10 @@ import (
"strings"
"time"

"google.golang.org/protobuf/types/known/timestamppb"

//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 @@ -551,30 +548,83 @@ func ExampleNewConstHistogram() {
prometheus.Labels{"owner": "example"},
)

var exemplars []*dto.Exemplar
n := "testName"
v := "testVal"
lp := dto.LabelPair{Name: &n, Value: &v}
var labelPairs []*dto.LabelPair
labelPairs = append(labelPairs, &lp)
vals := []float64{24.0, 42.0, 89.0, 157.0}
t, _ := time.Parse("unix", "Mon Jan _2 15:04:05 MST 2006")
ts := timestamppb.New(t)

for i := 0; i < 4; i++ {
e := dto.Exemplar{Label: labelPairs, Value: &vals[i], Timestamp: ts}
exemplars = append(exemplars, &e)
}
// 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",
)

// 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
// >
// bucket: <
// cumulative_count: 2403
// upper_bound: 50
// >
// bucket: <
// cumulative_count: 3221
// upper_bound: 100
// >
// bucket: <
// cumulative_count: 4233
// upper_bound: 200
// >
// >
}

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.MustNewConstHistogramWithExemplar(
h := prometheus.MustNewConstHistogram(
desc,
4711, 403.34,
map[float64]uint64{25: 121, 50: 2403, 100: 3221, 200: 4233},
exemplars,
"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).
Expand Down Expand Up @@ -608,7 +658,7 @@ func ExampleNewConstHistogram() {
// >
// value: 24
// timestamp: <
// seconds: -62135596800
// seconds: 1136214245
// >
// >
// >
Expand All @@ -622,7 +672,7 @@ func ExampleNewConstHistogram() {
// >
// value: 42
// timestamp: <
// seconds: -62135596800
// seconds: 1136214245
// >
// >
// >
Expand All @@ -636,7 +686,7 @@ func ExampleNewConstHistogram() {
// >
// value: 89
// timestamp: <
// seconds: -62135596800
// seconds: 1136214245
// >
// >
// >
Expand All @@ -650,7 +700,7 @@ func ExampleNewConstHistogram() {
// >
// value: 157
// timestamp: <
// seconds: -62135596800
// seconds: 1136214245
// >
// >
// >
Expand Down
63 changes: 1 addition & 62 deletions prometheus/histogram.go
Expand Up @@ -573,7 +573,6 @@ type constHistogram struct {
sum float64
buckets map[float64]uint64
labelPairs []*dto.LabelPair
exemplars []*dto.Exemplar
}

func (h *constHistogram) Desc() *Desc {
Expand All @@ -582,9 +581,8 @@ func (h *constHistogram) Desc() *Desc {

func (h *constHistogram) Write(out *dto.Metric) error {
his := &dto.Histogram{}
// h.buckets, buckets and bounds are all the same length

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

his.SampleCount = proto.Uint64(h.count)
his.SampleSum = proto.Float64(h.sum)
Expand All @@ -593,28 +591,11 @@ func (h *constHistogram) Write(out *dto.Metric) error {
CumulativeCount: proto.Uint64(count),
UpperBound: proto.Float64(upperBound),
})
bounds = append(bounds, upperBound)
}

// make sure that both bounds and buckets have the same ordering
if len(buckets) > 0 {
sort.Sort(buckSort(buckets))
sort.Float64s(bounds)
}

if len(h.exemplars) > 0 {
r := len(buckets)
l := len(h.exemplars)
for i := 0; i < r && i < l; i++ {
bound := sort.SearchFloat64s(bounds, *h.exemplars[i].Value)
// Only append the exemplar if it's within the bounds defined in the
// buckets.
if bound < r {
buckets[bound].Exemplar = h.exemplars[i]
}
}
}

his.Bucket = buckets

out.Histogram = his
Expand All @@ -636,7 +617,6 @@ func (h *constHistogram) Write(out *dto.Metric) error {
//
// NewConstHistogram returns an error if the length of labelValues is not
// consistent with the variable labels in Desc or if Desc is invalid.

func NewConstHistogram(
desc *Desc,
count uint64,
Expand Down Expand Up @@ -675,47 +655,6 @@ func MustNewConstHistogram(
return m
}

func NewConstHistogramWithExemplar(
desc *Desc,
count uint64,
sum float64,
buckets map[float64]uint64,
exemplars []*dto.Exemplar,
labelValues ...string,

) (Metric, error) {
if desc.err != nil {
return nil, desc.err
}
if err := validateLabelValues(labelValues, len(desc.variableLabels)); err != nil {
return nil, err
}

h, err := NewConstHistogram(desc, count, sum, buckets, labelValues...)
if err != nil {
return nil, err
}

h.(*constHistogram).exemplars = exemplars

return h, nil
}

// MustNewConstHistogram is a version of NewConstHistogram that panics where
// NewConstHistogram would have returned an error.
func MustNewConstHistogramWithExemplar(
desc *Desc,
count uint64,
sum float64,
buckets map[float64]uint64,
exemplars []*dto.Exemplar,
labelValues ...string,
) Metric {
h := MustNewConstHistogram(desc, count, sum, buckets, labelValues...)
h.(*constHistogram).exemplars = exemplars
return h
}

type buckSort []*dto.Bucket

func (s buckSort) Len() int {
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
}

0 comments on commit 6384207

Please sign in to comment.