Repository: prometheus-community/avalanche

Issue:
mtypes divide by zero crash if input has no summary or histogram metrics
The `mtypes` utility crashes with a 0 divide error if the input lacks summary or histogram metrics and the `-avalanche-flags-for-adjusted-series` flag is passed, e.g.

```
$ ./mtypes < /dev/null -avalanche-flags-for-adjusted-series 10

./mtypes < /dev/null -avalanche-flags-for-adjusted-series 10
Metric Type    Metric Families    Series (adjusted)    Series (adjusted) %        Average Buckets/Objectives
---            ---                ---                  ---                        ---
*              0                  0 (0)                100.000000 (100.000000)    -

Avalanche flags for the similar distribution to get to the adjusted series goal of: 10
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.computeAvalancheFlags(0x8a55a0?, {0x0, 0x0, 0x0, 0x0, 0x0}, 0xc0001239b0)
    [...]/avalanche/cmd/mtypes/main.go:127 +0xcbd
main.main()
    [...]/avalanche/cmd/mtypes/main.go:81 +0x5c5
```

This is true even if valid metrics are passed, so long as the metric set omits summary and/or histogram metrics.


Discussion:
@ringerc: I've raised https://github.com/prometheus-community/avalanche/pull/137 to fix this

Solution patch:
diff --git a/.gitignore b/.gitignore
index d2494cd..75782a9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
 ./avalanche
 .build/
 .idea/
-mtypes
-avalanche
\ No newline at end of file
+/mtypes
+/avalanche
+/cmd/mtypes/mtypes
+/cmd/avalanche/avalanche
diff --git a/cmd/mtypes/main.go b/cmd/mtypes/main.go
index 333f30e..e8f513c 100644
--- a/cmd/mtypes/main.go
+++ b/cmd/mtypes/main.go
@@ -21,6 +21,7 @@ import (
 	"fmt"
 	"io"
 	"log"
+	"math"
 	"net/http"
 	"net/url"
 	"os"
@@ -73,70 +74,98 @@ func main() {
 	if err != nil {
 		log.Fatal(err)
 	}
+	total := computeTotal(statistics)
+	writeStatistics(os.Stdout, total, statistics)
+	if *avalancheFlagsForTotal > 0 {
+		fmt.Fprintln(os.Stdout)
+		fmt.Fprintln(os.Stdout, "Avalanche flags for the similar distribution to get to the adjusted series goal of:", *avalancheFlagsForTotal)
+		seriesCount := 10
+		flags, adjustedSum := computeAvalancheFlags(*avalancheFlagsForTotal, seriesCount, total, statistics)
+		for _, f := range flags {
+			fmt.Fprintln(os.Stdout, f)
+		}
+		fmt.Fprintln(os.Stdout, "This should give the total adjusted series to:", adjustedSum*10)
+	}
+}
+
+func computeTotal(statistics map[dto.MetricType]stats) stats {
 	var total stats
 	for _, s := range statistics {
 		total.families += s.families
 		total.series += s.series
 		total.adjustedSeries += s.adjustedSeries
 	}
+	return total
+}
 
-	writeStatistics(os.Stdout, total, statistics)
+func computeAvalancheFlags(avalancheFlagsForTotal, seriesCount int, total stats, statistics map[dto.MetricType]stats) ([]string, int) {
+	// adjustedGoal is tracking the # of adjusted series we want to generate with avalanche.
+	adjustedGoal := float64(avalancheFlagsForTotal)
+	adjustedGoal /= float64(seriesCount)
+	// adjustedSum is tracking the total sum of series so far (at the end hopefully adjustedSum ~= adjustedGoal)
+	adjustedSum := 0
+	// Accumulate flags
+	var flags []string
+	for _, mtype := range allTypes {
+		s := statistics[mtype]
 
-	if *avalancheFlagsForTotal > 0 {
-		// adjustedGoal is tracking the # of adjusted series we want to generate with avalanche.
-		adjustedGoal := float64(*avalancheFlagsForTotal)
-		fmt.Println()
-		fmt.Println("Avalanche flags for the similar distribution to get to the adjusted series goal of:", adjustedGoal)
-
-		adjustedGoal /= 10.0 // Assuming --series-count=10
-		// adjustedSum is tracking the total sum of series so far (at the end hopefully adjustedSum ~= adjustedGoal)
-		adjustedSum := 0
-		for _, mtype := range allTypes {
-			s := statistics[mtype]
-
-			// adjustedSeriesRatio is tracking the ratio of this type in the input file.
-			// We try to get similar ratio, but with different absolute counts, given the total sum of series we are aiming for.
-			adjustedSeriesRatio := float64(s.adjustedSeries) / float64(total.adjustedSeries)
-
-			// adjustedSeriesForType is tracking (per metric type), how many unique series of that
-			// metric type avalanche needs to create according to the ratio we got from our input.
-			adjustedSeriesForType := int(adjustedGoal * adjustedSeriesRatio)
-
-			switch mtype {
-			case dto.MetricType_GAUGE:
-				fmt.Printf("--gauge-metric-count=%v\n", adjustedSeriesForType)
-				adjustedSum += adjustedSeriesForType
-			case dto.MetricType_COUNTER:
-				fmt.Printf("--counter-metric-count=%v\n", adjustedSeriesForType)
-				adjustedSum += adjustedSeriesForType
-			case dto.MetricType_HISTOGRAM:
-				avgBkts := s.buckets / s.series
+		// adjustedSeriesRatio is tracking the ratio of this type in the input file.
+		// We try to get similar ratio, but with different absolute counts, given the total sum of series we are aiming for.
+		adjustedSeriesRatio := float64(s.adjustedSeries) / float64(total.adjustedSeries)
+
+		// adjustedSeriesForType is tracking (per metric type), how many unique series of that
+		// metric type avalanche needs to create according to the ratio we got from our input.
+		var adjustedSeriesForType int
+		if !math.IsNaN(adjustedSeriesRatio) {
+			adjustedSeriesForType = int(adjustedGoal * adjustedSeriesRatio)
+		}
+
+		switch mtype {
+		case dto.MetricType_GAUGE:
+			flags = append(flags, fmt.Sprintf("--gauge-metric-count=%v", adjustedSeriesForType))
+			adjustedSum += adjustedSeriesForType
+		case dto.MetricType_COUNTER:
+			flags = append(flags, fmt.Sprintf("--counter-metric-count=%v", adjustedSeriesForType))
+			adjustedSum += adjustedSeriesForType
+		case dto.MetricType_HISTOGRAM:
+			var avgBkts int
+			if s.series > 0 {
+				avgBkts = s.buckets / s.series
 				adjustedSeriesForType /= 2 + avgBkts
-				fmt.Printf("--histogram-metric-count=%v\n", adjustedSeriesForType)
-				fmt.Printf("--histogram-metric-bucket-count=%v\n", avgBkts-1) // -1 is due to caveat of additional +Inf not added by avalanche.
-				adjustedSum += adjustedSeriesForType * (2 + avgBkts)
-			case metricType_NATIVE_HISTOGRAM:
-				fmt.Printf("--native-histogram-metric-count=%v\n", adjustedSeriesForType)
-				adjustedSum += adjustedSeriesForType
-			case dto.MetricType_SUMMARY:
-				avgObjs := s.objectives / s.series
+			}
+			flags = append(flags, fmt.Sprintf("--histogram-metric-count=%v", adjustedSeriesForType))
+			if s.series > 0 {
+				flags = append(flags, fmt.Sprintf("--histogram-metric-bucket-count=%v", avgBkts-1)) // -1 is due to caveat of additional +Inf not added by avalanche.
+			}
+			adjustedSum += adjustedSeriesForType * (2 + avgBkts)
+		case metricType_NATIVE_HISTOGRAM:
+			flags = append(flags, fmt.Sprintf("--native-histogram-metric-count=%v", adjustedSeriesForType))
+			adjustedSum += adjustedSeriesForType
+		case dto.MetricType_SUMMARY:
+			var avgObjs int
+			if s.series > 0 {
+				avgObjs = s.objectives / s.series
 				adjustedSeriesForType /= 2 + avgObjs
-				fmt.Printf("--summary-metric-count=%v\n", adjustedSeriesForType)
-				fmt.Printf("--summary-metric-objective-count=%v\n", avgObjs)
-				adjustedSum += adjustedSeriesForType * (2 + avgObjs)
-			default:
-				if s.series > 0 {
-					log.Fatalf("not supported %v metric in avalanche", mtype)
-				}
+			}
+			flags = append(flags, fmt.Sprintf("--summary-metric-count=%v", adjustedSeriesForType))
+			if s.series > 0 {
+				flags = append(flags, fmt.Sprintf("--summary-metric-objective-count=%v", avgObjs))
+			}
+			adjustedSum += adjustedSeriesForType * (2 + avgObjs)
+		default:
+			if s.series > 0 {
+				log.Fatalf("not supported %v metric in avalanche", mtype)
 			}
 		}
-		fmt.Printf("--series-count=10\n")
-		fmt.Printf("--value-interval=300 # Changes values every 5m.\n")
-		fmt.Printf("--series-interval=3600 # 1h series churn.\n")
-		fmt.Printf("--metric-interval=0\n")
-
-		fmt.Println("This should give the total adjusted series to:", adjustedSum*10)
 	}
+	flags = append(flags, fmt.Sprintf("--series-count=%v", seriesCount))
+	// Changes values every 5m.
+	flags = append(flags, "--value-interval=300")
+	// 1h series churn.
+	flags = append(flags, "--series-interval=3600")
+	flags = append(flags, "--metric-interval=0")
+
+	return flags, adjustedSum
 }
 
 var allTypes = []dto.MetricType{dto.MetricType_GAUGE, dto.MetricType_COUNTER, dto.MetricType_HISTOGRAM, metricType_NATIVE_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM, dto.MetricType_SUMMARY, dto.MetricType_UNTYPED}
diff --git a/go.mod b/go.mod
index c6633d6..1b0474c 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/prometheus-community/avalanche
 
-go 1.22.0
+go 1.23.0
 
 toolchain go1.23.1
 
@@ -8,28 +8,30 @@ require (
 	github.com/google/go-cmp v0.7.0
 	github.com/nelkinda/health-go v0.0.1
 	github.com/oklog/run v1.1.0
-	github.com/prometheus/client_golang v1.21.0
+	github.com/prometheus/client_golang v1.22.0
 	github.com/prometheus/client_golang/exp v0.0.0-20250227122456-ad23ad6d5468
-	github.com/prometheus/client_model v0.6.1
-	github.com/prometheus/common v0.62.0
-	github.com/prometheus/prometheus v0.53.1
+	github.com/prometheus/client_model v0.6.2
+	github.com/prometheus/common v0.64.0
+	github.com/prometheus/prometheus v0.304.1
 	github.com/stretchr/testify v1.10.0
 	gopkg.in/alecthomas/kingpin.v2 v2.2.6
 )
 
 require (
 	github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
-	github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 // indirect
+	github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/klauspost/compress v1.17.11 // indirect
+	github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
+	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/nelkinda/http-go v0.0.1 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
-	golang.org/x/sys v0.28.0 // indirect
-	google.golang.org/protobuf v1.36.5 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.25.0 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 2e59a49..bb5671c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,8 @@
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 h1:t3eaIm0rUkzbrIewtiFmMK5RXHej2XnoXNhxVsAYUfg=
-github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
+github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
+github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
 github.com/antchfx/xmlquery v1.2.4/go.mod h1:KQQuESaxSlqugE2ZBcM/qn+ebIpt+d+4Xx7YcSGAIrM=
 github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
 github.com/aslakhellesoy/gox v1.0.100/go.mod h1:AJl542QsKKG96COVsv0N74HHzVQgDIQPceVUh1aeU2M=
@@ -55,6 +55,8 @@ github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
+github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
 github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@@ -65,8 +67,8 @@ github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQL
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
-github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
-github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -98,18 +100,18 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
-github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_golang/exp v0.0.0-20250227122456-ad23ad6d5468 h1:Lg8dMj9NGvHGy9ORtzyCkTvHpz1rsE78HT3S9W4ZC1U=
 github.com/prometheus/client_golang/exp v0.0.0-20250227122456-ad23ad6d5468/go.mod h1:QdwnzTHLXXx636iZ1pfTiCI1Bn/b/20AgMqkPQr4xfA=
-github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
-github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
-github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
+github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
-github.com/prometheus/prometheus v0.53.1 h1:B0xu4VuVTKYrIuBMn/4YSUoIPYxs956qsOfcS4rqCuA=
-github.com/prometheus/prometheus v0.53.1/go.mod h1:RZDkzs+ShMBDkAPQkLEaLBXpjmDcjhNxU2drUVPgKUU=
+github.com/prometheus/prometheus v0.304.1 h1:e4kpJMb2Vh/PcR6LInake+ofcvFYHT+bCfmBvOkaZbY=
+github.com/prometheus/prometheus v0.304.1/go.mod h1:ioGx2SGKTY+fLnJSQCdTHqARVldGNS8OlIe3kvp98so=
 github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -170,11 +172,13 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
+golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@@ -188,8 +192,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -201,6 +205,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=



Test patch:
diff --git a/cmd/mtypes/main_test.go b/cmd/mtypes/main_test.go
index dcd5843..e591afc 100644
--- a/cmd/mtypes/main_test.go
+++ b/cmd/mtypes/main_test.go
@@ -19,25 +19,70 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	dto "github.com/prometheus/client_model/go"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestCalculateTargetStatistics(t *testing.T) {
-	s, err := calculateTargetStatistics(strings.NewReader(testInput))
-	if err != nil {
-		t.Fatal(err)
-	}
-	expected := map[dto.MetricType]stats{
-		dto.MetricType_COUNTER:   {families: 104, series: 166, adjustedSeries: 166},
-		dto.MetricType_GAUGE:     {families: 77, series: 94, adjustedSeries: 94},
-		dto.MetricType_HISTOGRAM: {families: 11, series: 17, adjustedSeries: 224, buckets: 190},
-		dto.MetricType_SUMMARY:   {families: 15, series: 27, adjustedSeries: 114, objectives: 60},
-	}
-	if diff := cmp.Diff(expected, s, cmp.AllowUnexported(stats{})); diff != "" {
-		t.Fatal(diff)
+	for _, tc := range []struct {
+		testName       string
+		testInput      string
+		expectedResult map[dto.MetricType]stats
+		expectedTotal  stats
+	}{
+		{
+			testName:  "samplePromInput",
+			testInput: samplePromTestInput,
+			expectedResult: map[dto.MetricType]stats{
+				dto.MetricType_COUNTER:   {families: 104, series: 166, adjustedSeries: 166},
+				dto.MetricType_GAUGE:     {families: 77, series: 94, adjustedSeries: 94},
+				dto.MetricType_HISTOGRAM: {families: 11, series: 17, adjustedSeries: 224, buckets: 190},
+				dto.MetricType_SUMMARY:   {families: 15, series: 27, adjustedSeries: 114, objectives: 60},
+			},
+			expectedTotal: stats{
+				families: 207, series: 304, adjustedSeries: 598,
+			},
+		},
+		{
+			testName:       "empty",
+			testInput:      "",
+			expectedResult: map[dto.MetricType]stats{},
+			expectedTotal: stats{
+				families: 0, series: 0, adjustedSeries: 0,
+			},
+		},
+		{
+			testName:  "justGauge",
+			testInput: justOneGaugeInput,
+			expectedResult: map[dto.MetricType]stats{
+				dto.MetricType_GAUGE: {families: 1, series: 1, adjustedSeries: 1},
+			},
+			expectedTotal: stats{
+				families: 1, series: 1, adjustedSeries: 1,
+			},
+		},
+	} {
+		t.Run(tc.testName, func(t *testing.T) {
+			s, err := calculateTargetStatistics(strings.NewReader(tc.testInput))
+			if err != nil {
+				t.Fatal(err)
+			}
+			if diff := cmp.Diff(tc.expectedResult, s, cmp.AllowUnexported(stats{})); diff != "" {
+				t.Fatal(diff)
+			}
+			if totalDiff := cmp.Diff(tc.expectedTotal, computeTotal(s), cmp.AllowUnexported(stats{})); totalDiff != "" {
+				t.Fatal(totalDiff)
+			}
+		})
 	}
 }
 
-const testInput = `# HELP gcm_export_pending_requests Number of in-flight requests to GCM.
+const justOneGaugeInput = `
+# HELP up scrape successful
+# TYPE up gauge
+up 1
+`
+
+const samplePromTestInput = `# HELP gcm_export_pending_requests Number of in-flight requests to GCM.
 # TYPE gcm_export_pending_requests gauge
 gcm_export_pending_requests 1
 # HELP gcm_export_projects_per_batch Number of different projects in a batch that's being sent.
@@ -1050,3 +1095,69 @@ promhttp_metric_handler_requests_total{code="200"} 179357
 promhttp_metric_handler_requests_total{code="500"} 0
 promhttp_metric_handler_requests_total{code="503"} 0
 `
+
+func TestComputeAvalancheFlags(t *testing.T) {
+	for _, tc := range []struct {
+		testName               string
+		avalancheFlagsForTotal int
+		seriesCount            int
+		statistics             map[dto.MetricType]stats
+		total                  stats
+		expectedSum            int
+		expectedFlags          []string
+	}{
+		{
+			testName:               "samplePromInput",
+			avalancheFlagsForTotal: 1000,
+			seriesCount:            10,
+			statistics: map[dto.MetricType]stats{
+				dto.MetricType_COUNTER:   {families: 104, series: 166, adjustedSeries: 166},
+				dto.MetricType_GAUGE:     {families: 77, series: 94, adjustedSeries: 94},
+				dto.MetricType_HISTOGRAM: {families: 11, series: 17, adjustedSeries: 224, buckets: 190},
+				dto.MetricType_SUMMARY:   {families: 15, series: 27, adjustedSeries: 114, objectives: 60},
+			},
+			total: stats{
+				families: 207, series: 304, adjustedSeries: 598,
+			},
+			expectedSum: 84,
+			expectedFlags: []string{
+				"--gauge-metric-count=15",
+				"--counter-metric-count=27",
+				"--histogram-metric-count=2",
+				"--histogram-metric-bucket-count=10",
+				"--native-histogram-metric-count=0",
+				"--summary-metric-count=4",
+				"--summary-metric-objective-count=2",
+				"--series-count=10",
+				"--value-interval=300",
+				"--series-interval=3600",
+				"--metric-interval=0",
+			},
+		},
+		{
+			testName:               "noInput",
+			avalancheFlagsForTotal: 1000,
+			seriesCount:            10,
+			statistics:             map[dto.MetricType]stats{},
+			total:                  stats{},
+			expectedSum:            0,
+			expectedFlags: []string{
+				"--gauge-metric-count=0",
+				"--counter-metric-count=0",
+				"--histogram-metric-count=0",
+				"--native-histogram-metric-count=0",
+				"--summary-metric-count=0",
+				"--series-count=10",
+				"--value-interval=300",
+				"--series-interval=3600",
+				"--metric-interval=0",
+			},
+		},
+	} {
+		t.Run(tc.testName, func(t *testing.T) {
+			avalancheFlags, adjustedSum := computeAvalancheFlags(tc.avalancheFlagsForTotal, tc.seriesCount, tc.total, tc.statistics)
+			assert.Equal(t, tc.expectedSum, adjustedSum)
+			assert.Equal(t, tc.expectedFlags, avalancheFlags)
+		})
+	}
+}