Skip to content

Commit 47c21fa

Browse files
committed
metrics: add exemplar support for counters
adds exemplar support for counters * utilizes Prometheus' underlying exemplar machinery * introduces contextual counters (which were a no-op till now) * adds testcases addresses (a part of): kubernetes#119697
1 parent 55b83c9 commit 47c21fa

File tree

3 files changed

+143
-2
lines changed

3 files changed

+143
-2
lines changed

staging/src/k8s.io/component-base/metrics/counter.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
// Counter is our internal representation for our wrapping struct around prometheus
2828
// counters. Counter implements both kubeCollector and CounterMetric.
2929
type Counter struct {
30+
ctx context.Context
3031
CounterMetric
3132
*CounterOpts
3233
lazyMetric
@@ -36,6 +37,9 @@ type Counter struct {
3637
// The implementation of the Metric interface is expected by testutil.GetCounterMetricValue.
3738
var _ Metric = &Counter{}
3839

40+
// All supported exemplar metric types implement the metricWithExemplar interface.
41+
var _ metricWithExemplar = &Counter{}
42+
3943
// NewCounter returns an object which satisfies the kubeCollector and CounterMetric interfaces.
4044
// However, the object returned will not measure anything unless the collector is first
4145
// registered, since the metric is lazily instantiated.
@@ -93,11 +97,25 @@ func (c *Counter) initializeDeprecatedMetric() {
9397
c.initializeMetric()
9498
}
9599

96-
// WithContext allows the normal Counter metric to pass in context. The context is no-op now.
100+
// WithContext allows the normal Counter metric to pass in context.
97101
func (c *Counter) WithContext(ctx context.Context) CounterMetric {
102+
c.ctx = ctx
98103
return c.CounterMetric
99104
}
100105

106+
// withExemplar initializes the exemplarMetric object and sets the exemplar value.
107+
func (c *Counter) withExemplar(v float64) {
108+
(&exemplarMetric{c}).withExemplar(v)
109+
}
110+
111+
func (c *Counter) Add(v float64) {
112+
c.withExemplar(v)
113+
}
114+
115+
func (c *Counter) Inc() {
116+
c.withExemplar(1)
117+
}
118+
101119
// CounterVec is the internal representation of our wrapping struct around prometheus
102120
// counterVecs. CounterVec implements both kubeCollector and CounterVecMetric.
103121
type CounterVec struct {

staging/src/k8s.io/component-base/metrics/counter_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ package metrics
1818

1919
import (
2020
"bytes"
21+
"context"
2122
"testing"
2223

2324
"github.com/blang/semver/v4"
25+
dto "github.com/prometheus/client_model/go"
2426
"github.com/prometheus/common/expfmt"
2527
"github.com/stretchr/testify/assert"
2628
"github.com/stretchr/testify/require"
29+
"go.opentelemetry.io/otel/trace"
2730

2831
apimachineryversion "k8s.io/apimachinery/pkg/version"
2932
)
@@ -286,3 +289,94 @@ func TestCounterWithLabelValueAllowList(t *testing.T) {
286289
})
287290
}
288291
}
292+
293+
func TestCounterWithExemplar(t *testing.T) {
294+
// Set exemplar.
295+
fn := func(offset int) []byte {
296+
arr := make([]byte, 16)
297+
for i := 0; i < 16; i++ {
298+
arr[i] = byte(2<<7 - i - offset)
299+
}
300+
return arr
301+
}
302+
traceID := trace.TraceID(fn(1))
303+
spanID := trace.SpanID(fn(2))
304+
ctxForSpanCtx := trace.ContextWithSpanContext(context.Background(), trace.NewSpanContext(trace.SpanContextConfig{
305+
TraceID: traceID,
306+
SpanID: spanID,
307+
}))
308+
toAdd := float64(40)
309+
310+
// Create contextual counter.
311+
counter := NewCounter(&CounterOpts{
312+
Name: "metric_exemplar_test",
313+
Help: "helpless",
314+
})
315+
_ = counter.WithContext(ctxForSpanCtx)
316+
317+
// Register counter.
318+
registry := newKubeRegistry(apimachineryversion.Info{
319+
Major: "1",
320+
Minor: "15",
321+
GitVersion: "v1.15.0-alpha-1.12345",
322+
})
323+
registry.MustRegister(counter)
324+
325+
// Call underlying exemplar methods.
326+
counter.Add(toAdd)
327+
counter.Inc()
328+
counter.Inc()
329+
330+
// Gather.
331+
mfs, err := registry.Gather()
332+
if err != nil {
333+
t.Fatalf("Gather failed %v", err)
334+
}
335+
if len(mfs) != 1 {
336+
t.Fatalf("Got %v metric families, Want: 1 metric family", len(mfs))
337+
}
338+
339+
// Verify metric type.
340+
mf := mfs[0]
341+
var m *dto.Metric
342+
switch mf.GetType() {
343+
case dto.MetricType_COUNTER:
344+
m = mfs[0].GetMetric()[0]
345+
default:
346+
t.Fatalf("Got %v metric type, Want: %v metric type", mf.GetType(), dto.MetricType_COUNTER)
347+
}
348+
349+
// Verify value.
350+
want := toAdd + 2
351+
got := m.GetCounter().GetValue()
352+
if got != want {
353+
t.Fatalf("Got %f, wanted %f as the count", got, want)
354+
}
355+
356+
// Verify exemplars.
357+
e := m.GetCounter().GetExemplar()
358+
if e == nil {
359+
t.Fatalf("Got nil exemplar, wanted an exemplar")
360+
}
361+
eLabels := e.GetLabel()
362+
if eLabels == nil {
363+
t.Fatalf("Got nil exemplar label, wanted an exemplar label")
364+
}
365+
if len(eLabels) != 2 {
366+
t.Fatalf("Got %v exemplar labels, wanted 2 exemplar labels", len(eLabels))
367+
}
368+
for _, l := range eLabels {
369+
switch *l.Name {
370+
case "trace_id":
371+
if *l.Value != traceID.String() {
372+
t.Fatalf("Got %s as traceID, wanted %s", *l.Value, traceID.String())
373+
}
374+
case "span_id":
375+
if *l.Value != spanID.String() {
376+
t.Fatalf("Got %s as spanID, wanted %s", *l.Value, spanID.String())
377+
}
378+
default:
379+
t.Fatalf("Got unexpected label %s", *l.Name)
380+
}
381+
}
382+
}

staging/src/k8s.io/component-base/metrics/metric.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ package metrics
1919
import (
2020
"sync"
2121

22+
"go.opentelemetry.io/otel/trace"
23+
2224
"github.com/blang/semver/v4"
2325
"github.com/prometheus/client_golang/prometheus"
2426
dto "github.com/prometheus/client_model/go"
25-
promext "k8s.io/component-base/metrics/prometheusextension"
2627

28+
promext "k8s.io/component-base/metrics/prometheusextension"
2729
"k8s.io/klog/v2"
2830
)
2931

@@ -210,6 +212,33 @@ func (c *selfCollector) Collect(ch chan<- prometheus.Metric) {
210212
ch <- c.metric
211213
}
212214

215+
// metricWithExemplar is an interface that knows how to attach an exemplar to certain supported metric types.
216+
type metricWithExemplar interface {
217+
withExemplar(v float64)
218+
}
219+
220+
// exemplarMetric is a holds a context to extract exemplar labels from, and a metric to attach them to. It implements the metricWithExemplar interface.
221+
type exemplarMetric struct {
222+
*Counter
223+
}
224+
225+
// withExemplar attaches an exemplar to the metric.
226+
func (e *exemplarMetric) withExemplar(v float64) {
227+
if m, ok := e.CounterMetric.(prometheus.ExemplarAdder); ok {
228+
maybeSpanCtx := trace.SpanContextFromContext(e.ctx)
229+
if maybeSpanCtx.IsValid() {
230+
exemplarLabels := prometheus.Labels{
231+
"trace_id": maybeSpanCtx.TraceID().String(),
232+
"span_id": maybeSpanCtx.SpanID().String(),
233+
}
234+
m.AddWithExemplar(v, exemplarLabels)
235+
return
236+
}
237+
}
238+
239+
e.CounterMetric.Add(v)
240+
}
241+
213242
// no-op vecs for convenience
214243
var noopCounterVec = &prometheus.CounterVec{}
215244
var noopHistogramVec = &prometheus.HistogramVec{}

0 commit comments

Comments
 (0)