mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
Add histogram exemplar support
This commit is contained in:
parent
d3fd5940e4
commit
d4210832f4
@ -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 {
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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{}
|
||||
|
Loading…
Reference in New Issue
Block a user