diff --git a/expfmt/decode.go b/expfmt/decode.go index 7b762370..8f8dc65d 100644 --- a/expfmt/decode.go +++ b/expfmt/decode.go @@ -220,7 +220,7 @@ func extractSamples(f *dto.MetricFamily, o *DecodeOptions) (model.Vector, error) return extractSummary(o, f), nil case dto.MetricType_UNTYPED: return extractUntyped(o, f), nil - case dto.MetricType_HISTOGRAM: + case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM: return extractHistogram(o, f), nil } return nil, fmt.Errorf("expfmt.extractSamples: unknown metric family type %v", f.GetType()) @@ -403,9 +403,13 @@ func extractHistogram(o *DecodeOptions, f *dto.MetricFamily) model.Vector { infSeen = true } + v := q.GetCumulativeCountFloat() + if v <= 0 { + v = float64(q.GetCumulativeCount()) + } samples = append(samples, &model.Sample{ Metric: model.Metric(lset), - Value: model.SampleValue(q.GetCumulativeCount()), + Value: model.SampleValue(v), Timestamp: timestamp, }) } @@ -428,9 +432,13 @@ func extractHistogram(o *DecodeOptions, f *dto.MetricFamily) model.Vector { } lset[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_count") + v := m.Histogram.GetSampleCountFloat() + if v <= 0 { + v = float64(m.Histogram.GetSampleCount()) + } count := &model.Sample{ Metric: model.Metric(lset), - Value: model.SampleValue(m.Histogram.GetSampleCount()), + Value: model.SampleValue(v), Timestamp: timestamp, } samples = append(samples, count) diff --git a/expfmt/openmetrics_create.go b/expfmt/openmetrics_create.go index 8dbf6d04..8c8bbaa6 100644 --- a/expfmt/openmetrics_create.go +++ b/expfmt/openmetrics_create.go @@ -208,6 +208,8 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E n, err = w.WriteString(" unknown\n") case dto.MetricType_HISTOGRAM: n, err = w.WriteString(" histogram\n") + case dto.MetricType_GAUGE_HISTOGRAM: + n, err = w.WriteString(" gaugehistogram\n") default: return written, fmt.Errorf("unknown metric type %s", metricType.String()) } @@ -325,7 +327,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E createdTsBytesWritten, err = writeOpenMetricsCreated(w, compliantName, "", metric, "", 0, metric.Summary.GetCreatedTimestamp()) n += createdTsBytesWritten } - case dto.MetricType_HISTOGRAM: + case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM: if metric.Histogram == nil { return written, fmt.Errorf( "expected histogram in metric %s %s", compliantName, metric, @@ -333,6 +335,12 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E } infSeen := false for _, b := range metric.Histogram.Bucket { + if b.GetCumulativeCountFloat() > 0 { + return written, fmt.Errorf( + "OpenMetrics v1.0 does not support float histogram %s %s", + compliantName, metric, + ) + } n, err = writeOpenMetricsSample( w, compliantName, "_bucket", metric, model.BucketLabel, b.GetUpperBound(), @@ -354,6 +362,9 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E 0, metric.Histogram.GetSampleCount(), true, nil, ) + // We do not check for a float sample count here + // because we will check for it below (and error + // out if needed). written += n if err != nil { return @@ -368,6 +379,12 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E if err != nil { return } + if metric.Histogram.GetSampleCountFloat() > 0 { + return written, fmt.Errorf( + "OpenMetrics v1.0 does not support float histogram %s %s", + compliantName, metric, + ) + } n, err = writeOpenMetricsSample( w, compliantName, "_count", metric, "", 0, 0, metric.Histogram.GetSampleCount(), true, diff --git a/expfmt/openmetrics_create_test.go b/expfmt/openmetrics_create_test.go index eb9a9d38..cd941f84 100644 --- a/expfmt/openmetrics_create_test.go +++ b/expfmt/openmetrics_create_test.go @@ -743,6 +743,50 @@ request_duration_microseconds_count 2693 # UNIT some_measure_seconds seconds some_measure_seconds_total{labelname="val1",basename="basevalue"} 42.0 some_measure_seconds_total{labelname="val2",basename="basevalue"} 0.23 1.23456789e+06 +`, + }, + // 11: Gauge histogram. + { + in: &dto.MetricFamily{ + Name: proto.String("name"), + Help: proto.String("doc string"), + Type: dto.MetricType_GAUGE_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(2693), + SampleSum: proto.Float64(1756047.3), + Bucket: []*dto.Bucket{ + { + UpperBound: proto.Float64(100), + CumulativeCount: proto.Uint64(123), + }, + { + UpperBound: proto.Float64(120), + CumulativeCount: proto.Uint64(412), + }, + { + UpperBound: proto.Float64(144), + CumulativeCount: proto.Uint64(592), + }, + { + UpperBound: proto.Float64(172.8), + CumulativeCount: proto.Uint64(1524), + }, + }, + }, + }, + }, + }, + out: `# HELP name doc string +# TYPE name gaugehistogram +name_bucket{le="100.0"} 123 +name_bucket{le="120.0"} 412 +name_bucket{le="144.0"} 592 +name_bucket{le="172.8"} 1524 +name_bucket{le="+Inf"} 2693 +name_sum 1.7560473e+06 +name_count 2693 `, }, } @@ -903,6 +947,47 @@ func TestOpenMetricsCreateError(t *testing.T) { }, err: "expected counter in metric", }, + // 2: Float histogram. + { + in: &dto.MetricFamily{ + Name: proto.String("name"), + Help: proto.String("doc string"), + Type: dto.MetricType_GAUGE_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + // Note that it is enough to fill the float fields even + // if the values are integers. + SampleCountFloat: proto.Float64(2693), + SampleSum: proto.Float64(1756047.3), + Bucket: []*dto.Bucket{ + { + UpperBound: proto.Float64(100), + CumulativeCountFloat: proto.Float64(123), + }, + { + UpperBound: proto.Float64(120), + CumulativeCountFloat: proto.Float64(412), + }, + { + UpperBound: proto.Float64(144), + CumulativeCountFloat: proto.Float64(592), + }, + { + UpperBound: proto.Float64(172.8), + CumulativeCountFloat: proto.Float64(1524), + }, + { + UpperBound: proto.Float64(math.Inf(+1)), + CumulativeCountFloat: proto.Float64(2693), + }, + }, + }, + }, + }, + }, + err: "OpenMetrics v1.0 does not support float histogram name histogram", + }, } for i, scenario := range scenarios { diff --git a/expfmt/text_create.go b/expfmt/text_create.go index c4e9c1bb..7e1d23ca 100644 --- a/expfmt/text_create.go +++ b/expfmt/text_create.go @@ -151,7 +151,10 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err e n, err = w.WriteString(" summary\n") case dto.MetricType_UNTYPED: n, err = w.WriteString(" untyped\n") - case dto.MetricType_HISTOGRAM: + case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM: + // The classic Prometheus text format has no notion of a gauge + // histogram. We render a gauge histogram in the same way as a + // regular histogram. n, err = w.WriteString(" histogram\n") default: return written, fmt.Errorf("unknown metric type %s", metricType.String()) @@ -223,7 +226,7 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err e w, name, "_count", metric, "", 0, float64(metric.Summary.GetSampleCount()), ) - case dto.MetricType_HISTOGRAM: + case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM: if metric.Histogram == nil { return written, fmt.Errorf( "expected histogram in metric %s %s", name, metric, @@ -231,10 +234,14 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err e } infSeen := false for _, b := range metric.Histogram.Bucket { + v := b.GetCumulativeCountFloat() + if v == 0 { + v = float64(b.GetCumulativeCount()) + } n, err = writeSample( w, name, "_bucket", metric, model.BucketLabel, b.GetUpperBound(), - float64(b.GetCumulativeCount()), + v, ) written += n if err != nil { @@ -245,10 +252,14 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err e } } if !infSeen { + v := metric.Histogram.GetSampleCountFloat() + if v == 0 { + v = float64(metric.Histogram.GetSampleCount()) + } n, err = writeSample( w, name, "_bucket", metric, model.BucketLabel, math.Inf(+1), - float64(metric.Histogram.GetSampleCount()), + v, ) written += n if err != nil { @@ -263,10 +274,11 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err e if err != nil { return } - n, err = writeSample( - w, name, "_count", metric, "", 0, - float64(metric.Histogram.GetSampleCount()), - ) + v := metric.Histogram.GetSampleCountFloat() + if v == 0 { + v = float64(metric.Histogram.GetSampleCount()) + } + n, err = writeSample(w, name, "_count", metric, "", 0, v) default: return written, fmt.Errorf( "unexpected type in metric %s %s", name, metric, diff --git a/expfmt/text_create_test.go b/expfmt/text_create_test.go index 967c46a2..dc214966 100644 --- a/expfmt/text_create_test.go +++ b/expfmt/text_create_test.go @@ -383,6 +383,186 @@ request_duration_microseconds_count 2693 out: `# HELP name doc string # TYPE name counter name -Inf +`, + }, + // 8: Float histogram with +Inf. + { + in: &dto.MetricFamily{ + Name: proto.String("name"), + Help: proto.String("doc string"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(2693.123), + SampleSum: proto.Float64(1756047.3), + Bucket: []*dto.Bucket{ + { + UpperBound: proto.Float64(100), + CumulativeCountFloat: proto.Float64(123.123), + }, + { + UpperBound: proto.Float64(120), + CumulativeCountFloat: proto.Float64(412), + }, + { + UpperBound: proto.Float64(144), + CumulativeCountFloat: proto.Float64(592), + }, + { + UpperBound: proto.Float64(172.8), + CumulativeCountFloat: proto.Float64(1524), + }, + { + UpperBound: proto.Float64(math.Inf(+1)), + CumulativeCountFloat: proto.Float64(2693.123), + }, + }, + }, + }, + }, + }, + out: `# HELP name doc string +# TYPE name histogram +name_bucket{le="100"} 123.123 +name_bucket{le="120"} 412 +name_bucket{le="144"} 592 +name_bucket{le="172.8"} 1524 +name_bucket{le="+Inf"} 2693.123 +name_sum 1.7560473e+06 +name_count 2693.123 +`, + }, + // 9: Float histogram without +Inf. + { + in: &dto.MetricFamily{ + Name: proto.String("name"), + Help: proto.String("doc string"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(2693.123), + SampleSum: proto.Float64(1756047.3), + Bucket: []*dto.Bucket{ + { + UpperBound: proto.Float64(100), + CumulativeCountFloat: proto.Float64(123.123), + }, + { + UpperBound: proto.Float64(120), + CumulativeCountFloat: proto.Float64(412), + }, + { + UpperBound: proto.Float64(144), + CumulativeCountFloat: proto.Float64(592), + }, + { + UpperBound: proto.Float64(172.8), + CumulativeCountFloat: proto.Float64(1524), + }, + }, + }, + }, + }, + }, + out: `# HELP name doc string +# TYPE name histogram +name_bucket{le="100"} 123.123 +name_bucket{le="120"} 412 +name_bucket{le="144"} 592 +name_bucket{le="172.8"} 1524 +name_bucket{le="+Inf"} 2693.123 +name_sum 1.7560473e+06 +name_count 2693.123 +`, + }, + // 10: Gauge histogram (rendered as regular histogram) + { + in: &dto.MetricFamily{ + Name: proto.String("name"), + Help: proto.String("doc string"), + Type: dto.MetricType_GAUGE_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(2693), + SampleSum: proto.Float64(1756047.3), + Bucket: []*dto.Bucket{ + { + UpperBound: proto.Float64(100), + CumulativeCount: proto.Uint64(123), + }, + { + UpperBound: proto.Float64(120), + CumulativeCount: proto.Uint64(412), + }, + { + UpperBound: proto.Float64(144), + CumulativeCount: proto.Uint64(592), + }, + { + UpperBound: proto.Float64(172.8), + CumulativeCount: proto.Uint64(1524), + }, + }, + }, + }, + }, + }, + out: `# HELP name doc string +# TYPE name histogram +name_bucket{le="100"} 123 +name_bucket{le="120"} 412 +name_bucket{le="144"} 592 +name_bucket{le="172.8"} 1524 +name_bucket{le="+Inf"} 2693 +name_sum 1.7560473e+06 +name_count 2693 +`, + }, + // 11: Gauge float histogram (rendered as regular histogram) + { + in: &dto.MetricFamily{ + Name: proto.String("name"), + Help: proto.String("doc string"), + Type: dto.MetricType_GAUGE_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(2693.123), + SampleSum: proto.Float64(1756047.3), + Bucket: []*dto.Bucket{ + { + UpperBound: proto.Float64(100), + CumulativeCountFloat: proto.Float64(123.123), + }, + { + UpperBound: proto.Float64(120), + CumulativeCountFloat: proto.Float64(412), + }, + { + UpperBound: proto.Float64(144), + CumulativeCountFloat: proto.Float64(592), + }, + { + UpperBound: proto.Float64(172.8), + CumulativeCountFloat: proto.Float64(1524), + }, + }, + }, + }, + }, + }, + out: `# HELP name doc string +# TYPE name histogram +name_bucket{le="100"} 123.123 +name_bucket{le="120"} 412 +name_bucket{le="144"} 592 +name_bucket{le="172.8"} 1524 +name_bucket{le="+Inf"} 2693.123 +name_sum 1.7560473e+06 +name_count 2693.123 `, }, } diff --git a/expfmt/text_parse.go b/expfmt/text_parse.go index f3888687..00c8841a 100644 --- a/expfmt/text_parse.go +++ b/expfmt/text_parse.go @@ -131,9 +131,44 @@ func (p *TextParser) TextToMetricFamilies(in io.Reader) (map[string]*dto.MetricF if p.err != nil && errors.Is(p.err, io.EOF) { p.parseError("unexpected end of input stream") } + for _, histogramMetric := range p.histograms { + normalizeHistogram(histogramMetric.GetHistogram()) + } return p.metricFamiliesByName, p.err } +// normalizeHistogram makes sure that all the buckets and the count in each +// histogram is either completely float or completely integer. +func normalizeHistogram(histogram *dto.Histogram) { + if histogram == nil { + return + } + anyFloats := false + if histogram.GetSampleCountFloat() != 0 { + anyFloats = true + } else { + for _, b := range histogram.GetBucket() { + if b.GetCumulativeCountFloat() != 0 { + anyFloats = true + break + } + } + } + if !anyFloats { + return + } + if histogram.GetSampleCountFloat() == 0 { + histogram.SampleCountFloat = proto.Float64(float64(histogram.GetSampleCount())) + histogram.SampleCount = nil + } + for _, b := range histogram.GetBucket() { + if b.GetCumulativeCountFloat() == 0 { + b.CumulativeCountFloat = proto.Float64(float64(b.GetCumulativeCount())) + b.CumulativeCount = nil + } + } +} + func (p *TextParser) reset(in io.Reader) { p.metricFamiliesByName = map[string]*dto.MetricFamily{} p.currentLabelPairs = nil @@ -283,7 +318,9 @@ func (p *TextParser) readingLabels() stateFn { // Summaries/histograms are special. We have to reset the // currentLabels map, currentQuantile and currentBucket before starting to // read labels. - if p.currentMF.GetType() == dto.MetricType_SUMMARY || p.currentMF.GetType() == dto.MetricType_HISTOGRAM { + if p.currentMF.GetType() == dto.MetricType_SUMMARY || + p.currentMF.GetType() == dto.MetricType_HISTOGRAM || + p.currentMF.GetType() == dto.MetricType_GAUGE_HISTOGRAM { p.currentLabels = map[string]string{} p.currentLabels[string(model.MetricNameLabel)] = p.currentMF.GetName() p.currentQuantile = math.NaN() @@ -376,7 +413,9 @@ func (p *TextParser) startLabelName() stateFn { // Special summary/histogram treatment. Don't add 'quantile' and 'le' // labels to 'real' labels. if (p.currentMF.GetType() != dto.MetricType_SUMMARY || p.currentLabelPair.GetName() != model.QuantileLabel) && - (p.currentMF.GetType() != dto.MetricType_HISTOGRAM || p.currentLabelPair.GetName() != model.BucketLabel) { + ((p.currentMF.GetType() != dto.MetricType_HISTOGRAM && + p.currentMF.GetType() != dto.MetricType_GAUGE_HISTOGRAM) || + p.currentLabelPair.GetName() != model.BucketLabel) { p.currentLabelPairs = append(p.currentLabelPairs, p.currentLabelPair) } // Check for duplicate label names. @@ -427,7 +466,7 @@ func (p *TextParser) startLabelValue() stateFn { } } // Similar special treatment of histograms. - if p.currentMF.GetType() == dto.MetricType_HISTOGRAM { + if p.currentMF.GetType() == dto.MetricType_HISTOGRAM || p.currentMF.GetType() == dto.MetricType_GAUGE_HISTOGRAM { if p.currentLabelPair.GetName() == model.BucketLabel { if p.currentBucket, p.err = parseFloat(p.currentLabelPair.GetValue()); p.err != nil { // Create a more helpful error message. @@ -478,7 +517,7 @@ func (p *TextParser) readingValue() stateFn { p.summaries[signature] = p.currentMetric p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric) } - case dto.MetricType_HISTOGRAM: + case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM: signature := model.LabelsToSignature(p.currentLabels) if histogram := p.histograms[signature]; histogram != nil { p.currentMetric = histogram @@ -524,24 +563,38 @@ func (p *TextParser) readingValue() stateFn { }, ) } - case dto.MetricType_HISTOGRAM: + case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM: // *sigh* if p.currentMetric.Histogram == nil { p.currentMetric.Histogram = &dto.Histogram{} } switch { case p.currentIsHistogramCount: - p.currentMetric.Histogram.SampleCount = proto.Uint64(uint64(value)) + if uintValue := uint64(value); value == float64(uintValue) { + p.currentMetric.Histogram.SampleCount = proto.Uint64(uintValue) + } else { + if value < 0 { + p.parseError(fmt.Sprintf("negative count for histogram %q", p.currentMF.GetName())) + return nil + } + p.currentMetric.Histogram.SampleCountFloat = proto.Float64(value) + } case p.currentIsHistogramSum: p.currentMetric.Histogram.SampleSum = proto.Float64(value) case !math.IsNaN(p.currentBucket): - p.currentMetric.Histogram.Bucket = append( - p.currentMetric.Histogram.Bucket, - &dto.Bucket{ - UpperBound: proto.Float64(p.currentBucket), - CumulativeCount: proto.Uint64(uint64(value)), - }, - ) + b := &dto.Bucket{ + UpperBound: proto.Float64(p.currentBucket), + } + if uintValue := uint64(value); value == float64(uintValue) { + b.CumulativeCount = proto.Uint64(uintValue) + } else { + if value < 0 { + p.parseError(fmt.Sprintf("negative bucket population for histogram %q", p.currentMF.GetName())) + return nil + } + b.CumulativeCountFloat = proto.Float64(value) + } + p.currentMetric.Histogram.Bucket = append(p.currentMetric.Histogram.Bucket, b) } default: p.err = fmt.Errorf("unexpected type for metric name %q", p.currentMF.GetName()) @@ -604,10 +657,18 @@ func (p *TextParser) readingType() stateFn { if p.readTokenUntilNewline(false); p.err != nil { return nil // Unexpected end of input. } - metricType, ok := dto.MetricType_value[strings.ToUpper(p.currentToken.String())] + typ := strings.ToUpper(p.currentToken.String()) // Tolerate any combination of upper and lower case. + metricType, ok := dto.MetricType_value[typ] // Tolerate "gauge_histogram" (not originally part of the text format). if !ok { - p.parseError(fmt.Sprintf("unknown metric type %q", p.currentToken.String())) - return nil + // We also want to tolerate "gaugehistogram" to mark a gauge + // histogram, because that string is used in OpenMetrics. Note, + // however, that gauge histograms do not officially exist in the + // classic text format. + if typ != "GAUGEHISTOGRAM" { + p.parseError(fmt.Sprintf("unknown metric type %q", p.currentToken.String())) + return nil + } + metricType = int32(dto.MetricType_GAUGE_HISTOGRAM) } p.currentMF.Type = dto.MetricType(metricType).Enum() return p.startOfLine @@ -857,7 +918,8 @@ func (p *TextParser) setOrCreateCurrentMF() { } histogramName := histogramMetricName(name) if p.currentMF = p.metricFamiliesByName[histogramName]; p.currentMF != nil { - if p.currentMF.GetType() == dto.MetricType_HISTOGRAM { + if p.currentMF.GetType() == dto.MetricType_HISTOGRAM || + p.currentMF.GetType() == dto.MetricType_GAUGE_HISTOGRAM { if isCount(name) { p.currentIsHistogramCount = true } diff --git a/expfmt/text_parse_test.go b/expfmt/text_parse_test.go index f9c6d4cd..edcee13a 100644 --- a/expfmt/text_parse_test.go +++ b/expfmt/text_parse_test.go @@ -656,6 +656,307 @@ request_duration_microseconds_count 2693 }, }, }, + // 12: A full float histogram. + { + in: ` +# HELP request_duration_microseconds The response latency. +# TYPE request_duration_microseconds histogram +request_duration_microseconds_bucket{le="100"} 123.5 +request_duration_microseconds_bucket{le="120"} 412.6 +request_duration_microseconds_bucket{le="144"} 592.7 +request_duration_microseconds_bucket{le="172.8"} 1524.8 +request_duration_microseconds_bucket{le="+Inf"} 2693.9 +request_duration_microseconds_sum 1.7560473e+06 +request_duration_microseconds_count 2693.9 +`, + out: []*dto.MetricFamily{ + { + Name: proto.String("request_duration_microseconds"), + Help: proto.String("The response latency."), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(2693.9), + SampleSum: proto.Float64(1756047.3), + Bucket: []*dto.Bucket{ + { + UpperBound: proto.Float64(100), + CumulativeCountFloat: proto.Float64(123.5), + }, + { + UpperBound: proto.Float64(120), + CumulativeCountFloat: proto.Float64(412.6), + }, + { + UpperBound: proto.Float64(144), + CumulativeCountFloat: proto.Float64(592.7), + }, + { + UpperBound: proto.Float64(172.8), + CumulativeCountFloat: proto.Float64(1524.8), + }, + { + UpperBound: proto.Float64(math.Inf(+1)), + CumulativeCountFloat: proto.Float64(2693.9), + }, + }, + }, + }, + }, + }, + }, + }, + // 13: A float histogram where only the count is really a float. + { + in: ` +# HELP request_duration_microseconds The response latency. +# TYPE request_duration_microseconds histogram +request_duration_microseconds_bucket{le="100"} 123 +request_duration_microseconds_bucket{le="120"} 412 +request_duration_microseconds_bucket{le="144"} 592 +request_duration_microseconds_bucket{le="172.8"} 1524 +request_duration_microseconds_sum 1.7560473e+06 +request_duration_microseconds_count 2693.9 +`, + out: []*dto.MetricFamily{ + { + Name: proto.String("request_duration_microseconds"), + Help: proto.String("The response latency."), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(2693.9), + SampleSum: proto.Float64(1756047.3), + Bucket: []*dto.Bucket{ + { + UpperBound: proto.Float64(100), + CumulativeCountFloat: proto.Float64(123), + }, + { + UpperBound: proto.Float64(120), + CumulativeCountFloat: proto.Float64(412), + }, + { + UpperBound: proto.Float64(144), + CumulativeCountFloat: proto.Float64(592), + }, + { + UpperBound: proto.Float64(172.8), + CumulativeCountFloat: proto.Float64(1524), + }, + }, + }, + }, + }, + }, + }, + }, + // 14: A float histogram where only one of the buckets is really a float. + { + in: ` +# HELP request_duration_microseconds The response latency. +# TYPE request_duration_microseconds histogram +request_duration_microseconds_bucket{le="100"} 123 +request_duration_microseconds_bucket{le="120"} 412 +request_duration_microseconds_bucket{le="144"} 592 +request_duration_microseconds_bucket{le="172.8"} 1524.8 +request_duration_microseconds_bucket{le="+Inf"} 2693 +request_duration_microseconds_sum 1.7560473e+06 +request_duration_microseconds_count 2693 +`, + out: []*dto.MetricFamily{ + { + Name: proto.String("request_duration_microseconds"), + Help: proto.String("The response latency."), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(2693), + SampleSum: proto.Float64(1756047.3), + Bucket: []*dto.Bucket{ + { + UpperBound: proto.Float64(100), + CumulativeCountFloat: proto.Float64(123), + }, + { + UpperBound: proto.Float64(120), + CumulativeCountFloat: proto.Float64(412), + }, + { + UpperBound: proto.Float64(144), + CumulativeCountFloat: proto.Float64(592), + }, + { + UpperBound: proto.Float64(172.8), + CumulativeCountFloat: proto.Float64(1524.8), + }, + { + UpperBound: proto.Float64(math.Inf(+1)), + CumulativeCountFloat: proto.Float64(2693), + }, + }, + }, + }, + }, + }, + }, + }, + // 15: A gauge histogram. + { + in: ` +# HELP request_duration_microseconds The response latency. +# TYPE request_duration_microseconds gaugehistogram +request_duration_microseconds_bucket{le="100"} 123 +request_duration_microseconds_bucket{le="120"} 412 +request_duration_microseconds_bucket{le="144"} 592 +request_duration_microseconds_bucket{le="172.8"} 1524 +request_duration_microseconds_bucket{le="+Inf"} 2693 +request_duration_microseconds_sum 1.7560473e+06 +request_duration_microseconds_count 2693 +`, + out: []*dto.MetricFamily{ + { + Name: proto.String("request_duration_microseconds"), + Help: proto.String("The response latency."), + Type: dto.MetricType_GAUGE_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(2693), + SampleSum: proto.Float64(1756047.3), + Bucket: []*dto.Bucket{ + { + UpperBound: proto.Float64(100), + CumulativeCount: proto.Uint64(123), + }, + { + UpperBound: proto.Float64(120), + CumulativeCount: proto.Uint64(412), + }, + { + UpperBound: proto.Float64(144), + CumulativeCount: proto.Uint64(592), + }, + { + UpperBound: proto.Float64(172.8), + CumulativeCount: proto.Uint64(1524), + }, + { + UpperBound: proto.Float64(math.Inf(+1)), + CumulativeCount: proto.Uint64(2693), + }, + }, + }, + }, + }, + }, + }, + }, + // 16: A gauge histogram with alternative spelling. + { + in: ` +# HELP request_duration_microseconds The response latency. +# TYPE request_duration_microseconds gauge_histogram +request_duration_microseconds_bucket{le="100"} 123 +request_duration_microseconds_bucket{le="120"} 412 +request_duration_microseconds_bucket{le="144"} 592 +request_duration_microseconds_bucket{le="172.8"} 1524 +request_duration_microseconds_bucket{le="+Inf"} 2693 +request_duration_microseconds_sum 1.7560473e+06 +request_duration_microseconds_count 2693 +`, + out: []*dto.MetricFamily{ + { + Name: proto.String("request_duration_microseconds"), + Help: proto.String("The response latency."), + Type: dto.MetricType_GAUGE_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(2693), + SampleSum: proto.Float64(1756047.3), + Bucket: []*dto.Bucket{ + { + UpperBound: proto.Float64(100), + CumulativeCount: proto.Uint64(123), + }, + { + UpperBound: proto.Float64(120), + CumulativeCount: proto.Uint64(412), + }, + { + UpperBound: proto.Float64(144), + CumulativeCount: proto.Uint64(592), + }, + { + UpperBound: proto.Float64(172.8), + CumulativeCount: proto.Uint64(1524), + }, + { + UpperBound: proto.Float64(math.Inf(+1)), + CumulativeCount: proto.Uint64(2693), + }, + }, + }, + }, + }, + }, + }, + }, + // 17: A float gauge histogram where only one of the buckets is really a float and with alternative spelling. + { + in: ` +# HELP request_duration_microseconds The response latency. +# TYPE request_duration_microseconds gauge_histogram +request_duration_microseconds_bucket{le="100"} 123 +request_duration_microseconds_bucket{le="120"} 412 +request_duration_microseconds_bucket{le="144"} 592 +request_duration_microseconds_bucket{le="172.8"} 1524.8 +request_duration_microseconds_bucket{le="+Inf"} 2693 +request_duration_microseconds_sum 1.7560473e+06 +request_duration_microseconds_count 2693 +`, + out: []*dto.MetricFamily{ + { + Name: proto.String("request_duration_microseconds"), + Help: proto.String("The response latency."), + Type: dto.MetricType_GAUGE_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(2693), + SampleSum: proto.Float64(1756047.3), + Bucket: []*dto.Bucket{ + { + UpperBound: proto.Float64(100), + CumulativeCountFloat: proto.Float64(123), + }, + { + UpperBound: proto.Float64(120), + CumulativeCountFloat: proto.Float64(412), + }, + { + UpperBound: proto.Float64(144), + CumulativeCountFloat: proto.Float64(592), + }, + { + UpperBound: proto.Float64(172.8), + CumulativeCountFloat: proto.Float64(1524.8), + }, + { + UpperBound: proto.Float64(math.Inf(+1)), + CumulativeCountFloat: proto.Float64(2693), + }, + }, + }, + }, + }, + }, + }, + }, } for i, scenario := range scenarios { @@ -801,31 +1102,31 @@ metric 4.12 `, errUTF8: `text format parsing error in line 3: second TYPE line for metric name "metric", or TYPE reported after samples`, }, - // 14: + // 15: { in: ` # TYPE metric bla `, errUTF8: "text format parsing error in line 2: unknown metric type", }, - // 15: + // 16: { in: ` # TYPE met-ric `, errUTF8: "text format parsing error in line 2: invalid metric name in comment", }, - // 16: + // 17: { in: `@invalidmetric{label="bla"} 3.14 2`, errUTF8: "text format parsing error in line 1: invalid metric name", }, - // 17: + // 18: { in: `{label="bla"} 3.14 2`, errUTF8: "text format parsing error in line 1: invalid metric name", }, - // 18: + // 19: { in: ` # TYPE metric histogram @@ -833,67 +1134,67 @@ metric_bucket{le="bla"} 3.14 `, errUTF8: "text format parsing error in line 3: expected float as value for 'le' label", }, - // 19: Invalid UTF-8 in label value. + // 20: Invalid UTF-8 in label value. { in: "metric{l=\"\xbd\"} 3.14\n", errUTF8: "text format parsing error in line 1: invalid label value \"\\xbd\"", }, - // 20: Go 1.13 sometimes allows underscores in numbers. + // 21: Go 1.13 sometimes allows underscores in numbers. { in: "foo 1_2\n", errUTF8: "text format parsing error in line 1: expected float as value", }, - // 21: Go 1.13 supports hex floating point. + // 22: Go 1.13 supports hex floating point. { in: "foo 0x1p-3\n", errUTF8: "text format parsing error in line 1: expected float as value", }, - // 22: Check for various other literals variants, just in case. + // 23: Check for various other literals variants, just in case. { in: "foo 0x1P-3\n", errUTF8: "text format parsing error in line 1: expected float as value", }, - // 23: + // 24: { in: "foo 0B1\n", errUTF8: "text format parsing error in line 1: expected float as value", }, - // 24: + // 25: { in: "foo 0O1\n", errUTF8: "text format parsing error in line 1: expected float as value", }, - // 25: + // 26: { in: "foo 0X1\n", errUTF8: "text format parsing error in line 1: expected float as value", }, - // 26: + // 27: { in: "foo 0x1\n", errUTF8: "text format parsing error in line 1: expected float as value", }, - // 27: + // 28: { in: "foo 0b1\n", errUTF8: "text format parsing error in line 1: expected float as value", }, - // 28: + // 29: { in: "foo 0o1\n", errUTF8: "text format parsing error in line 1: expected float as value", }, - // 29: + // 30: { in: "foo 0x1\n", errUTF8: "text format parsing error in line 1: expected float as value", }, - // 30: + // 31: { in: "foo 0x1\n", errUTF8: "text format parsing error in line 1: expected float as value", }, - // 31: Check histogram label. + // 32: Check histogram label. { in: ` # TYPE metric histogram @@ -901,7 +1202,7 @@ metric_bucket{le="0x1p-3"} 3.14 `, errUTF8: "text format parsing error in line 3: expected float as value for 'le' label", }, - // 32: Check quantile label. + // 33: Check quantile label. { in: ` # TYPE metric summary @@ -909,28 +1210,28 @@ metric{quantile="0x1p-3"} 3.14 `, errUTF8: "text format parsing error in line 3: expected float as value for 'quantile' label", }, - // 33: Check duplicate label. + // 34: Check duplicate label. { in: `metric{label="bla",label="bla"} 3.14`, errUTF8: "text format parsing error in line 1: duplicate label names for metric", }, - // 34: Multiple quoted metric names. + // 35: Multiple quoted metric names. { in: `{"one.name","another.name"} 3.14`, errUTF8: "text format parsing error in line 1: multiple metric names", errLegacy: `text format parsing error in line 1: invalid metric name "one.name"`, }, - // 35: Invalid escape sequence in quoted metric name. + // 36: Invalid escape sequence in quoted metric name. { in: `{"a\xc5z",label="bla"} 3.14`, errUTF8: "text format parsing error in line 1: invalid escape sequence", }, - // 36: Unexpected end of quoted metric name. + // 37: Unexpected end of quoted metric name. { in: `{"metric.name".label="bla"} 3.14`, errUTF8: "text format parsing error in line 1: unexpected end of metric name", }, - // 37: Invalid escape sequence in quoted metric name. + // 38: Invalid escape sequence in quoted metric name. { in: ` # TYPE "metric.name\t" counter @@ -938,7 +1239,7 @@ metric{quantile="0x1p-3"} 3.14 `, errUTF8: "text format parsing error in line 2: invalid escape sequence", }, - // 38: Newline in quoted metric name. + // 39: Newline in quoted metric name. { in: ` # TYPE "metric @@ -948,7 +1249,7 @@ name",label="bla"} 3.14 `, errUTF8: `text format parsing error in line 2: metric name "metric" contains unescaped new-line`, }, - // 39: Newline in quoted label name. + // 40: Newline in quoted label name. { in: ` {"metric.name","new @@ -957,19 +1258,80 @@ line"="bla"} 3.14 errUTF8: `text format parsing error in line 2: label name "new" contains unescaped new-line`, errLegacy: `text format parsing error in line 2: invalid metric name "metric.name"`, }, - // 40: dotted name fails legacy validation. + // 41: Dotted metric name fails legacy validation. { in: `{"metric.name",foo="bla"} 3.14 `, errUTF8: ``, errLegacy: `text format parsing error in line 1: invalid metric name "metric.name"`, }, + // 42: Dotted label name fails legacy validation. { in: `metric_name{"foo"="bar", "dotted.label"="bla"} 3.14 `, errUTF8: ``, errLegacy: `text format parsing error in line 1: invalid label name "dotted.label"`, }, + // 43: Histogram with negative count. + { + in: ` +# HELP request_duration_microseconds The response latency. +# TYPE request_duration_microseconds histogram +request_duration_microseconds_bucket{le="100"} 123 +request_duration_microseconds_bucket{le="120"} 412 +request_duration_microseconds_bucket{le="144"} 592 +request_duration_microseconds_bucket{le="172.8"} 1524 +request_duration_microseconds_bucket{le="+Inf"} 2693 +request_duration_microseconds_sum 1.7560473e+06 +request_duration_microseconds_count -2693 +`, + errUTF8: `text format parsing error in line 10: negative count for histogram "request_duration_microseconds"`, + }, + // 44: Histogram with negative bucket. + { + in: ` +# HELP request_duration_microseconds The response latency. +# TYPE request_duration_microseconds histogram +request_duration_microseconds_bucket{le="100"} 123 +request_duration_microseconds_bucket{le="120"} -412 +request_duration_microseconds_bucket{le="144"} 592 +request_duration_microseconds_bucket{le="172.8"} 1524 +request_duration_microseconds_bucket{le="+Inf"} 2693 +request_duration_microseconds_sum 1.7560473e+06 +request_duration_microseconds_count 2693 +`, + errUTF8: `text format parsing error in line 5: negative bucket population for histogram "request_duration_microseconds"`, + }, + // 45: Histogram with negative float count. + { + in: ` +# HELP request_duration_microseconds The response latency. +# TYPE request_duration_microseconds histogram +request_duration_microseconds_bucket{le="100"} 123 +request_duration_microseconds_bucket{le="120"} 412 +request_duration_microseconds_bucket{le="144"} 592 +request_duration_microseconds_bucket{le="172.8"} 1524 +request_duration_microseconds_bucket{le="+Inf"} 2693 +request_duration_microseconds_sum 1.7560473e+06 +request_duration_microseconds_count -2693.123 +`, + errUTF8: `text format parsing error in line 10: negative count for histogram "request_duration_microseconds"`, + }, + // 46: Histogram with negative float bucket. + { + in: ` +# HELP request_duration_microseconds The response latency. +# TYPE request_duration_microseconds histogram +request_duration_microseconds_bucket{le="100"} 123 +request_duration_microseconds_bucket{le="120"} -412.456 +request_duration_microseconds_bucket{le="144"} 592 +request_duration_microseconds_bucket{le="172.8"} 1524 +request_duration_microseconds_bucket{le="+Inf"} 2693 +request_duration_microseconds_sum 1.7560473e+06 +request_duration_microseconds_count 2693 +`, + errUTF8: `text format parsing error in line 5: negative bucket population for histogram "request_duration_microseconds"`, + }, } for i, scenario := range scenarios { parser.scheme = model.UTF8Validation