diff --git a/expfmt/openmetrics_2_0_create.go b/expfmt/openmetrics_2_0_create.go index 9f35aced..fda30b83 100644 --- a/expfmt/openmetrics_2_0_create.go +++ b/expfmt/openmetrics_2_0_create.go @@ -260,7 +260,7 @@ func writeOpenMetrics20Sample(w enhancedWriter, name string, metric *dto.Metric, } } - if exemplar != nil && len(exemplar.Label) > 0 && exemplar.Timestamp != nil { + if exemplar != nil && exemplar.Timestamp != nil { n, err = writeExemplar20(w, exemplar) written += n if err != nil { @@ -279,7 +279,7 @@ func writeOpenMetrics20Sample(w enhancedWriter, name string, metric *dto.Metric, // writeExemplar20 writes the provided exemplar in OpenMetrics 2.0 format to w. // In OpenMetrics 2.0, exemplars without a timestamp are dropped. func writeExemplar20(w enhancedWriter, e *dto.Exemplar) (int, error) { - if e == nil || len(e.Label) == 0 || e.Timestamp == nil { + if e == nil || e.Timestamp == nil { return 0, nil } if err := validateExemplar20(e); err != nil { @@ -291,7 +291,11 @@ func writeExemplar20(w enhancedWriter, e *dto.Exemplar) (int, error) { if err != nil { return written, err } - n, err = writeOpenMetricsNameAndLabelPairs(w, "", e.Label, "", 0) + if len(e.Label) == 0 { + n, err = w.WriteString("{}") + } else { + n, err = writeOpenMetricsNameAndLabelPairs(w, "", e.Label, "", 0) + } written += n if err != nil { return written, err @@ -342,7 +346,7 @@ func writeOpenMetrics20Timestamp(w enhancedWriter, f float64) (int, error) { } } -// Stubs for Summary and Histogram +// Stubs for Summary func writeCompositeSummary(w enhancedWriter, name string, metric *dto.Metric) (int, error) { _ = w @@ -352,11 +356,532 @@ func writeCompositeSummary(w enhancedWriter, name string, metric *dto.Metric) (i } func writeCompositeHistogram(w enhancedWriter, name string, metric *dto.Metric, isGauge bool) (int, error) { - _ = w - _ = name - _ = metric - _ = isGauge - return 0, errors.New("histogram not implemented yet") + h := metric.Histogram + if h == nil { + return 0, fmt.Errorf("expected histogram in metric %s", name) + } + + isNative := h.Schema != nil + hasClassicBuckets := len(h.Bucket) > 0 || !isNative + + if err := validateLabels20(metric.Label); err != nil { + return 0, err + } + if hasClassicBuckets { + for _, lp := range metric.Label { + if lp.GetName() == "le" { + return 0, fmt.Errorf("metric %s has classic buckets but label set contains %q label", name, "le") + } + } + } + + var isFloatCount bool + var sampleCountFloat float64 + var sampleCountUint uint64 + switch { + case h.SampleCountFloat != nil && (*h.SampleCountFloat > 0 || h.SampleCount == nil || isGauge): + isFloatCount = true + sampleCountFloat = *h.SampleCountFloat + if math.IsNaN(sampleCountFloat) { + if isGauge { + return 0, fmt.Errorf("gaugehistogram count cannot be NaN in metric %s", name) + } + return 0, fmt.Errorf("histogram count cannot be NaN in metric %s", name) + } + if !isGauge && sampleCountFloat < 0 { + return 0, fmt.Errorf("histogram count cannot be negative (%g) in metric %s", sampleCountFloat, name) + } + case h.SampleCount != nil: + sampleCountUint = *h.SampleCount + sampleCountFloat = float64(sampleCountUint) + } + + written := 0 + n, err := writeOpenMetricsNameAndLabelPairs(w, name, metric.Label, "", 0) + written += n + if err != nil { + return written, err + } + + n, err = w.WriteString(" {") + written += n + if err != nil { + return written, err + } + + if isGauge { + n, err = w.WriteString("gcount:") + } else { + n, err = w.WriteString("count:") + } + written += n + if err != nil { + return written, err + } + + if isFloatCount { + n, err = writeFloat(w, sampleCountFloat) + } else { + n, err = writeUint(w, sampleCountUint) + } + written += n + if err != nil { + return written, err + } + + if isGauge { + n, err = w.WriteString(",gsum:") + } else { + n, err = w.WriteString(",sum:") + } + written += n + if err != nil { + return written, err + } + n, err = writeFloat(w, h.GetSampleSum()) + written += n + if err != nil { + return written, err + } + + if isNative { + n, err = writeNativeBuckets(w, name, h, isGauge) + written += n + if err != nil { + return written, err + } + } + + var classicExemplars []*dto.Exemplar + if hasClassicBuckets { + n, err = writeClassicBuckets(w, name, h, sampleCountFloat, sampleCountUint, isFloatCount, isGauge, &classicExemplars) + written += n + if err != nil { + return written, err + } + } + + err = w.WriteByte('}') + written++ + if err != nil { + return written, err + } + + if metric.TimestampMs != nil { + err = w.WriteByte(' ') + written++ + if err != nil { + return written, err + } + n, err = writeOpenMetrics20Timestamp(w, float64(*metric.TimestampMs)/1000) + written += n + if err != nil { + return written, err + } + } + + if !isGauge && h.CreatedTimestamp != nil { + ts := h.CreatedTimestamp + if err := ts.CheckValid(); err != nil { + return written, fmt.Errorf("invalid created timestamp in metric %s: %w", name, err) + } + n, err = w.WriteString(" st@") + written += n + if err != nil { + return written, err + } + n, err = writeProtoTimestamp(w, ts) + written += n + if err != nil { + return written, err + } + } + + var exemplarsToEmit []*dto.Exemplar + if len(classicExemplars) > 0 { + exemplarsToEmit = classicExemplars + } else if len(h.Exemplars) > 0 { + exemplarsToEmit = h.Exemplars + } + + for _, e := range exemplarsToEmit { + if e == nil || e.Timestamp == nil { + continue + } + n, err = writeExemplar20(w, e) + written += n + if err != nil { + return written, err + } + } + + err = w.WriteByte('\n') + written++ + if err != nil { + return written, err + } + + return written, nil +} + +func writeNativeBuckets(w enhancedWriter, name string, h *dto.Histogram, isGauge bool) (int, error) { + schema := *h.Schema + if schema < -4 || schema > 8 { + return 0, fmt.Errorf("native histogram schema %d is out of range [-4, 8] in metric %s", schema, name) + } + + zeroThreshold := h.GetZeroThreshold() + if math.IsNaN(zeroThreshold) || math.IsInf(zeroThreshold, 0) || zeroThreshold < 0 { + return 0, fmt.Errorf("native histogram zero_threshold %g must be a non-negative, finite number in metric %s", zeroThreshold, name) + } + + var isFloatZeroCount bool + var zeroCountFloat float64 + var zeroCountUint uint64 + switch { + case h.ZeroCountFloat != nil && (*h.ZeroCountFloat > 0 || h.ZeroCount == nil || isGauge): + isFloatZeroCount = true + zeroCountFloat = *h.ZeroCountFloat + if math.IsNaN(zeroCountFloat) { + return 0, fmt.Errorf("native histogram zero_count cannot be NaN in metric %s", name) + } + if !isGauge && zeroCountFloat < 0 { + return 0, fmt.Errorf("native histogram zero_count cannot be negative (%g) in metric %s", zeroCountFloat, name) + } + case h.ZeroCount != nil: + zeroCountUint = *h.ZeroCount + zeroCountFloat = float64(zeroCountUint) + } + + written := 0 + n, err := w.WriteString(",schema:") + written += n + if err != nil { + return written, err + } + n, err = writeInt(w, int64(schema)) + written += n + if err != nil { + return written, err + } + + n, err = w.WriteString(",zero_threshold:") + written += n + if err != nil { + return written, err + } + n, err = writeFloat(w, zeroThreshold) + written += n + if err != nil { + return written, err + } + + n, err = w.WriteString(",zero_count:") + written += n + if err != nil { + return written, err + } + if isFloatZeroCount { + n, err = writeFloat(w, zeroCountFloat) + } else { + n, err = writeUint(w, zeroCountUint) + } + written += n + if err != nil { + return written, err + } + + n, err = writeSpansAndBuckets(w, name, "negative", h.NegativeSpan, h.NegativeDelta, h.NegativeCount, isGauge) + written += n + if err != nil { + return written, err + } + + n, err = writeSpansAndBuckets(w, name, "positive", h.PositiveSpan, h.PositiveDelta, h.PositiveCount, isGauge) + written += n + if err != nil { + return written, err + } + + return written, nil +} + +func writeSpansAndBuckets( + w enhancedWriter, + name string, + spanName string, + spans []*dto.BucketSpan, + deltas []int64, + floatCounts []float64, + isGauge bool, +) (int, error) { + isFloatBuckets := len(floatCounts) > 0 + var numBuckets int + if isFloatBuckets { + numBuckets = len(floatCounts) + } else { + numBuckets = len(deltas) + } + + var totalLength uint64 + for i, span := range spans { + if span == nil { + return 0, errors.New("expected non-nil bucket span") + } + if i > 0 && span.GetOffset() < 0 { + return 0, fmt.Errorf("subsequent %s span offset cannot be negative: %d in metric %s", spanName, span.GetOffset(), name) + } + totalLength += uint64(span.GetLength()) + } + + if numBuckets == 0 { + if totalLength == 0 { + return 0, nil + } + return 0, fmt.Errorf("sum of %s span lengths (%d) does not match bucket count (0) in metric %s", spanName, totalLength, name) + } + + if totalLength != uint64(numBuckets) { + return 0, fmt.Errorf("sum of %s span lengths (%d) does not match bucket count (%d) in metric %s", spanName, totalLength, numBuckets, name) + } + + if isFloatBuckets { + for _, v := range floatCounts { + if math.IsNaN(v) { + return 0, fmt.Errorf("%s bucket count cannot be NaN in metric %s", spanName, name) + } + if !isGauge && v < 0 { + return 0, fmt.Errorf("%s bucket count cannot be negative (%g) in metric %s", spanName, v, name) + } + } + } else { + var current int64 + for _, d := range deltas { + current += d + if !isGauge && current < 0 { + return 0, fmt.Errorf("%s bucket count cannot be negative (%d) in metric %s", spanName, current, name) + } + } + } + + written := 0 + n, err := w.WriteString("," + spanName + "_spans:[") + written += n + if err != nil { + return written, err + } + for i, span := range spans { + if i > 0 { + err = w.WriteByte(',') + written++ + if err != nil { + return written, err + } + } + n, err = writeInt(w, int64(span.GetOffset())) + written += n + if err != nil { + return written, err + } + err = w.WriteByte(':') + written++ + if err != nil { + return written, err + } + n, err = writeUint(w, uint64(span.GetLength())) + written += n + if err != nil { + return written, err + } + } + err = w.WriteByte(']') + written++ + if err != nil { + return written, err + } + + n, err = w.WriteString("," + spanName + "_buckets:[") + written += n + if err != nil { + return written, err + } + if isFloatBuckets { + for i, v := range floatCounts { + if i > 0 { + err = w.WriteByte(',') + written++ + if err != nil { + return written, err + } + } + n, err = writeFloat(w, v) + written += n + if err != nil { + return written, err + } + } + } else { + var current int64 + for i, d := range deltas { + if i > 0 { + err = w.WriteByte(',') + written++ + if err != nil { + return written, err + } + } + current += d + n, err = writeInt(w, current) + written += n + if err != nil { + return written, err + } + } + } + err = w.WriteByte(']') + written++ + if err != nil { + return written, err + } + + return written, nil +} + +func writeClassicBuckets( + w enhancedWriter, + name string, + h *dto.Histogram, + sampleCount float64, + sampleCountUint uint64, + isFloatCount bool, + isGauge bool, + collectedExemplars *[]*dto.Exemplar, +) (int, error) { + var infSeen bool + var prevBound float64 + for i, b := range h.Bucket { + if b == nil { + return 0, errors.New("expected non-nil bucket") + } + ub := b.GetUpperBound() + if math.IsNaN(ub) { + return 0, fmt.Errorf("classic bucket upper bound cannot be NaN in metric %s", name) + } + if i > 0 && ub <= prevBound { + return 0, fmt.Errorf("classic bucket upper bounds must be strictly increasing: %g <= %g in metric %s", ub, prevBound, name) + } + prevBound = ub + + var bCount float64 + switch { + case b.CumulativeCountFloat != nil && (*b.CumulativeCountFloat > 0 || b.CumulativeCount == nil || isGauge): + bCount = *b.CumulativeCountFloat + case b.CumulativeCount != nil: + bCount = float64(*b.CumulativeCount) + } + + if math.IsInf(ub, +1) { + if i != len(h.Bucket)-1 { + return 0, fmt.Errorf("+Inf bucket must be the last bucket in metric %s", name) + } + infSeen = true + if isFloatCount { + if bCount != sampleCount { + return 0, fmt.Errorf("classic bucket +Inf count (%g) does not match sample count (%g) in metric %s", bCount, sampleCount, name) + } + } else { + if b.CumulativeCount != nil && *b.CumulativeCount != sampleCountUint { + return 0, fmt.Errorf("classic bucket +Inf count (%d) does not match sample count (%d) in metric %s", *b.CumulativeCount, sampleCountUint, name) + } else if b.CumulativeCount == nil && bCount != sampleCount { + return 0, fmt.Errorf("classic bucket +Inf count (%g) does not match sample count (%g) in metric %s", bCount, sampleCount, name) + } + } + } + + if b.CumulativeCountFloat != nil { + c := *b.CumulativeCountFloat + if math.IsNaN(c) { + return 0, fmt.Errorf("classic bucket count cannot be NaN in metric %s", name) + } + if !isGauge && c < 0 { + return 0, fmt.Errorf("classic bucket count cannot be negative (%g) in metric %s", c, name) + } + } + } + + written := 0 + n, err := w.WriteString(",bucket:[") + written += n + if err != nil { + return written, err + } + + for i, b := range h.Bucket { + if i > 0 { + err = w.WriteByte(',') + written++ + if err != nil { + return written, err + } + } + n, err = writeFloat(w, b.GetUpperBound()) + written += n + if err != nil { + return written, err + } + err = w.WriteByte(':') + written++ + if err != nil { + return written, err + } + switch { + case b.CumulativeCountFloat != nil && (*b.CumulativeCountFloat > 0 || b.CumulativeCount == nil || isGauge): + n, err = writeFloat(w, *b.CumulativeCountFloat) + case b.CumulativeCount != nil: + n, err = writeUint(w, *b.CumulativeCount) + default: + n, err = writeUint(w, 0) + } + written += n + if err != nil { + return written, err + } + if b.Exemplar != nil && b.Exemplar.Timestamp != nil { + *collectedExemplars = append(*collectedExemplars, b.Exemplar) + } + } + + if !infSeen { + if len(h.Bucket) > 0 { + err = w.WriteByte(',') + written++ + if err != nil { + return written, err + } + } + n, err = w.WriteString("+Inf:") + written += n + if err != nil { + return written, err + } + if isFloatCount { + n, err = writeFloat(w, sampleCount) + } else { + n, err = writeUint(w, sampleCountUint) + } + written += n + if err != nil { + return written, err + } + } + + err = w.WriteByte(']') + written++ + if err != nil { + return written, err + } + + return written, nil } func validateLabels20(labels []*dto.LabelPair) error { diff --git a/expfmt/openmetrics_2_0_create_test.go b/expfmt/openmetrics_2_0_create_test.go index 6de60358..9df96e2c 100644 --- a/expfmt/openmetrics_2_0_create_test.go +++ b/expfmt/openmetrics_2_0_create_test.go @@ -161,6 +161,27 @@ node_memory_active_bytes 1.2345e+09 1234567890 }, out: `# TYPE http_requests_total counter http_requests_total 1027 1234567891 st@1234567890 # {trace_id="1234"} 1 1234567890.5 +`, + }, + { + name: "CounterWithExemplarWithoutLabels", + in: &dto.MetricFamily{ + Name: proto.String("http_requests_total"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + { + Counter: &dto.Counter{ + Value: proto.Float64(1027), + Exemplar: &dto.Exemplar{ + Value: proto.Float64(1), + Timestamp: ×tamppb.Timestamp{Seconds: 1234567890, Nanos: 500000000}, + }, + }, + }, + }, + }, + out: `# TYPE http_requests_total counter +http_requests_total 1027 # {} 1 1234567890.5 `, }, { @@ -262,6 +283,460 @@ http_requests_total 1027 }, out: `# TYPE "你好_total" counter {"你好_total","🌎"="🌍"} 1027 +`, + }, + { + name: "ClassicHistogram", + in: &dto.MetricFamily{ + Name: proto.String("request_duration_seconds"), + Help: proto.String("Request duration histogram."), + Type: dto.MetricType_HISTOGRAM.Enum(), + Unit: proto.String("seconds"), + Metric: []*dto.Metric{ + { + Label: []*dto.LabelPair{ + {Name: proto.String("handler"), Value: proto.String("query")}, + }, + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(3), + SampleSum: proto.Float64(6.0), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(0.1), CumulativeCount: proto.Uint64(1)}, + {UpperBound: proto.Float64(1.0), CumulativeCount: proto.Uint64(2)}, + {UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(3)}, + }, + }, + }, + }, + }, + out: `# HELP request_duration_seconds Request duration histogram. +# TYPE request_duration_seconds histogram +# UNIT request_duration_seconds seconds +request_duration_seconds{handler="query"} {count:3,sum:6,bucket:[0.1:1,1:2,+Inf:3]} +`, + }, + { + name: "ClassicHistogram_ImplicitPosInf", + in: &dto.MetricFamily{ + Name: proto.String("request_duration_seconds"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(2), + SampleSum: proto.Float64(1.5), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(0.1), CumulativeCount: proto.Uint64(1)}, + {UpperBound: proto.Float64(1.0), CumulativeCount: proto.Uint64(2)}, + }, + }, + }, + }, + }, + out: `# TYPE request_duration_seconds histogram +request_duration_seconds {count:2,sum:1.5,bucket:[0.1:1,1:2,+Inf:2]} +`, + }, + { + name: "ClassicHistogram_LargeCount_ImplicitPosInf", + in: &dto.MetricFamily{ + Name: proto.String("request_duration_seconds"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(9007199254740993), + SampleSum: proto.Float64(1.5), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(0.1), CumulativeCount: proto.Uint64(1)}, + }, + }, + }, + }, + }, + out: `# TYPE request_duration_seconds histogram +request_duration_seconds {count:9007199254740993,sum:1.5,bucket:[0.1:1,+Inf:9007199254740993]} +`, + }, + { + name: "ClassicHistogram_NegativeThresholds", + in: &dto.MetricFamily{ + Name: proto.String("temperature_deviation"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(10), + SampleSum: proto.Float64(15.0), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(math.Inf(-1)), CumulativeCount: proto.Uint64(0)}, + {UpperBound: proto.Float64(-1.0), CumulativeCount: proto.Uint64(2)}, + {UpperBound: proto.Float64(0.5), CumulativeCount: proto.Uint64(5)}, + {UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(10)}, + }, + }, + }, + }, + }, + out: `# TYPE temperature_deviation histogram +temperature_deviation {count:10,sum:15,bucket:[-Inf:0,-1:2,0.5:5,+Inf:10]} +`, + }, + { + name: "ClassicHistogram_FloatCounts", + in: &dto.MetricFamily{ + Name: proto.String("request_duration_seconds"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(5.5), + SampleSum: proto.Float64(12.1), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(0.1), CumulativeCountFloat: proto.Float64(2.5)}, + {UpperBound: proto.Float64(1.0), CumulativeCountFloat: proto.Float64(5.5)}, + {UpperBound: proto.Float64(math.Inf(+1)), CumulativeCountFloat: proto.Float64(5.5)}, + }, + }, + }, + }, + }, + out: `# TYPE request_duration_seconds histogram +request_duration_seconds {count:5.5,sum:12.1,bucket:[0.1:2.5,1:5.5,+Inf:5.5]} +`, + }, + { + name: "ClassicHistogram_WithTimestampsAndExemplars", + in: &dto.MetricFamily{ + Name: proto.String("request_duration_seconds"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(17), + SampleSum: proto.Float64(324789.3), + CreatedTimestamp: ×tamppb.Timestamp{Seconds: 1520879607, Nanos: 789000000}, + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(0.01), CumulativeCount: proto.Uint64(0)}, + { + UpperBound: proto.Float64(0.1), + CumulativeCount: proto.Uint64(8), + Exemplar: &dto.Exemplar{ + Value: proto.Float64(0.054), + Timestamp: ×tamppb.Timestamp{Seconds: 1520879607, Nanos: 700000000}, + }, + }, + { + UpperBound: proto.Float64(1.0), + CumulativeCount: proto.Uint64(11), + Exemplar: &dto.Exemplar{ + Label: []*dto.LabelPair{ + {Name: proto.String("trace_id"), Value: proto.String("KOO5S4vxi0o")}, + }, + Value: proto.Float64(1.67), + Timestamp: ×tamppb.Timestamp{Seconds: 1520879602, Nanos: 890000000}, + }, + }, + { + UpperBound: proto.Float64(10.0), + CumulativeCount: proto.Uint64(17), + Exemplar: &dto.Exemplar{ + Label: []*dto.LabelPair{ + {Name: proto.String("trace_id"), Value: proto.String("oHg5SJYRHA0")}, + }, + Value: proto.Float64(9.8), + Timestamp: ×tamppb.Timestamp{Seconds: 1520879607, Nanos: 789000000}, + }, + }, + {UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(17)}, + }, + }, + TimestampMs: proto.Int64(1520879610000), + }, + }, + }, + out: `# TYPE request_duration_seconds histogram +request_duration_seconds {count:17,sum:324789.3,bucket:[0.01:0,0.1:8,1:11,10:17,+Inf:17]} 1520879610 st@1520879607.789 # {} 0.054 1520879607.7 # {trace_id="KOO5S4vxi0o"} 1.67 1520879602.89 # {trace_id="oHg5SJYRHA0"} 9.8 1520879607.789 +`, + }, + { + name: "NativeHistogram_PositiveSpans", + in: &dto.MetricFamily{ + Name: proto.String("latency_seconds"), + Help: proto.String("Service latency (native histogram)."), + Type: dto.MetricType_HISTOGRAM.Enum(), + Unit: proto.String("seconds"), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(5), + SampleSum: proto.Float64(12.1), + Schema: proto.Int32(0), + ZeroThreshold: proto.Float64(0.001), + ZeroCount: proto.Uint64(2), + PositiveSpan: []*dto.BucketSpan{ + {Offset: proto.Int32(0), Length: proto.Uint32(3)}, + }, + PositiveDelta: []int64{1, 0, 0}, + }, + }, + }, + }, + out: `# HELP latency_seconds Service latency (native histogram). +# TYPE latency_seconds histogram +# UNIT latency_seconds seconds +latency_seconds {count:5,sum:12.1,schema:0,zero_threshold:0.001,zero_count:2,positive_spans:[0:3],positive_buckets:[1,1,1]} +`, + }, + { + name: "NativeHistogram_NegativeAndPositiveSpans_WithExemplars", + in: &dto.MetricFamily{ + Name: proto.String("acme_http_request_seconds"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Label: []*dto.LabelPair{ + {Name: proto.String("path"), Value: proto.String("/api/v1")}, + {Name: proto.String("method"), Value: proto.String("GET")}, + }, + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(59), + SampleSum: proto.Float64(120.0), + Schema: proto.Int32(7), + ZeroThreshold: proto.Float64(1e-4), + ZeroCount: proto.Uint64(0), + NegativeSpan: []*dto.BucketSpan{ + {Offset: proto.Int32(1), Length: proto.Uint32(2)}, + }, + NegativeDelta: []int64{5, 2}, + PositiveSpan: []*dto.BucketSpan{ + {Offset: proto.Int32(-1), Length: proto.Uint32(2)}, + {Offset: proto.Int32(3), Length: proto.Uint32(4)}, + }, + PositiveDelta: []int64{5, 2, 3, -1, -1, 0}, + Exemplars: []*dto.Exemplar{ + { + Label: []*dto.LabelPair{ + {Name: proto.String("trace_id"), Value: proto.String("shaZ8oxi")}, + }, + Value: proto.Float64(0.67), + Timestamp: ×tamppb.Timestamp{Seconds: 1520879607, Nanos: 789000000}, + }, + }, + }, + }, + }, + }, + out: `# TYPE acme_http_request_seconds histogram +acme_http_request_seconds{path="/api/v1",method="GET"} {count:59,sum:120,schema:7,zero_threshold:0.0001,zero_count:0,negative_spans:[1:2],negative_buckets:[5,7],positive_spans:[-1:2,3:4],positive_buckets:[5,7,10,9,8,8]} # {trace_id="shaZ8oxi"} 0.67 1520879607.789 +`, + }, + { + name: "NativeHistogram_ZeroObservations", + in: &dto.MetricFamily{ + Name: proto.String("acme_http_request_seconds"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Label: []*dto.LabelPair{ + {Name: proto.String("path"), Value: proto.String("/api/v1")}, + {Name: proto.String("method"), Value: proto.String("GET")}, + }, + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(0), + SampleSum: proto.Float64(0), + Schema: proto.Int32(3), + ZeroThreshold: proto.Float64(1e-4), + ZeroCount: proto.Uint64(0), + }, + }, + }, + }, + out: `# TYPE acme_http_request_seconds histogram +acme_http_request_seconds{path="/api/v1",method="GET"} {count:0,sum:0,schema:3,zero_threshold:0.0001,zero_count:0} +`, + }, + { + name: "NativeFloatHistogram", + in: &dto.MetricFamily{ + Name: proto.String("payload_size"), + Help: proto.String("Payload size (float native histogram)."), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(5.5), + SampleSum: proto.Float64(12.1), + Schema: proto.Int32(0), + ZeroThreshold: proto.Float64(0.001), + ZeroCountFloat: proto.Float64(2.5), + PositiveSpan: []*dto.BucketSpan{ + {Offset: proto.Int32(0), Length: proto.Uint32(2)}, + }, + PositiveCount: []float64{2.0, 1.0}, + }, + }, + }, + }, + out: `# HELP payload_size Payload size (float native histogram). +# TYPE payload_size histogram +payload_size {count:5.5,sum:12.1,schema:0,zero_threshold:0.001,zero_count:2.5,positive_spans:[0:2],positive_buckets:[2,1]} +`, + }, + { + name: "DualHistogram_NativeAndClassic", + in: &dto.MetricFamily{ + Name: proto.String("acme_http_request_seconds"), + Help: proto.String("Latency histogram of all of ACME's HTTP requests."), + Type: dto.MetricType_HISTOGRAM.Enum(), + Unit: proto.String("seconds"), + Metric: []*dto.Metric{ + { + Label: []*dto.LabelPair{ + {Name: proto.String("path"), Value: proto.String("/api/v1")}, + {Name: proto.String("method"), Value: proto.String("GET")}, + }, + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(2), + SampleSum: proto.Float64(120.0), + Schema: proto.Int32(0), + ZeroThreshold: proto.Float64(1e-4), + ZeroCount: proto.Uint64(0), + PositiveSpan: []*dto.BucketSpan{ + {Offset: proto.Int32(1), Length: proto.Uint32(2)}, + }, + PositiveDelta: []int64{1, 0}, + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(0.5), CumulativeCount: proto.Uint64(1)}, + {UpperBound: proto.Float64(1.0), CumulativeCount: proto.Uint64(2)}, + {UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(2)}, + }, + }, + }, + }, + }, + out: `# HELP acme_http_request_seconds Latency histogram of all of ACME's HTTP requests. +# TYPE acme_http_request_seconds histogram +# UNIT acme_http_request_seconds seconds +acme_http_request_seconds{path="/api/v1",method="GET"} {count:2,sum:120,schema:0,zero_threshold:0.0001,zero_count:0,positive_spans:[1:2],positive_buckets:[1,1],bucket:[0.5:1,1:2,+Inf:2]} +`, + }, + { + name: "GaugeHistogram_Classic", + in: &dto.MetricFamily{ + Name: proto.String("foo"), + Type: dto.MetricType_GAUGE_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(42), + SampleSum: proto.Float64(3289.3), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(0.01), CumulativeCount: proto.Uint64(20)}, + {UpperBound: proto.Float64(0.1), CumulativeCount: proto.Uint64(25)}, + {UpperBound: proto.Float64(1.0), CumulativeCount: proto.Uint64(34)}, + {UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(42)}, + }, + }, + }, + }, + }, + out: `# TYPE foo gaugehistogram +foo {gcount:42,gsum:3289.3,bucket:[0.01:20,0.1:25,1:34,+Inf:42]} +`, + }, + { + name: "GaugeHistogram_NativeFloat", + in: &dto.MetricFamily{ + Name: proto.String("acme_http_request_seconds:rate5m"), + Type: dto.MetricType_GAUGE_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Label: []*dto.LabelPair{ + {Name: proto.String("path"), Value: proto.String("/api/v1")}, + {Name: proto.String("method"), Value: proto.String("GET")}, + }, + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(0.01), + SampleSum: proto.Float64(2.0), + Schema: proto.Int32(0), + ZeroThreshold: proto.Float64(1e-4), + ZeroCountFloat: proto.Float64(0.0), + PositiveSpan: []*dto.BucketSpan{ + {Offset: proto.Int32(1), Length: proto.Uint32(2)}, + }, + PositiveCount: []float64{0.005, 0.005}, + }, + }, + }, + }, + out: `# TYPE acme_http_request_seconds:rate5m gaugehistogram +acme_http_request_seconds:rate5m{path="/api/v1",method="GET"} {gcount:0.01,gsum:2,schema:0,zero_threshold:0.0001,zero_count:0,positive_spans:[1:2],positive_buckets:[0.005,0.005]} +`, + }, + { + name: "GaugeHistogram_NegativeValues", + in: &dto.MetricFamily{ + Name: proto.String("net_flow_rate"), + Type: dto.MetricType_GAUGE_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(-1.5), + SampleSum: proto.Float64(-3.2), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(-0.5), CumulativeCountFloat: proto.Float64(-2.0)}, + {UpperBound: proto.Float64(math.Inf(+1)), CumulativeCountFloat: proto.Float64(-1.5)}, + }, + }, + }, + }, + }, + out: `# TYPE net_flow_rate gaugehistogram +net_flow_rate {gcount:-1.5,gsum:-3.2,bucket:[-0.5:-2,+Inf:-1.5]} +`, + }, + { + name: "ClassicHistogram_EmptyBuckets", + in: &dto.MetricFamily{ + Name: proto.String("empty_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(0), + SampleSum: proto.Float64(0), + }, + }, + }, + }, + out: `# TYPE empty_histogram histogram +empty_histogram {count:0,sum:0,bucket:[+Inf:0]} +`, + }, + { + name: "Histogram_UTF8", + in: &dto.MetricFamily{ + Name: proto.String("http.latency"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Label: []*dto.LabelPair{ + {Name: proto.String("service.name"), Value: proto.String("my-service")}, + }, + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(1), + SampleSum: proto.Float64(0.05), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(0.1), CumulativeCount: proto.Uint64(1)}, + {UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(1)}, + }, + }, + }, + }, + }, + out: `# TYPE "http.latency" histogram +{"http.latency","service.name"="my-service"} {count:1,sum:0.05,bucket:[0.1:1,+Inf:1]} `, }, } @@ -400,26 +875,464 @@ func TestCreateOpenMetrics20_Errors(t *testing.T) { expectedErr: "summary not implemented yet", }, { - name: "HistogramNotImplemented", + name: "HistogramCountNegative", in: &dto.MetricFamily{ - Name: proto.String("test_metric"), + Name: proto.String("test_histogram"), Type: dto.MetricType_HISTOGRAM.Enum(), Metric: []*dto.Metric{ - {Histogram: &dto.Histogram{}}, + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(-1.0), + SampleSum: proto.Float64(0.0), + }, + }, }, }, - expectedErr: "histogram not implemented yet", + expectedErr: "histogram count cannot be negative", }, { - name: "GaugeHistogramNotImplemented", + name: "HistogramCountNaN", in: &dto.MetricFamily{ - Name: proto.String("test_metric"), + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(math.NaN()), + SampleSum: proto.Float64(0.0), + }, + }, + }, + }, + expectedErr: "histogram count cannot be NaN", + }, + { + name: "GaugeHistogramCountNaN", + in: &dto.MetricFamily{ + Name: proto.String("test_gauge_histogram"), Type: dto.MetricType_GAUGE_HISTOGRAM.Enum(), Metric: []*dto.Metric{ - {Histogram: &dto.Histogram{}}, + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(math.NaN()), + SampleSum: proto.Float64(0.0), + }, + }, + }, + }, + expectedErr: "gaugehistogram count cannot be NaN", + }, + { + name: "MetricHasLeLabel_ClassicHistogram", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Label: []*dto.LabelPair{ + {Name: proto.String("le"), Value: proto.String("0.1")}, + }, + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(1), + SampleSum: proto.Float64(0.1), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(0.1), CumulativeCount: proto.Uint64(1)}, + {UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(1)}, + }, + }, + }, + }, + }, + expectedErr: "has classic buckets but label set contains \"le\" label", + }, + { + name: "MetricHasLeLabel_ClassicGaugeHistogram", + in: &dto.MetricFamily{ + Name: proto.String("test_gauge_histogram"), + Type: dto.MetricType_GAUGE_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Label: []*dto.LabelPair{ + {Name: proto.String("le"), Value: proto.String("0.1")}, + }, + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(1), + SampleSum: proto.Float64(0.1), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(0.1), CumulativeCount: proto.Uint64(1)}, + {UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(1)}, + }, + }, + }, + }, + }, + expectedErr: "has classic buckets but label set contains \"le\" label", + }, + { + name: "ClassicBucketThresholdNaN", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(1), + SampleSum: proto.Float64(0.1), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(math.NaN()), CumulativeCount: proto.Uint64(1)}, + }, + }, + }, + }, + }, + expectedErr: "classic bucket upper bound cannot be NaN", + }, + { + name: "ClassicBucketThresholdsUnsorted", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(2), + SampleSum: proto.Float64(0.3), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(1.0), CumulativeCount: proto.Uint64(1)}, + {UpperBound: proto.Float64(0.5), CumulativeCount: proto.Uint64(2)}, + {UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(2)}, + }, + }, + }, + }, + }, + expectedErr: "classic bucket upper bounds must be strictly increasing", + }, + { + name: "ClassicBucketThresholdsDuplicate", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(2), + SampleSum: proto.Float64(0.3), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(1.0), CumulativeCount: proto.Uint64(1)}, + {UpperBound: proto.Float64(1.0), CumulativeCount: proto.Uint64(2)}, + {UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(2)}, + }, + }, + }, + }, + }, + expectedErr: "classic bucket upper bounds must be strictly increasing", + }, + { + name: "ClassicBucketPosInfMismatch", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(5), + SampleSum: proto.Float64(10.0), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(1.0), CumulativeCount: proto.Uint64(2)}, + {UpperBound: proto.Float64(math.Inf(+1)), CumulativeCount: proto.Uint64(4)}, + }, + }, + }, + }, + }, + expectedErr: "classic bucket +Inf count (4) does not match sample count (5)", + }, + { + name: "ClassicBucketCountNaN", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(1.0), + SampleSum: proto.Float64(0.1), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(0.1), CumulativeCountFloat: proto.Float64(math.NaN())}, + {UpperBound: proto.Float64(math.Inf(+1)), CumulativeCountFloat: proto.Float64(1.0)}, + }, + }, + }, + }, + }, + expectedErr: "classic bucket count cannot be NaN", + }, + { + name: "ClassicBucketCountNegative", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(1.0), + SampleSum: proto.Float64(0.1), + Bucket: []*dto.Bucket{ + {UpperBound: proto.Float64(0.1), CumulativeCountFloat: proto.Float64(-1.0)}, + {UpperBound: proto.Float64(math.Inf(+1)), CumulativeCountFloat: proto.Float64(1.0)}, + }, + }, + }, + }, + }, + expectedErr: "classic bucket count cannot be negative", + }, + { + name: "NativeHistogramSchemaOutOfRange_Low", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(0), + SampleSum: proto.Float64(0), + Schema: proto.Int32(-5), + ZeroThreshold: proto.Float64(0.001), + ZeroCount: proto.Uint64(0), + }, + }, + }, + }, + expectedErr: "native histogram schema -5 is out of range [-4, 8]", + }, + { + name: "NativeHistogramSchemaOutOfRange_High", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(0), + SampleSum: proto.Float64(0), + Schema: proto.Int32(9), + ZeroThreshold: proto.Float64(0.001), + ZeroCount: proto.Uint64(0), + }, + }, + }, + }, + expectedErr: "native histogram schema 9 is out of range [-4, 8]", + }, + { + name: "NativeHistogramZeroThresholdNegative", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(0), + SampleSum: proto.Float64(0), + Schema: proto.Int32(0), + ZeroThreshold: proto.Float64(-0.001), + ZeroCount: proto.Uint64(0), + }, + }, + }, + }, + expectedErr: "native histogram zero_threshold -0.001 must be a non-negative, finite number", + }, + { + name: "NativeHistogramZeroThresholdNaN", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(0), + SampleSum: proto.Float64(0), + Schema: proto.Int32(0), + ZeroThreshold: proto.Float64(math.NaN()), + ZeroCount: proto.Uint64(0), + }, + }, + }, + }, + expectedErr: "native histogram zero_threshold NaN must be a non-negative, finite number", + }, + { + name: "NativeHistogramZeroThresholdInf", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(0), + SampleSum: proto.Float64(0), + Schema: proto.Int32(0), + ZeroThreshold: proto.Float64(math.Inf(+1)), + ZeroCount: proto.Uint64(0), + }, + }, + }, + }, + expectedErr: "native histogram zero_threshold +Inf must be a non-negative, finite number", + }, + { + name: "NativeHistogramZeroCountNaN", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(0), + SampleSum: proto.Float64(0), + Schema: proto.Int32(0), + ZeroThreshold: proto.Float64(0.001), + ZeroCountFloat: proto.Float64(math.NaN()), + }, + }, + }, + }, + expectedErr: "native histogram zero_count cannot be NaN", + }, + { + name: "NativeHistogramZeroCountNegative", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(0), + SampleSum: proto.Float64(0), + Schema: proto.Int32(0), + ZeroThreshold: proto.Float64(0.001), + ZeroCountFloat: proto.Float64(-1.0), + }, + }, + }, + }, + expectedErr: "native histogram zero_count cannot be negative", + }, + { + name: "NativeHistogramSubsequentSpanNegativeOffset", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(2), + SampleSum: proto.Float64(1.0), + Schema: proto.Int32(0), + ZeroThreshold: proto.Float64(0.001), + ZeroCount: proto.Uint64(0), + PositiveSpan: []*dto.BucketSpan{ + {Offset: proto.Int32(0), Length: proto.Uint32(1)}, + {Offset: proto.Int32(-1), Length: proto.Uint32(1)}, + }, + PositiveDelta: []int64{1, 0}, + }, + }, + }, + }, + expectedErr: "subsequent positive span offset cannot be negative: -1", + }, + { + name: "NativeHistogramSpanLengthMismatch", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(2), + SampleSum: proto.Float64(1.0), + Schema: proto.Int32(0), + ZeroThreshold: proto.Float64(0.001), + ZeroCount: proto.Uint64(0), + PositiveSpan: []*dto.BucketSpan{ + {Offset: proto.Int32(0), Length: proto.Uint32(3)}, + }, + PositiveDelta: []int64{1, 0}, + }, + }, + }, + }, + expectedErr: "sum of positive span lengths (3) does not match bucket count (2)", + }, + { + name: "NativeHistogramBucketCountNaN", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCountFloat: proto.Float64(1.0), + SampleSum: proto.Float64(1.0), + Schema: proto.Int32(0), + ZeroThreshold: proto.Float64(0.001), + ZeroCountFloat: proto.Float64(0), + PositiveSpan: []*dto.BucketSpan{ + {Offset: proto.Int32(0), Length: proto.Uint32(1)}, + }, + PositiveCount: []float64{math.NaN()}, + }, + }, + }, + }, + expectedErr: "positive bucket count cannot be NaN", + }, + { + name: "NativeHistogramBucketCountNegative", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(1), + SampleSum: proto.Float64(1.0), + Schema: proto.Int32(0), + ZeroThreshold: proto.Float64(0.001), + ZeroCount: proto.Uint64(0), + PositiveSpan: []*dto.BucketSpan{ + {Offset: proto.Int32(0), Length: proto.Uint32(1)}, + }, + PositiveDelta: []int64{-1}, + }, + }, + }, + }, + expectedErr: "positive bucket count cannot be negative (-1)", + }, + { + name: "HistogramInvalidCreatedTimestamp", + in: &dto.MetricFamily{ + Name: proto.String("test_histogram"), + Type: dto.MetricType_HISTOGRAM.Enum(), + Metric: []*dto.Metric{ + { + Histogram: &dto.Histogram{ + SampleCount: proto.Uint64(0), + SampleSum: proto.Float64(0), + CreatedTimestamp: ×tamppb.Timestamp{ + Nanos: -1, + }, + }, + }, }, }, - expectedErr: "histogram not implemented yet", + expectedErr: "invalid created timestamp in metric test_histogram", }, { name: "CounterValueNaN",