diff --git a/staging/src/k8s.io/component-base/metrics/counter.go b/staging/src/k8s.io/component-base/metrics/counter.go index 974b762f91f..3ed920c3ae3 100644 --- a/staging/src/k8s.io/component-base/metrics/counter.go +++ b/staging/src/k8s.io/component-base/metrics/counter.go @@ -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 { diff --git a/staging/src/k8s.io/component-base/metrics/histogram.go b/staging/src/k8s.io/component-base/metrics/histogram.go index 2ffbe3192bc..4f839634d11 100644 --- a/staging/src/k8s.io/component-base/metrics/histogram.go +++ b/staging/src/k8s.io/component-base/metrics/histogram.go @@ -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), + } } diff --git a/staging/src/k8s.io/component-base/metrics/histogram_test.go b/staging/src/k8s.io/component-base/metrics/histogram_test.go index d5fc24f7c7c..5efbfb6eeae 100644 --- a/staging/src/k8s.io/component-base/metrics/histogram_test.go +++ b/staging/src/k8s.io/component-base/metrics/histogram_test.go @@ -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) + } + } +} diff --git a/staging/src/k8s.io/component-base/metrics/metric.go b/staging/src/k8s.io/component-base/metrics/metric.go index 602772b56e7..c8b083995af 100644 --- a/staging/src/k8s.io/component-base/metrics/metric.go +++ b/staging/src/k8s.io/component-base/metrics/metric.go @@ -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{}