Path: blob/main/pkg/util/testappender/internal/dtobuilder/dtobuilder.go
4099 views
package dtobuilder12import (3"math"4"sort"5"strconv"6"time"78dto "github.com/prometheus/client_model/go"9"github.com/prometheus/common/model"10"github.com/prometheus/prometheus/model/exemplar"11"github.com/prometheus/prometheus/model/labels"12"github.com/prometheus/prometheus/model/metadata"13"github.com/prometheus/prometheus/model/textparse"14"google.golang.org/protobuf/types/known/timestamppb"15"k8s.io/utils/pointer"16)1718// Sample represents an individually written sample to a storage.Appender.19type Sample struct {20Labels labels.Labels21Timestamp int6422Value float6423PrintTimestamp bool24}2526// SeriesExemplar represents an individually written exemplar to a27// storage.Appender.28type SeriesExemplar struct {29// Labels is the labels of the series exposing the exemplar, not the labels30// on the exemplar itself.31Labels labels.Labels32Exemplar exemplar.Exemplar33}3435// Build converts a series of written samples, exemplars, and metadata into a36// slice of *dto.MetricFamily.37func Build(38samples map[string]Sample,39exemplars map[string]SeriesExemplar,40metadata map[string]metadata.Metadata,41) []*dto.MetricFamily {4243b := builder{44Samples: samples,45Exemplars: exemplars,46Metadata: metadata,4748familyLookup: make(map[string]*dto.MetricFamily),49}50return b.Build()51}5253type builder struct {54Samples map[string]Sample55Exemplars map[string]SeriesExemplar56Metadata map[string]metadata.Metadata5758families []*dto.MetricFamily59familyLookup map[string]*dto.MetricFamily60}6162// Build converts the dtoBuilder's Samples, Exemplars, and Metadata into a set63// of []*dto.MetricFamily.64func (b *builder) Build() []*dto.MetricFamily {65// *dto.MetricFamily represents a set of samples for a given family of66// metrics. All metrics with the same __name__ belong to the same family.67//68// Each *dto.MetricFamily has a set of *dto.Metric, which contain individual69// samples within that family. The *dto.Metric is where non-__name__ labels70// are kept.71//72// *dto.Metrics can represent counters, gauges, summaries, histograms, and73// untyped values.74//75// In the case of a summary, the *dto.Metric contains multiple samples,76// holding each quantile, the _count, and the _sum. Similarly for histograms,77// the *dto.Metric contains each bucket, the _count, and the _sum.78//79// Because *dto.Metrics for summaries and histograms contain multiple80// samples, Build must roll up individually recorded samples into the81// appropriate *dto.Metric. See buildMetricsFromSamples for more information.8283// We *must* do things in the following order:84//85// 1. Populate the families from metadata so we know what fields in86// *dto.Metric to set.87// 2. Populate *dto.Metric values from provided samples.88// 3. Assign exemplars to *dto.Metrics as appropriate.89b.buildFamiliesFromMetadata()90b.buildMetricsFromSamples()91b.injectExemplars()9293// Sort all the data before returning.94sortMetricFamilies(b.families)95return b.families96}9798// buildFamiliesFromMetadata populates the list of families based on the99// metadata known to the dtoBuilder. familyLookup will be updated for all100// metrics which map to the same family.101//102// In the case of summaries and histograms, multiple metrics map to the same103// family (the bucket/quantile, the _sum, and the _count metrics).104func (b *builder) buildFamiliesFromMetadata() {105for familyName, m := range b.Metadata {106mt := textParseToMetricType(m.Type)107mf := &dto.MetricFamily{108Name: pointer.String(familyName),109Type: &mt,110}111if m.Help != "" {112mf.Help = pointer.String(m.Help)113}114115b.families = append(b.families, mf)116117// Determine how to populate the lookup table.118switch mt {119case dto.MetricType_SUMMARY:120// Summaries include metrics with the family name (for quantiles),121// followed by _sum and _count suffixes.122b.familyLookup[familyName] = mf123b.familyLookup[familyName+"_sum"] = mf124b.familyLookup[familyName+"_count"] = mf125case dto.MetricType_HISTOGRAM:126// Histograms include metrics for _bucket, _sum, and _count suffixes.127b.familyLookup[familyName+"_bucket"] = mf128b.familyLookup[familyName+"_sum"] = mf129b.familyLookup[familyName+"_count"] = mf130default:131// Everything else matches the family name exactly.132b.familyLookup[familyName] = mf133}134}135}136137func textParseToMetricType(tp textparse.MetricType) dto.MetricType {138switch tp {139case textparse.MetricTypeCounter:140return dto.MetricType_COUNTER141case textparse.MetricTypeGauge:142return dto.MetricType_GAUGE143case textparse.MetricTypeHistogram:144return dto.MetricType_HISTOGRAM145case textparse.MetricTypeSummary:146return dto.MetricType_SUMMARY147default:148// There are other values for m.Type, but they're all149// OpenMetrics-specific and we're only converting into the Prometheus150// exposition format.151return dto.MetricType_UNTYPED152}153}154155// buildMetricsFromSamples populates *dto.Metrics. If the MetricFamily doesn't156// exist for a given sample, a new one is created.157func (b *builder) buildMetricsFromSamples() {158for _, sample := range b.Samples {159// Get or create the metric family.160metricName := sample.Labels.Get(model.MetricNameLabel)161mf := b.getOrCreateMetricFamily(metricName)162163// Retrieve the *dto.Metric based on labels.164m := getOrCreateMetric(mf, sample.Labels)165if sample.PrintTimestamp {166m.TimestampMs = pointer.Int64(sample.Timestamp)167}168169switch familyType(mf) {170case dto.MetricType_COUNTER:171m.Counter = &dto.Counter{172Value: pointer.Float64(sample.Value),173}174175case dto.MetricType_GAUGE:176m.Gauge = &dto.Gauge{177Value: pointer.Float64(sample.Value),178}179180case dto.MetricType_SUMMARY:181if m.Summary == nil {182m.Summary = &dto.Summary{}183}184185switch {186case metricName == mf.GetName()+"_count":187val := uint64(sample.Value)188m.Summary.SampleCount = &val189case metricName == mf.GetName()+"_sum":190m.Summary.SampleSum = pointer.Float64(sample.Value)191case metricName == mf.GetName():192quantile, err := strconv.ParseFloat(sample.Labels.Get(model.QuantileLabel), 64)193if err != nil {194continue195}196197m.Summary.Quantile = append(m.Summary.Quantile, &dto.Quantile{198Quantile: &quantile,199Value: pointer.Float64(sample.Value),200})201}202203case dto.MetricType_UNTYPED:204m.Untyped = &dto.Untyped{205Value: pointer.Float64(sample.Value),206}207208case dto.MetricType_HISTOGRAM:209if m.Histogram == nil {210m.Histogram = &dto.Histogram{}211}212213switch {214case metricName == mf.GetName()+"_count":215val := uint64(sample.Value)216m.Histogram.SampleCount = &val217case metricName == mf.GetName()+"_sum":218m.Histogram.SampleSum = pointer.Float64(sample.Value)219case metricName == mf.GetName()+"_bucket":220boundary, err := strconv.ParseFloat(sample.Labels.Get(model.BucketLabel), 64)221if err != nil {222continue223}224225count := uint64(sample.Value)226227m.Histogram.Bucket = append(m.Histogram.Bucket, &dto.Bucket{228UpperBound: &boundary,229CumulativeCount: &count,230})231}232}233}234}235236func (b *builder) getOrCreateMetricFamily(familyName string) *dto.MetricFamily {237mf, ok := b.familyLookup[familyName]238if ok {239return mf240}241242mt := dto.MetricType_UNTYPED243mf = &dto.MetricFamily{244Name: &familyName,245Type: &mt,246}247b.families = append(b.families, mf)248b.familyLookup[familyName] = mf249return mf250}251252func getOrCreateMetric(mf *dto.MetricFamily, l labels.Labels) *dto.Metric {253metricLabels := toLabelPairs(familyType(mf), l)254255for _, check := range mf.Metric {256if labelPairsEqual(check.Label, metricLabels) {257return check258}259}260261m := &dto.Metric{262Label: metricLabels,263}264mf.Metric = append(mf.Metric, m)265return m266}267268// toLabelPairs converts labels.Labels into []*dto.LabelPair. The __name__269// label is always dropped, since the metric name is retrieved from the family270// name instead.271//272// The quantile label is dropped for summaries, and the le label is dropped for273// histograms.274func toLabelPairs(mt dto.MetricType, ls labels.Labels) []*dto.LabelPair {275res := make([]*dto.LabelPair, 0, len(ls))276for _, l := range ls {277if l.Name == model.MetricNameLabel {278continue279} else if l.Name == model.QuantileLabel && mt == dto.MetricType_SUMMARY {280continue281} else if l.Name == model.BucketLabel && mt == dto.MetricType_HISTOGRAM {282continue283}284285res = append(res, &dto.LabelPair{286Name: pointer.String(l.Name),287Value: pointer.String(l.Value),288})289}290291sort.Slice(res, func(i, j int) bool {292switch {293case *res[i].Name < *res[j].Name:294return true295case *res[i].Value < *res[j].Value:296return true297default:298return false299}300})301return res302}303304func labelPairsEqual(a, b []*dto.LabelPair) bool {305if len(a) != len(b) {306return false307}308309for i := 0; i < len(a); i++ {310if *a[i].Name != *b[i].Name || *a[i].Value != *b[i].Value {311return false312}313}314315return true316}317318func familyType(mf *dto.MetricFamily) dto.MetricType {319ty := mf.Type320if ty == nil {321return dto.MetricType_UNTYPED322}323return *ty324}325326// injectExemplars populates the exemplars in the various *dto.Metric327// instances. Exemplars are ignored if the parent *dto.MetricFamily doesn't328// support exeplars based on metric type.329func (b *builder) injectExemplars() {330for _, e := range b.Exemplars {331// Get or create the metric family.332exemplarName := e.Labels.Get(model.MetricNameLabel)333334mf, ok := b.familyLookup[exemplarName]335if !ok {336// No metric family, which means no corresponding sample; ignore.337continue338}339340m := getMetric(mf, e.Labels)341if m == nil {342continue343}344345// Only counters and histograms support exemplars.346switch familyType(mf) {347case dto.MetricType_COUNTER:348if m.Counter == nil {349// Sample never added; ignore.350continue351}352m.Counter.Exemplar = convertExemplar(dto.MetricType_COUNTER, e.Exemplar)353case dto.MetricType_HISTOGRAM:354if m.Histogram == nil {355// Sample never added; ignore.356continue357}358359switch {360case exemplarName == mf.GetName()+"_bucket":361boundary, err := strconv.ParseFloat(e.Labels.Get(model.BucketLabel), 64)362if err != nil {363continue364}365bucket := findBucket(m.Histogram, boundary)366if bucket == nil {367continue368}369bucket.Exemplar = convertExemplar(dto.MetricType_HISTOGRAM, e.Exemplar)370}371}372}373}374375func getMetric(mf *dto.MetricFamily, l labels.Labels) *dto.Metric {376metricLabels := toLabelPairs(familyType(mf), l)377378for _, check := range mf.Metric {379if labelPairsEqual(check.Label, metricLabels) {380return check381}382}383384return nil385}386387func convertExemplar(mt dto.MetricType, e exemplar.Exemplar) *dto.Exemplar {388res := &dto.Exemplar{389Label: toLabelPairs(mt, e.Labels),390Value: &e.Value,391}392if e.HasTs {393res.Timestamp = timestamppb.New(time.UnixMilli(e.Ts))394}395return res396}397398func findBucket(h *dto.Histogram, bound float64) *dto.Bucket {399for _, b := range h.GetBucket() {400// If it's close enough, use the bucket.401if math.Abs(b.GetUpperBound()-bound) < 1e-9 {402return b403}404}405406return nil407}408409410