gVisor metrics library: Add distribution statistics to Prometheus Histogram.

This plumbs the distribution statistics out to the data consumed from the
gVisor sandbox process by the `runsc metric-server` process.

PiperOrigin-RevId: 537990236
This commit is contained in:
Etienne Perot
2023-06-05 15:04:15 -07:00
committed by gVisor bot
parent 2f0e2dfeb9
commit f8d8cc45ab
3 changed files with 67 additions and 7 deletions
+5 -2
View File
@@ -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,
},
})
}
+21
View File
@@ -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
+41 -5
View File
@@ -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] {