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

Added MustNewMetricWithExemplars that wraps metrics with exemplar #3

Merged
merged 1 commit into from Mar 16, 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
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?
wperron marked this conversation as resolved.
Show resolved Hide resolved
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
}
wperron marked this conversation as resolved.
Show resolved Hide resolved

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