Skip to content

Commit 7b802a6

Browse files
authored
Merge pull request kubernetes#119949 from rexagod/119697-1
metrics: add exemplar support for counters
2 parents 07e7368 + 460b847 commit 7b802a6

File tree

11 files changed

+160
-2
lines changed

11 files changed

+160
-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: 95 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,95 @@ 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+
SpanID: spanID,
306+
TraceID: traceID,
307+
TraceFlags: trace.FlagsSampled,
308+
}))
309+
toAdd := float64(40)
310+
311+
// Create contextual counter.
312+
counter := NewCounter(&CounterOpts{
313+
Name: "metric_exemplar_test",
314+
Help: "helpless",
315+
})
316+
_ = counter.WithContext(ctxForSpanCtx)
317+
318+
// Register counter.
319+
registry := newKubeRegistry(apimachineryversion.Info{
320+
Major: "1",
321+
Minor: "15",
322+
GitVersion: "v1.15.0-alpha-1.12345",
323+
})
324+
registry.MustRegister(counter)
325+
326+
// Call underlying exemplar methods.
327+
counter.Add(toAdd)
328+
counter.Inc()
329+
counter.Inc()
330+
331+
// Gather.
332+
mfs, err := registry.Gather()
333+
if err != nil {
334+
t.Fatalf("Gather failed %v", err)
335+
}
336+
if len(mfs) != 1 {
337+
t.Fatalf("Got %v metric families, Want: 1 metric family", len(mfs))
338+
}
339+
340+
// Verify metric type.
341+
mf := mfs[0]
342+
var m *dto.Metric
343+
switch mf.GetType() {
344+
case dto.MetricType_COUNTER:
345+
m = mfs[0].GetMetric()[0]
346+
default:
347+
t.Fatalf("Got %v metric type, Want: %v metric type", mf.GetType(), dto.MetricType_COUNTER)
348+
}
349+
350+
// Verify value.
351+
want := toAdd + 2
352+
got := m.GetCounter().GetValue()
353+
if got != want {
354+
t.Fatalf("Got %f, wanted %f as the count", got, want)
355+
}
356+
357+
// Verify exemplars.
358+
e := m.GetCounter().GetExemplar()
359+
if e == nil {
360+
t.Fatalf("Got nil exemplar, wanted an exemplar")
361+
}
362+
eLabels := e.GetLabel()
363+
if eLabels == nil {
364+
t.Fatalf("Got nil exemplar label, wanted an exemplar label")
365+
}
366+
if len(eLabels) != 2 {
367+
t.Fatalf("Got %v exemplar labels, wanted 2 exemplar labels", len(eLabels))
368+
}
369+
for _, l := range eLabels {
370+
switch *l.Name {
371+
case "trace_id":
372+
if *l.Value != traceID.String() {
373+
t.Fatalf("Got %s as traceID, wanted %s", *l.Value, traceID.String())
374+
}
375+
case "span_id":
376+
if *l.Value != spanID.String() {
377+
t.Fatalf("Got %s as spanID, wanted %s", *l.Value, spanID.String())
378+
}
379+
default:
380+
t.Fatalf("Got unexpected label %s", *l.Name)
381+
}
382+
}
383+
}

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() && maybeSpanCtx.IsSampled() {
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{}

staging/src/k8s.io/dynamic-resource-allocation/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ require (
5858
github.com/spf13/pflag v1.0.5 // indirect
5959
github.com/stoewer/go-strcase v1.3.0 // indirect
6060
github.com/x448/float16 v0.8.4 // indirect
61+
go.opentelemetry.io/otel v1.28.0 // indirect
62+
go.opentelemetry.io/otel/trace v1.28.0 // indirect
6163
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
6264
golang.org/x/net v0.30.0 // indirect
6365
golang.org/x/oauth2 v0.23.0 // indirect

staging/src/k8s.io/dynamic-resource-allocation/go.sum

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

staging/src/k8s.io/endpointslice/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ require (
4848
github.com/prometheus/procfs v0.15.1 // indirect
4949
github.com/spf13/pflag v1.0.5 // indirect
5050
github.com/x448/float16 v0.8.4 // indirect
51+
go.opentelemetry.io/otel v1.28.0 // indirect
52+
go.opentelemetry.io/otel/trace v1.28.0 // indirect
5153
golang.org/x/net v0.30.0 // indirect
5254
golang.org/x/oauth2 v0.23.0 // indirect
5355
golang.org/x/sys v0.26.0 // indirect

staging/src/k8s.io/endpointslice/go.sum

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

staging/src/k8s.io/kube-proxy/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ require (
3434
github.com/spf13/cobra v1.8.1 // indirect
3535
github.com/spf13/pflag v1.0.5 // indirect
3636
github.com/x448/float16 v0.8.4 // indirect
37+
go.opentelemetry.io/otel v1.28.0 // indirect
38+
go.opentelemetry.io/otel/trace v1.28.0 // indirect
3739
golang.org/x/net v0.30.0 // indirect
3840
golang.org/x/sys v0.26.0 // indirect
3941
golang.org/x/text v0.19.0 // indirect

staging/src/k8s.io/kube-proxy/go.sum

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

staging/src/k8s.io/kubelet/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ require (
4747
github.com/spf13/cobra v1.8.1 // indirect
4848
github.com/spf13/pflag v1.0.5 // indirect
4949
github.com/x448/float16 v0.8.4 // indirect
50+
go.opentelemetry.io/otel v1.28.0 // indirect
51+
go.opentelemetry.io/otel/trace v1.28.0 // indirect
5052
golang.org/x/net v0.30.0 // indirect
5153
golang.org/x/oauth2 v0.23.0 // indirect
5254
golang.org/x/sys v0.26.0 // indirect

0 commit comments

Comments
 (0)