diff --git a/pkg/metric/metric.go b/pkg/metric/metric.go index 66f994f43..8c46249a4 100644 --- a/pkg/metric/metric.go +++ b/pkg/metric/metric.go @@ -1505,8 +1505,11 @@ func GetSnapshot(options SnapshotOptions) (*prometheus.Snapshot, error) { Metric: m.prometheusMetric, Labels: labels, HistogramValue: &prometheus.Histogram{ - Total: prometheus.Number{Int: statistics[fieldKey].sampleSum}, - Buckets: buckets, + Total: prometheus.Number{Int: statistics[fieldKey].sampleSum}, + SumOfSquaredDeviations: prometheus.Number{Float: statistics[fieldKey].sumOfSquaredDeviations}, + Min: prometheus.Number{Int: statistics[fieldKey].min}, + Max: prometheus.Number{Int: statistics[fieldKey].max}, + Buckets: buckets, }, }) } diff --git a/pkg/prometheus/prometheus.go b/pkg/prometheus/prometheus.go index feeee5ecb..05bfe03a4 100644 --- a/pkg/prometheus/prometheus.go +++ b/pkg/prometheus/prometheus.go @@ -181,6 +181,8 @@ func NewFloat(val float64) *Number { // IsInteger returns whether this number contains an integer value. // This is defined as either having the `Float` part set to zero (in which case the `Int` part takes // precedence), or having `Float` be a value equal to its own rounding and not a special float. +// +//go:nosplit func (n *Number) IsInteger() bool { if n.Float == 0 { return true @@ -202,6 +204,8 @@ func (n *Number) String() string { // SameType returns true if `n` and `other` are either both floating-point or both integers. // If a `Number` is zero, it is considered of the same type as any other zero `Number`. +// +//go:nosplit func (n *Number) SameType(other *Number) bool { // Within `n` and `other`, at least one of `Int` or `Float` must be set to zero. // Therefore, this verifies that there is at least one shared zero between the two. @@ -210,6 +214,8 @@ func (n *Number) SameType(other *Number) bool { // GreaterThan returns true if n > other. // Precondition: n.SameType(other) is true. Panics otherwise. +// +//go:nosplit func (n *Number) GreaterThan(other *Number) bool { if !n.SameType(other) { panic("tried to compare two numbers of different types") @@ -298,6 +304,12 @@ type Bucket struct { type Histogram struct { // Total is the sum of sample values across all buckets. Total Number `json:"total"` + // Min is the minimum sample ever recorded in this histogram. + Min Number `json:"min"` + // Max is the maximum sample ever recorded in this histogram. + Max Number `json:"max"` + // SumOfSquaredDeviations is the number of squared deviations of all samples. + SumOfSquaredDeviations Number `json:"ssd"` // Buckets contains per-bucket data. // A distribution with n finite-boundary buckets should have n+2 entries here. // The 0th entry is the underflow bucket (i.e. the one with -inf as lower bound), @@ -764,6 +776,15 @@ func (d *Data) writeTo(w io.Writer, when time.Time, options SnapshotExportOption if err := d.writeMetricLine(w, "_count", &samples, when, options, nil, metricsWritten); err != nil { return err } + if err := d.writeMetricLine(w, "_min", &d.HistogramValue.Min, when, options, nil, metricsWritten); err != nil { + return err + } + if err := d.writeMetricLine(w, "_max", &d.HistogramValue.Max, when, options, nil, metricsWritten); err != nil { + return err + } + if err := d.writeMetricLine(w, "_ssd", &d.HistogramValue.SumOfSquaredDeviations, when, options, nil, metricsWritten); err != nil { + return err + } // Empty line after the histogram. if _, err := io.WriteString(w, "\n"); err != nil { return err diff --git a/pkg/prometheus/prometheus_test.go b/pkg/prometheus/prometheus_test.go index 8ae6b57fb..6244c9090 100644 --- a/pkg/prometheus/prometheus_test.go +++ b/pkg/prometheus/prometheus_test.go @@ -171,14 +171,29 @@ func (m *metricMetadata) float(val float64) *Data { // value, and this value will be used as the value for that field. // If a field accepts multiple values, the function will panic. func (m *metricMetadata) dist(samples ...int64) *Data { - var total int64 + var total, min, max int64 + var ssd float64 buckets := make([]Bucket, len(m.PB.GetDistributionBucketLowerBounds())+1) var bucket *Bucket for i, lowerBound := range m.PB.GetDistributionBucketLowerBounds() { (&buckets[i]).UpperBound = Number{Int: lowerBound} } (&buckets[len(buckets)-1]).UpperBound = Number{Float: math.Inf(1)} - for _, sample := range samples { + for i, sample := range samples { + if i == 0 { + min = sample + max = sample + } else { + if sample < min { + min = sample + } + if sample > max { + max = sample + } + oldMean := float64(total) / float64(i+1) + newMean := float64(total+sample) / float64(i+2) + ssd += (float64(sample) - oldMean) * (float64(sample) - newMean) + } total += sample bucket = &buckets[0] for i, lowerBound := range m.PB.GetDistributionBucketLowerBounds() { @@ -194,8 +209,11 @@ func (m *metricMetadata) dist(samples ...int64) *Data { Metric: m.metric(), Labels: m.labels(), HistogramValue: &Histogram{ - Total: Number{Int: total}, - Buckets: buckets, + Total: Number{Int: total}, + Buckets: buckets, + Min: Number{Int: min}, + Max: Number{Int: max}, + SumOfSquaredDeviations: Number{Float: ssd}, }, } } @@ -1165,6 +1183,9 @@ func TestSnapshotToPrometheus(t *testing.T) { foo_dist_bucket{le="+inf"} 8 {TIMESTAMP} foo_dist_sum 126 {TIMESTAMP} foo_dist_count 8 {TIMESTAMP} + foo_dist_min -1 {TIMESTAMP} + foo_dist_max 99 {TIMESTAMP} + foo_dist_ssd 8187.5 {TIMESTAMP} `, }, { @@ -1193,6 +1214,9 @@ func TestSnapshotToPrometheus(t *testing.T) { foo_dist_bucket{le="+inf"} 0 {TIMESTAMP} foo_dist_sum 0 {TIMESTAMP} foo_dist_count 0 {TIMESTAMP} + foo_dist_min 0 {TIMESTAMP} + foo_dist_max 0 {TIMESTAMP} + foo_dist_ssd 0 {TIMESTAMP} `, }, { @@ -1214,6 +1238,9 @@ func TestSnapshotToPrometheus(t *testing.T) { foo_dist_bucket{field1="val1a",le="+inf"} 8 {TIMESTAMP} foo_dist_sum{field1="val1a"} 126 {TIMESTAMP} foo_dist_count{field1="val1a"} 8 {TIMESTAMP} + foo_dist_min{field1="val1a"} -1 {TIMESTAMP} + foo_dist_max{field1="val1a"} 99 {TIMESTAMP} + foo_dist_ssd{field1="val1a"} 8187.5 {TIMESTAMP} foo_dist_bucket{field1="val1b",le="0"} 0 {TIMESTAMP} foo_dist_bucket{field1="val1b",le="1"} 0 {TIMESTAMP} foo_dist_bucket{field1="val1b",le="2"} 0 {TIMESTAMP} @@ -1222,6 +1249,9 @@ func TestSnapshotToPrometheus(t *testing.T) { foo_dist_bucket{field1="val1b",le="+inf"} 3 {TIMESTAMP} foo_dist_sum{field1="val1b"} 11 {TIMESTAMP} foo_dist_count{field1="val1b"} 3 {TIMESTAMP} + foo_dist_min{field1="val1b"} 3 {TIMESTAMP} + foo_dist_max{field1="val1b"} 5 {TIMESTAMP} + foo_dist_ssd{field1="val1b"} 8.25 {TIMESTAMP} `, }, { @@ -1250,6 +1280,9 @@ func TestSnapshotToPrometheus(t *testing.T) { some_prefix_foo_dist_bucket{field1="val1a",field2="val2a",le="+inf"} 8 {TIMESTAMP} some_prefix_foo_dist_sum{field1="val1a",field2="val2a"} 126 {TIMESTAMP} some_prefix_foo_dist_count{field1="val1a",field2="val2a"} 8 {TIMESTAMP} + some_prefix_foo_dist_min{field1="val1a",field2="val2a"} -1 {TIMESTAMP} + some_prefix_foo_dist_max{field1="val1a",field2="val2a"} 99 {TIMESTAMP} + some_prefix_foo_dist_ssd{field1="val1a",field2="val2a"} 8187.5 {TIMESTAMP} some_prefix_foo_dist_bucket{field1="val1b",field2="val2a",le="0"} 0 {TIMESTAMP} some_prefix_foo_dist_bucket{field1="val1b",field2="val2a",le="1"} 0 {TIMESTAMP} some_prefix_foo_dist_bucket{field1="val1b",field2="val2a",le="2"} 0 {TIMESTAMP} @@ -1258,6 +1291,9 @@ func TestSnapshotToPrometheus(t *testing.T) { some_prefix_foo_dist_bucket{field1="val1b",field2="val2a",le="+inf"} 3 {TIMESTAMP} some_prefix_foo_dist_sum{field1="val1b",field2="val2a"} 11 {TIMESTAMP} some_prefix_foo_dist_count{field1="val1b",field2="val2a"} 3 {TIMESTAMP} + some_prefix_foo_dist_min{field1="val1b",field2="val2a"} 3 {TIMESTAMP} + some_prefix_foo_dist_max{field1="val1b",field2="val2a"} 5 {TIMESTAMP} + some_prefix_foo_dist_ssd{field1="val1b",field2="val2a"} 8.25 {TIMESTAMP} `, }, } { @@ -1473,7 +1509,7 @@ func TestGroupSameNameMetrics(t *testing.T) { t.Fatalf("invalid line: %q", line) } metricName := line[:len(line)-len(strippedMetricName)] - for _, distribSuffix := range []string{"_sum", "_count", "_bucket"} { + for _, distribSuffix := range []string{"_sum", "_count", "_bucket", "_min", "_max", "_ssd"} { metricName = strings.TrimSuffix(metricName, distribSuffix) } if lastMetric != "" && lastMetric != metricName && seenMetrics[metricName] {