Add histogram exemplar support

This commit is contained in:
Richa Banker 2024-03-18 12:51:12 -07:00
parent d3fd5940e4
commit d4210832f4
4 changed files with 272 additions and 29 deletions

View File

@ -21,6 +21,8 @@ import (
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/trace"
dto "github.com/prometheus/client_model/go"
)
@ -40,6 +42,11 @@ var _ Metric = &Counter{}
// All supported exemplar metric types implement the metricWithExemplar interface.
var _ metricWithExemplar = &Counter{}
// exemplarCounterMetric holds a context to extract exemplar labels from, and a counter metric to attach them to. It implements the metricWithExemplar interface.
type exemplarCounterMetric struct {
*Counter
}
// NewCounter returns an object which satisfies the kubeCollector and CounterMetric interfaces.
// However, the object returned will not measure anything unless the collector is first
// registered, since the metric is lazily instantiated.
@ -105,7 +112,7 @@ func (c *Counter) WithContext(ctx context.Context) CounterMetric {
// withExemplar initializes the exemplarMetric object and sets the exemplar value.
func (c *Counter) withExemplar(v float64) {
(&exemplarMetric{c}).withExemplar(v)
(&exemplarCounterMetric{c}).withExemplar(v)
}
func (c *Counter) Add(v float64) {
@ -116,6 +123,23 @@ func (c *Counter) Inc() {
c.withExemplar(1)
}
// withExemplar attaches an exemplar to the metric.
func (e *exemplarCounterMetric) withExemplar(v float64) {
if m, ok := e.CounterMetric.(prometheus.ExemplarAdder); ok {
maybeSpanCtx := trace.SpanContextFromContext(e.ctx)
if maybeSpanCtx.IsValid() && maybeSpanCtx.IsSampled() {
exemplarLabels := prometheus.Labels{
"trace_id": maybeSpanCtx.TraceID().String(),
"span_id": maybeSpanCtx.SpanID().String(),
}
m.AddWithExemplar(v, exemplarLabels)
return
}
}
e.CounterMetric.Add(v)
}
// CounterVec is the internal representation of our wrapping struct around prometheus
// counterVecs. CounterVec implements both kubeCollector and CounterVecMetric.
type CounterVec struct {

View File

@ -21,17 +21,55 @@ import (
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/trace"
)
// Histogram is our internal representation for our wrapping struct around prometheus
// histograms. Summary implements both kubeCollector and ObserverMetric
type Histogram struct {
ctx context.Context
ObserverMetric
*HistogramOpts
lazyMetric
selfCollector
}
// exemplarHistogramMetric holds a context to extract exemplar labels from, and a historgram metric to attach them to. It implements the metricWithExemplar interface.
type exemplarHistogramMetric struct {
*Histogram
}
type exemplarHistogramVec struct {
*HistogramVecWithContext
observer prometheus.Observer
}
func (h *Histogram) Observe(v float64) {
h.withExemplar(v)
}
// withExemplar initializes the exemplarMetric object and sets the exemplar value.
func (h *Histogram) withExemplar(v float64) {
(&exemplarHistogramMetric{h}).withExemplar(v)
}
// withExemplar attaches an exemplar to the metric.
func (e *exemplarHistogramMetric) withExemplar(v float64) {
if m, ok := e.Histogram.ObserverMetric.(prometheus.ExemplarObserver); ok {
maybeSpanCtx := trace.SpanContextFromContext(e.ctx)
if maybeSpanCtx.IsValid() && maybeSpanCtx.IsSampled() {
exemplarLabels := prometheus.Labels{
"trace_id": maybeSpanCtx.TraceID().String(),
"span_id": maybeSpanCtx.SpanID().String(),
}
m.ObserveWithExemplar(v, exemplarLabels)
return
}
}
e.ObserverMetric.Observe(v)
}
// NewHistogram returns an object which is Histogram-like. However, nothing
// will be measured until the histogram is registered somewhere.
func NewHistogram(opts *HistogramOpts) *Histogram {
@ -74,6 +112,7 @@ func (h *Histogram) initializeDeprecatedMetric() {
// WithContext allows the normal Histogram metric to pass in context. The context is no-op now.
func (h *Histogram) WithContext(ctx context.Context) ObserverMetric {
h.ctx = ctx
return h.ObserverMetric
}
@ -216,12 +255,37 @@ type HistogramVecWithContext struct {
ctx context.Context
}
func (h *exemplarHistogramVec) Observe(v float64) {
h.withExemplar(v)
}
func (h *exemplarHistogramVec) withExemplar(v float64) {
if m, ok := h.observer.(prometheus.ExemplarObserver); ok {
maybeSpanCtx := trace.SpanContextFromContext(h.HistogramVecWithContext.ctx)
if maybeSpanCtx.IsValid() && maybeSpanCtx.IsSampled() {
m.ObserveWithExemplar(v, prometheus.Labels{
"trace_id": maybeSpanCtx.TraceID().String(),
"span_id": maybeSpanCtx.SpanID().String(),
})
return
}
}
h.observer.Observe(v)
}
// WithLabelValues is the wrapper of HistogramVec.WithLabelValues.
func (vc *HistogramVecWithContext) WithLabelValues(lvs ...string) ObserverMetric {
return vc.HistogramVec.WithLabelValues(lvs...)
func (vc *HistogramVecWithContext) WithLabelValues(lvs ...string) *exemplarHistogramVec {
return &exemplarHistogramVec{
HistogramVecWithContext: vc,
observer: vc.HistogramVec.WithLabelValues(lvs...),
}
}
// With is the wrapper of HistogramVec.With.
func (vc *HistogramVecWithContext) With(labels map[string]string) ObserverMetric {
return vc.HistogramVec.With(labels)
func (vc *HistogramVecWithContext) With(labels map[string]string) *exemplarHistogramVec {
return &exemplarHistogramVec{
HistogramVecWithContext: vc,
observer: vc.HistogramVec.With(labels),
}
}

View File

@ -17,12 +17,15 @@ limitations under the License.
package metrics
import (
"context"
"testing"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace"
apimachineryversion "k8s.io/apimachinery/pkg/version"
)
@ -313,3 +316,179 @@ func TestHistogramWithLabelValueAllowList(t *testing.T) {
})
}
}
func TestHistogramWithExemplar(t *testing.T) {
// Arrange.
traceID := trace.TraceID([]byte("trace-0000-xxxxx"))
spanID := trace.SpanID([]byte("span-0000-xxxxx"))
ctxForSpanCtx := trace.ContextWithSpanContext(context.Background(), trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID,
SpanID: spanID,
TraceFlags: trace.FlagsSampled,
}))
value := float64(10)
histogram := NewHistogram(&HistogramOpts{
Name: "histogram_exemplar_test",
Help: "helpless",
Buckets: []float64{100},
})
_ = histogram.WithContext(ctxForSpanCtx)
registry := newKubeRegistry(apimachineryversion.Info{
Major: "1",
Minor: "15",
GitVersion: "v1.15.0-alpha-1.12345",
})
registry.MustRegister(histogram)
// Act.
histogram.Observe(value)
// Assert.
mfs, err := registry.Gather()
if err != nil {
t.Fatalf("Gather failed %v", err)
}
if len(mfs) != 1 {
t.Fatalf("Got %v metric families, Want: 1 metric family", len(mfs))
}
mf := mfs[0]
var m *dto.Metric
switch mf.GetType() {
case dto.MetricType_HISTOGRAM:
m = mfs[0].GetMetric()[0]
default:
t.Fatalf("Got %v metric type, Want: %v metric type", mf.GetType(), dto.MetricType_COUNTER)
}
want := value
got := m.GetHistogram().GetSampleSum()
if got != want {
t.Fatalf("Got %f, wanted %f as the count", got, want)
}
buckets := m.GetHistogram().GetBucket()
if len(buckets) == 0 {
t.Fatalf("Got 0 buckets, wanted 1")
}
e := buckets[0].GetExemplar()
if e == nil {
t.Fatalf("Got nil exemplar, wanted an exemplar")
}
eLabels := e.GetLabel()
if eLabels == nil {
t.Fatalf("Got nil exemplar label, wanted an exemplar label")
}
if len(eLabels) != 2 {
t.Fatalf("Got %v exemplar labels, wanted 2 exemplar labels", len(eLabels))
}
for _, l := range eLabels {
switch *l.Name {
case "trace_id":
if *l.Value != traceID.String() {
t.Fatalf("Got %s as traceID, wanted %s", *l.Value, traceID.String())
}
case "span_id":
if *l.Value != spanID.String() {
t.Fatalf("Got %s as spanID, wanted %s", *l.Value, spanID.String())
}
default:
t.Fatalf("Got unexpected label %s", *l.Name)
}
}
}
func TestHistogramVecWithExemplar(t *testing.T) {
// Arrange.
traceID := trace.TraceID([]byte("trace-0000-xxxxx"))
spanID := trace.SpanID([]byte("span-0000-xxxxx"))
ctxForSpanCtx := trace.ContextWithSpanContext(context.Background(), trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID,
SpanID: spanID,
TraceFlags: trace.FlagsSampled,
}))
value := float64(10)
histogramVec := NewHistogramVec(&HistogramOpts{
Name: "histogram_exemplar_test",
Help: "helpless",
Buckets: []float64{100},
}, []string{"group"})
h := histogramVec.WithContext(ctxForSpanCtx)
registry := newKubeRegistry(apimachineryversion.Info{
Major: "1",
Minor: "15",
GitVersion: "v1.15.0-alpha-1.12345",
})
registry.MustRegister(histogramVec)
// Act.
h.WithLabelValues("foo").Observe(value)
// Assert.
mfs, err := registry.Gather()
if err != nil {
t.Fatalf("Gather failed %v", err)
}
if len(mfs) != 1 {
t.Fatalf("Got %v metric families, Want: 1 metric family", len(mfs))
}
mf := mfs[0]
var m *dto.Metric
switch mf.GetType() {
case dto.MetricType_HISTOGRAM:
m = mfs[0].GetMetric()[0]
default:
t.Fatalf("Got %v metric type, Want: %v metric type", mf.GetType(), dto.MetricType_COUNTER)
}
want := value
got := m.GetHistogram().GetSampleSum()
if got != want {
t.Fatalf("Got %f, wanted %f as the count", got, want)
}
buckets := m.GetHistogram().GetBucket()
if len(buckets) == 0 {
t.Fatalf("Got 0 buckets, wanted 1")
}
e := buckets[0].GetExemplar()
if e == nil {
t.Fatalf("Got nil exemplar, wanted an exemplar")
}
eLabels := e.GetLabel()
if eLabels == nil {
t.Fatalf("Got nil exemplar label, wanted an exemplar label")
}
if len(eLabels) != 2 {
t.Fatalf("Got %v exemplar labels, wanted 2 exemplar labels", len(eLabels))
}
for _, l := range eLabels {
switch *l.Name {
case "trace_id":
if *l.Value != traceID.String() {
t.Fatalf("Got %s as traceID, wanted %s", *l.Value, traceID.String())
}
case "span_id":
if *l.Value != spanID.String() {
t.Fatalf("Got %s as spanID, wanted %s", *l.Value, spanID.String())
}
default:
t.Fatalf("Got unexpected label %s", *l.Name)
}
}
}

View File

@ -19,8 +19,6 @@ package metrics
import (
"sync"
"go.opentelemetry.io/otel/trace"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
@ -217,28 +215,6 @@ type metricWithExemplar interface {
withExemplar(v float64)
}
// exemplarMetric is a holds a context to extract exemplar labels from, and a metric to attach them to. It implements the metricWithExemplar interface.
type exemplarMetric struct {
*Counter
}
// withExemplar attaches an exemplar to the metric.
func (e *exemplarMetric) withExemplar(v float64) {
if m, ok := e.CounterMetric.(prometheus.ExemplarAdder); ok {
maybeSpanCtx := trace.SpanContextFromContext(e.ctx)
if maybeSpanCtx.IsValid() && maybeSpanCtx.IsSampled() {
exemplarLabels := prometheus.Labels{
"trace_id": maybeSpanCtx.TraceID().String(),
"span_id": maybeSpanCtx.SpanID().String(),
}
m.AddWithExemplar(v, exemplarLabels)
return
}
}
e.CounterMetric.Add(v)
}
// no-op vecs for convenience
var noopCounterVec = &prometheus.CounterVec{}
var noopHistogramVec = &prometheus.HistogramVec{}