diff --git a/cmd/avalanche.go b/cmd/avalanche.go index e48948e..65fe770 100644 --- a/cmd/avalanche.go +++ b/cmd/avalanche.go @@ -18,6 +18,7 @@ import ( "fmt" "log" "math/rand" + "os" "strconv" "sync" "time" @@ -30,36 +31,67 @@ import ( ) var ( - metricCount = kingpin.Flag("metric-count", "Number of metrics to serve.").Default("500").Int() - labelCount = kingpin.Flag("label-count", "Number of labels per-metric.").Default("10").Int() - seriesCount = kingpin.Flag("series-count", "Number of series per-metric.").Default("10").Int() - metricLength = kingpin.Flag("metricname-length", "Modify length of metric names.").Default("5").Int() - labelLength = kingpin.Flag("labelname-length", "Modify length of label names.").Default("5").Int() - constLabels = kingpin.Flag("const-label", "Constant label to add to every metric. Format is labelName=labelValue. Flag can be specified multiple times.").Strings() - valueInterval = kingpin.Flag("value-interval", "Change series values every {interval} seconds.").Default("30").Int() - labelInterval = kingpin.Flag("series-interval", "Change series_id label values every {interval} seconds.").Default("60").Int() - metricInterval = kingpin.Flag("metric-interval", "Change __name__ label values every {interval} seconds.").Default("120").Int() - port = kingpin.Flag("port", "Port to serve at").Default("9001").Int() - remoteURL = kingpin.Flag("remote-url", "URL to send samples via remote_write API.").URL() - remotePprofURLs = kingpin.Flag("remote-pprof-urls", "a list of urls to download pprofs during the remote write: --remote-pprof-urls=http://127.0.0.1:10902/debug/pprof/heap --remote-pprof-urls=http://127.0.0.1:10902/debug/pprof/profile").URLList() - remotePprofInterval = kingpin.Flag("remote-pprof-interval", "how often to download pprof profiles.When not provided it will download a profile once before the end of the test.").Duration() - remoteBatchSize = kingpin.Flag("remote-batch-size", "how many samples to send with each remote_write API request.").Default("2000").Int() - remoteRequestCount = kingpin.Flag("remote-requests-count", "how many requests to send in total to the remote_write API.").Default("100").Int() - remoteReqsInterval = kingpin.Flag("remote-write-interval", "delay between each remote write request.").Default("100ms").Duration() - remoteTenant = kingpin.Flag("remote-tenant", "Tenant ID to include in remote_write send").Default("0").String() - tlsClientInsecure = kingpin.Flag("tls-client-insecure", "Skip certificate check on tls connection").Default("false").Bool() - remoteTenantHeader = kingpin.Flag("remote-tenant-header", "Tenant ID to include in remote_write send. The default, is the default tenant header expected by Cortex.").Default("X-Scope-OrgID").String() + metricCount = kingpin.Flag("metric-count", "Number of metrics to serve.").Default("500").Int() + labelCount = kingpin.Flag("label-count", "Number of labels per-metric.").Default("10").Int() + seriesCount = kingpin.Flag("series-count", "Number of series per-metric.").Default("100").Int() + seriesChangeRate = kingpin.Flag("series-change-rate", "The rate at which the number of active series changes over time. Applies to 'gradual-change' mode.").Default("100").Int() + maxSeriesCount = kingpin.Flag("max-series-count", "Maximum number of series to serve. Applies to 'gradual-change' mode.").Default("1000").Int() + minSeriesCount = kingpin.Flag("min-series-count", "Minimum number of series to serve. Applies to 'gradual-change' mode.").Default("100").Int() + metricLength = kingpin.Flag("metricname-length", "Modify length of metric names.").Default("5").Int() + labelLength = kingpin.Flag("labelname-length", "Modify length of label names.").Default("5").Int() + constLabels = kingpin.Flag("const-label", "Constant label to add to every metric. Format is labelName=labelValue. Flag can be specified multiple times.").Strings() + valueInterval = kingpin.Flag("value-interval", "Change series values every {interval} seconds.").Default("30").Int() + labelInterval = kingpin.Flag("series-interval", "Change series_id label values every {interval} seconds.").Default("60").Int() + metricInterval = kingpin.Flag("metric-interval", "Change __name__ label values every {interval} seconds.").Default("120").Int() + seriesChangeInterval = kingpin.Flag("series-change-interval", "Change the number of series every {interval} seconds. Applies to 'gradual-change' and 'double-halve' modes.").Default("10").Int() + seriesOperationMode = kingpin.Flag("series-operation-mode", "Mode of operation: 'gradual-change', 'double-halve'").Default("default").String() + port = kingpin.Flag("port", "Port to serve at").Default("9001").Int() + remoteURL = kingpin.Flag("remote-url", "URL to send samples via remote_write API.").URL() + remotePprofURLs = kingpin.Flag("remote-pprof-urls", "a list of urls to download pprofs during the remote write: --remote-pprof-urls=http://127.0.0.1:10902/debug/pprof/heap --remote-pprof-urls=http://127.0.0.1:10902/debug/pprof/profile").URLList() + remotePprofInterval = kingpin.Flag("remote-pprof-interval", "how often to download pprof profiles.When not provided it will download a profile once before the end of the test.").Duration() + remoteBatchSize = kingpin.Flag("remote-batch-size", "how many samples to send with each remote_write API request.").Default("2000").Int() + remoteRequestCount = kingpin.Flag("remote-requests-count", "how many requests to send in total to the remote_write API.").Default("100").Int() + remoteReqsInterval = kingpin.Flag("remote-write-interval", "delay between each remote write request.").Default("100ms").Duration() + remoteTenant = kingpin.Flag("remote-tenant", "Tenant ID to include in remote_write send").Default("0").String() + tlsClientInsecure = kingpin.Flag("tls-client-insecure", "Skip certificate check on tls connection").Default("false").Bool() + remoteTenantHeader = kingpin.Flag("remote-tenant-header", "Tenant ID to include in remote_write send. The default, is the default tenant header expected by Cortex.").Default("X-Scope-OrgID").String() ) func main() { kingpin.Version(version.Print("avalanche")) log.SetFlags(log.Ltime | log.Lshortfile) // Show file name and line in logs. - kingpin.CommandLine.Help = "avalanche - metrics test server" + kingpin.CommandLine.Help = "avalanche - metrics test server\n" + + "\nSeries Operation Modes:\n" + + " double-halve:\n" + + " Alternately doubles and halves the series count at regular intervals.\n" + + " Usage: ./avalanche --operation-mode=double-halve --series-change-interval=30 --series-count=20\n" + + " Description: This mode alternately doubles and halves the series count at regular intervals.\n" + + " The series count is doubled on one tick and halved on the next, ensuring it never drops below 1.\n" + + "\n" + + " gradual-change:\n" + + " Gradually changes the series count by a fixed rate at regular intervals.\n" + + " Usage: ./avalanche --operation-mode=gradual-change --series-change-interval=30 --series-change-rate=10 --series-count=20\n" + + " Description: This mode gradually increases the series count by seriesChangeRate on each tick up to maxSeriesCount,\n" + + " then decreases it back to the starting value, and repeats this cycle indefinitely.\n" + + " The series count is incremented by seriesChangeRate on each tick, ensuring it never drops below 1." + kingpin.Parse() + if *maxSeriesCount <= *minSeriesCount { + fmt.Fprintf(os.Stderr, "Error: --max-series-count (%d) must be greater than --min-series-count (%d)\n", *maxSeriesCount, *minSeriesCount) + os.Exit(1) + } + if *minSeriesCount < 0 { + fmt.Fprintf(os.Stderr, "Error: --min-series-count must be 0 or higher, got %d\n", *minSeriesCount) + os.Exit(1) + } + if *seriesChangeRate <= 0 { + fmt.Fprintf(os.Stderr, "Error: --series-change-rate must be greater than 0, got %d\n", *seriesChangeRate) + os.Exit(1) + } stop := make(chan struct{}) defer close(stop) - updateNotify, err := metrics.RunMetrics(*metricCount, *labelCount, *seriesCount, *metricLength, *labelLength, *valueInterval, *labelInterval, *metricInterval, *constLabels, stop) + updateNotify, err := metrics.RunMetrics(*metricCount, *labelCount, *seriesCount, *seriesChangeRate, *maxSeriesCount, *minSeriesCount, *metricLength, *labelLength, *valueInterval, *labelInterval, *metricInterval, *seriesChangeInterval, *seriesOperationMode, *constLabels, stop) if err != nil { log.Fatal(err) } diff --git a/go.mod b/go.mod index e23200d..bfccd67 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.54.0 github.com/prometheus/prometheus v0.53.1 + github.com/stretchr/testify v1.9.0 gopkg.in/alecthomas/kingpin.v2 v2.2.6 ) @@ -18,8 +19,11 @@ require ( github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // 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/nelkinda/http-go v0.0.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/procfs v0.12.0 // indirect golang.org/x/sys v0.21.0 // indirect google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 44a108c..2aa3ae0 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,11 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv 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= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= @@ -102,6 +105,8 @@ github.com/prometheus/prometheus v0.53.1/go.mod h1:RZDkzs+ShMBDkAPQkLEaLBXpjmDcj 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= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -176,6 +181,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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= diff --git a/metrics/serve.go b/metrics/serve.go index f4a495f..ca49bca 100644 --- a/metrics/serve.go +++ b/metrics/serve.go @@ -82,8 +82,124 @@ func cycleValues(labelKeys, labelValues []string, seriesCount, seriesCycle int) } } +func handleValueTicks(labelKeys, labelValues *[]string, currentSeriesCount, seriesCycle *int, updateNotify chan struct{}, valueTick *time.Ticker) { + for tick := range valueTick.C { + metricsMux.Lock() + fmt.Printf("%v: refreshing metric values\n", tick) + cycleValues(*labelKeys, *labelValues, *currentSeriesCount, *seriesCycle) + metricsMux.Unlock() + + select { + case updateNotify <- struct{}{}: + default: + } + } +} + +func handleSeriesTicks(labelKeys, labelValues *[]string, currentSeriesCount, seriesCycle *int, updateNotify chan struct{}, seriesTick *time.Ticker) { + for tick := range seriesTick.C { + metricsMux.Lock() + fmt.Printf("%v: refreshing series cycle\n", tick) + deleteValues(*labelKeys, *labelValues, *currentSeriesCount, *seriesCycle) + (*seriesCycle)++ + cycleValues(*labelKeys, *labelValues, *currentSeriesCount, *seriesCycle) + metricsMux.Unlock() + + select { + case updateNotify <- struct{}{}: + default: + } + } +} + +func handleMetricTicks(metricCount, metricLength, metricCycle *int, labelKeys *[]string, updateNotify chan struct{}, metricTick *time.Ticker) { + for tick := range metricTick.C { + metricsMux.Lock() + fmt.Printf("%v: refreshing metric cycle\n", tick) + (*metricCycle)++ + unregisterMetrics() + registerMetrics(*metricCount, *metricLength, *metricCycle, *labelKeys) + metricsMux.Unlock() + select { + case updateNotify <- struct{}{}: + default: + } + } +} + +func changeSeriesGradual(seriesChangeRate, maxSeriesCount, minSeriesCount, currentSeriesCount *int, seriesIncrease *bool) { + fmt.Printf("Current series count: %d\n", *currentSeriesCount) + if *seriesIncrease { + *currentSeriesCount += *seriesChangeRate + if *currentSeriesCount >= *maxSeriesCount { + *currentSeriesCount = *maxSeriesCount + *seriesIncrease = false + } + } else { + *currentSeriesCount -= *seriesChangeRate + if *currentSeriesCount < *minSeriesCount { + *currentSeriesCount = *minSeriesCount + *seriesIncrease = true + } + } +} + +func changeSeriesDoubleHalve(currentSeriesCount *int, seriesIncrease *bool) { + if *seriesIncrease { + *currentSeriesCount *= 2 + } else { + *currentSeriesCount /= 2 + if *currentSeriesCount < 1 { + *currentSeriesCount = 1 + } + } + *seriesIncrease = !*seriesIncrease +} + +func handleDoubleHalveMode(metricCount, metricLength, metricCycle, seriesCycle int, labelKeys, labelValues []string, currentSeriesCount *int, changeSeriesTick *time.Ticker, updateNotify chan struct{}) { + seriesIncrease := true + for tick := range changeSeriesTick.C { + metricsMux.Lock() + unregisterMetrics() + registerMetrics(metricCount, metricLength, metricCycle, labelKeys) + cycleValues(labelKeys, labelValues, *currentSeriesCount, seriesCycle) + metricsMux.Unlock() + + changeSeriesDoubleHalve(currentSeriesCount, &seriesIncrease) + + fmt.Printf("%v: Adjusting series count. New count: %d\n", tick, *currentSeriesCount) + + select { + case updateNotify <- struct{}{}: + default: + } + } +} + +func handleGradualChangeMode(metricCount, metricLength, metricCycle, seriesCycle int, labelKeys, labelValues []string, seriesChangeRate, maxSeriesCount, minSeriesCount int, seriesCount *int, changeSeriesTick *time.Ticker, updateNotify chan struct{}) { + *seriesCount = minSeriesCount + seriesIncrease := true + + for tick := range changeSeriesTick.C { + metricsMux.Lock() + unregisterMetrics() + registerMetrics(metricCount, metricLength, metricCycle, labelKeys) + cycleValues(labelKeys, labelValues, *seriesCount, seriesCycle) + metricsMux.Unlock() + + changeSeriesGradual(&seriesChangeRate, &maxSeriesCount, &minSeriesCount, seriesCount, &seriesIncrease) + + fmt.Printf("%v: Adjusting series count. New count: %d\n", tick, *seriesCount) + + select { + case updateNotify <- struct{}{}: + default: + } + } +} + // RunMetrics creates a set of Prometheus test series that update over time -func RunMetrics(metricCount, labelCount, seriesCount, metricLength, labelLength, valueInterval, seriesInterval, metricInterval int, constLabels []string, stop chan struct{}) (chan struct{}, error) { +func RunMetrics(metricCount, labelCount, seriesCount, seriesChangeRate, maxSeriesCount, minSeriesCount, metricLength, labelLength, valueInterval, seriesInterval, metricInterval, seriesChangeInterval int, seriesOperationMode string, constLabels []string, stop chan struct{}) (chan struct{}, error) { labelKeys := make([]string, labelCount) for idx := 0; idx < labelCount; idx++ { labelKeys[idx] = fmt.Sprintf("label_key_%s_%v", strings.Repeat("k", labelLength), idx) @@ -103,62 +219,46 @@ func RunMetrics(metricCount, labelCount, seriesCount, metricLength, labelLength, metricCycle := 0 seriesCycle := 0 - registerMetrics(metricCount, metricLength, metricCycle, labelKeys) - cycleValues(labelKeys, labelValues, seriesCount, seriesCycle) valueTick := time.NewTicker(time.Duration(valueInterval) * time.Second) seriesTick := time.NewTicker(time.Duration(seriesInterval) * time.Second) metricTick := time.NewTicker(time.Duration(metricInterval) * time.Second) + changeSeriesTick := time.NewTicker(time.Duration(seriesChangeInterval) * time.Second) updateNotify := make(chan struct{}, 1) - go func() { - for tick := range valueTick.C { - fmt.Printf("%v: refreshing metric values\n", tick) - metricsMux.Lock() - cycleValues(labelKeys, labelValues, seriesCount, seriesCycle) - metricsMux.Unlock() - select { - case updateNotify <- struct{}{}: - default: - } - } - }() + currentSeriesCount := seriesCount - go func() { - for tick := range seriesTick.C { - fmt.Printf("%v: refreshing series cycle\n", tick) - metricsMux.Lock() - deleteValues(labelKeys, labelValues, seriesCount, seriesCycle) - seriesCycle++ - cycleValues(labelKeys, labelValues, seriesCount, seriesCycle) - metricsMux.Unlock() - select { - case updateNotify <- struct{}{}: - default: - } - } - }() + switch seriesOperationMode { + case "double-halve": + registerMetrics(metricCount, metricLength, metricCycle, labelKeys) + cycleValues(labelKeys, labelValues, currentSeriesCount, seriesCycle) + go handleDoubleHalveMode(metricCount, metricLength, metricCycle, seriesCycle, labelKeys, labelValues, ¤tSeriesCount, changeSeriesTick, updateNotify) + go handleValueTicks(&labelKeys, &labelValues, ¤tSeriesCount, &seriesCycle, updateNotify, valueTick) + go handleSeriesTicks(&labelKeys, &labelValues, ¤tSeriesCount, &seriesCycle, updateNotify, seriesTick) - go func() { - for tick := range metricTick.C { - fmt.Printf("%v: refreshing metric cycle\n", tick) - metricsMux.Lock() - metricCycle++ - unregisterMetrics() - registerMetrics(metricCount, metricLength, metricCycle, labelKeys) - cycleValues(labelKeys, labelValues, seriesCount, seriesCycle) - metricsMux.Unlock() - select { - case updateNotify <- struct{}{}: - default: - } + case "gradual-change": + if minSeriesCount >= maxSeriesCount { + return nil, fmt.Errorf("error: minSeriesCount must be less than maxSeriesCount, got %d and %d", minSeriesCount, maxSeriesCount) } - }() + registerMetrics(metricCount, metricLength, metricCycle, labelKeys) + cycleValues(labelKeys, labelValues, minSeriesCount, seriesCycle) + go handleGradualChangeMode(metricCount, metricLength, metricCycle, seriesCycle, labelKeys, labelValues, seriesChangeRate, maxSeriesCount, minSeriesCount, ¤tSeriesCount, changeSeriesTick, updateNotify) + go handleValueTicks(&labelKeys, &labelValues, ¤tSeriesCount, &seriesCycle, updateNotify, valueTick) + go handleSeriesTicks(&labelKeys, &labelValues, ¤tSeriesCount, &seriesCycle, updateNotify, seriesTick) + + default: + registerMetrics(metricCount, metricLength, metricCycle, labelKeys) + cycleValues(labelKeys, labelValues, currentSeriesCount, seriesCycle) + go handleValueTicks(&labelKeys, &labelValues, ¤tSeriesCount, &seriesCycle, updateNotify, valueTick) + go handleSeriesTicks(&labelKeys, &labelValues, ¤tSeriesCount, &seriesCycle, updateNotify, seriesTick) + go handleMetricTicks(&metricCount, &metricLength, &metricCycle, &labelKeys, updateNotify, metricTick) + } go func() { <-stop valueTick.Stop() seriesTick.Stop() metricTick.Stop() + changeSeriesTick.Stop() }() return updateNotify, nil diff --git a/metrics/serve_test.go b/metrics/serve_test.go new file mode 100644 index 0000000..fd8800c --- /dev/null +++ b/metrics/serve_test.go @@ -0,0 +1,163 @@ +// Copyright 2024 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "fmt" + "testing" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" +) + +// Helper function to count the series in the registry +func countSeries(t *testing.T, registry *prometheus.Registry) int { + metricsFamilies, err := registry.Gather() + assert.NoError(t, err) + + seriesCount := 0 + for _, mf := range metricsFamilies { + for range mf.Metric { + seriesCount++ + } + } + + return seriesCount +} + +func TestRunMetricsSeriesCountChangeDoubleHalve(t *testing.T) { + const ( + initialSeriesCount = 5 + metricCount = 1 + labelCount = 1 + maxSeriesCount = 10 + minSeriesCount = 1 + seriesChangeRate = 1 + metricLength = 1 + labelLength = 1 + valueInterval = 100 + seriesInterval = 100 + metricInterval = 100 + seriesChangeInterval = 3 + operationMode = "double-halve" + constLabel = "constLabel=test" + ) + + stop := make(chan struct{}) + defer close(stop) + + promRegistry = prometheus.NewRegistry() + + _, err := RunMetrics(metricCount, labelCount, initialSeriesCount, seriesChangeRate, maxSeriesCount, minSeriesCount, metricLength, labelLength, valueInterval, seriesInterval, metricInterval, seriesChangeInterval, operationMode, []string{constLabel}, stop) + assert.NoError(t, err) + time.Sleep(2 * time.Second) + for i := 0; i < 4; i++ { + time.Sleep(time.Duration(seriesChangeInterval) * time.Second) + if i%2 == 0 { // Expecting halved series count + currentCount := countSeries(t, promRegistry) + expectedCount := initialSeriesCount + assert.Equal(t, expectedCount, currentCount, "Halved series count should be %d but got %d", expectedCount, currentCount) + } else { // Expecting doubled series count + currentCount := countSeries(t, promRegistry) + expectedCount := initialSeriesCount * 2 + assert.Equal(t, expectedCount, currentCount, "Doubled series count should be %d but got %d", expectedCount, currentCount) + } + } +} + +func TestRunMetricsGradualChange(t *testing.T) { + const ( + metricCount = 1 + labelCount = 1 + seriesCount = 100 + maxSeriesCount = 30 + minSeriesCount = 10 + seriesChangeRate = 10 + metricLength = 1 + labelLength = 1 + valueInterval = 100 + seriesInterval = 100 + metricInterval = 100 + seriesChangeInterval = 3 + operationMode = "gradual-change" + constLabel = "constLabel=test" + ) + + stop := make(chan struct{}) + defer close(stop) + + promRegistry = prometheus.NewRegistry() + + _, err := RunMetrics(metricCount, labelCount, seriesCount, seriesChangeRate, maxSeriesCount, minSeriesCount, metricLength, labelLength, valueInterval, seriesInterval, metricInterval, seriesChangeInterval, operationMode, []string{constLabel}, stop) + assert.NoError(t, err) + + time.Sleep(2 * time.Second) + currentCount := countSeries(t, promRegistry) + expectedInitialCount := currentCount + assert.Equal(t, expectedInitialCount, currentCount, "Initial series count should be minSeriesCount %d but got %d", expectedInitialCount, currentCount) + + assert.Eventually(t, func() bool { + graduallyIncreasedCount := countSeries(t, promRegistry) + fmt.Println("seriesCount: ", graduallyIncreasedCount) + if graduallyIncreasedCount > maxSeriesCount { + t.Fatalf("Gradually increased series count should be less than maxSeriesCount %d but got %d", maxSeriesCount, graduallyIncreasedCount) + } + if currentCount > graduallyIncreasedCount { + t.Fatalf("Gradually increased series count should be greater than initial series count %d but got %d", currentCount, graduallyIncreasedCount) + } else { + currentCount = graduallyIncreasedCount + } + + return graduallyIncreasedCount == maxSeriesCount + }, 15*time.Second, seriesChangeInterval*time.Second, "Did not receive update notification for series count gradual increase in time") + + assert.Eventually(t, func() bool { + graduallyIncreasedCount := countSeries(t, promRegistry) + fmt.Println("seriesCount: ", graduallyIncreasedCount) + if graduallyIncreasedCount < minSeriesCount { + t.Fatalf("Gradually increased series count should be less than maxSeriesCount %d but got %d", maxSeriesCount, graduallyIncreasedCount) + } + + return graduallyIncreasedCount == minSeriesCount + }, 15*time.Second, seriesChangeInterval*time.Second, "Did not receive update notification for series count gradual increase in time") +} + +// if min is bigger than maxSeriesCount, fail in GradualChange +func TestRunMetricsWithInvalidSeriesCounts(t *testing.T) { + const ( + metricCount = 1 + labelCount = 1 + seriesCount = 100 + maxSeriesCount = 10 + minSeriesCount = 100 + seriesChangeRate = 10 + metricLength = 1 + labelLength = 1 + valueInterval = 100 + seriesInterval = 100 + metricInterval = 100 + seriesChangeInterval = 3 + operationMode = "gradual-change" + constLabel = "constLabel=test" + ) + + stop := make(chan struct{}) + defer close(stop) + + promRegistry = prometheus.NewRegistry() + + _, err := RunMetrics(metricCount, labelCount, seriesCount, seriesChangeRate, maxSeriesCount, minSeriesCount, metricLength, labelLength, valueInterval, seriesInterval, metricInterval, seriesChangeInterval, operationMode, []string{constLabel}, stop) + assert.Error(t, err) +}