From 16f2caa352112e3795bd447b8dced1a28f3a4238 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Thu, 3 Sep 2020 02:56:52 -0700 Subject: [PATCH 01/16] Add default quantiles, update config tests, and add quantile tests --- exporters/metric/cortex/config.go | 15 +++++++++ exporters/metric/cortex/config_data_test.go | 34 +++++++++++++++++++-- exporters/metric/cortex/config_test.go | 12 ++++++++ exporters/metric/cortex/cortex_test.go | 3 +- 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/exporters/metric/cortex/config.go b/exporters/metric/cortex/config.go index 51fa2851b41..d03e97fd36c 100644 --- a/exporters/metric/cortex/config.go +++ b/exporters/metric/cortex/config.go @@ -41,6 +41,9 @@ var ( // ErrNoBasicAuthPassword occurs when no password or password file was provided for // basic authentication. ErrNoBasicAuthPassword = fmt.Errorf("no password or password file provided for basic authentication") + + // ErrInvalidQuantiles occurs when the supplied quantiles are not between 0 and 1. + ErrInvalidQuantiles = fmt.Errorf("cannot have quantiles that are less than 0 or greater than 1") ) // Config contains properties the Exporter uses to export metrics data to Cortex. @@ -86,6 +89,15 @@ func (c *Config) Validate() error { return ErrTwoBearerTokens } + // Verify that provided quantiles are between 0 and 1. + if c.Quantiles != nil { + for _, quantile := range c.Quantiles { + if quantile < 0 || quantile > 1 { + return ErrInvalidQuantiles + } + } + } + // Add default values for missing properties. if c.Endpoint == "" { c.Endpoint = "/api/prom/push" @@ -97,6 +109,9 @@ func (c *Config) Validate() error { if c.PushInterval == 0 { c.PushInterval = 10 * time.Second } + if c.Quantiles == nil { + c.Quantiles = []float64{0, 0.25, 0.5, 0.75, 1} + } return nil } diff --git a/exporters/metric/cortex/config_data_test.go b/exporters/metric/cortex/config_data_test.go index 46a8ce2b0a0..6a04eae36d2 100644 --- a/exporters/metric/cortex/config_data_test.go +++ b/exporters/metric/cortex/config_data_test.go @@ -26,6 +26,7 @@ var validatedStandardConfig = cortex.Config{ Name: "Config", RemoteTimeout: 30 * time.Second, PushInterval: 10 * time.Second, + Quantiles: []float64{0, 0.25, 0.5, 0.75, 1}, } // Config struct with default values other than the remote timeout. This is used to verify @@ -35,6 +36,17 @@ var validatedCustomTimeoutConfig = cortex.Config{ Name: "Config", RemoteTimeout: 10 * time.Second, PushInterval: 10 * time.Second, + Quantiles: []float64{0, 0.25, 0.5, 0.75, 1}, +} + +// Config struct with default values other than the quantiles. This is used to verify +// the output of Validate(). +var validatedQuantilesConfig = cortex.Config{ + Endpoint: "/api/prom/push", + Name: "Config", + RemoteTimeout: 30 * time.Second, + PushInterval: 10 * time.Second, + Quantiles: []float64{0, 0.5, 1}, } // Example Config struct with a custom remote timeout. @@ -110,7 +122,7 @@ var exampleTwoAuthConfig = cortex.Config{ BearerToken: "bearer_token", } -// Example Config struct with no password for basic authentication +// Example Config struct with no password for basic authentication. var exampleNoPasswordConfig = cortex.Config{ Endpoint: "/api/prom/push", Name: "Config", @@ -121,7 +133,7 @@ var exampleNoPasswordConfig = cortex.Config{ }, } -// Example Config struct with no password for basic authentication +// Example Config struct with no password for basic authentication. var exampleNoUsernameConfig = cortex.Config{ Endpoint: "/api/prom/push", Name: "Config", @@ -131,3 +143,21 @@ var exampleNoUsernameConfig = cortex.Config{ "password": "password", }, } + +// Example Config struct with invalid quantiles. +var exampleInvalidQuantilesConfig = cortex.Config{ + Endpoint: "/api/prom/push", + Name: "Config", + RemoteTimeout: 30 * time.Second, + PushInterval: 10 * time.Second, + Quantiles: []float64{0, 1, 2, 3}, +} + +// Example Config struct with valid quantiles. +var exampleValidQuantilesConfig = cortex.Config{ + Endpoint: "/api/prom/push", + Name: "Config", + RemoteTimeout: 30 * time.Second, + PushInterval: 10 * time.Second, + Quantiles: []float64{0, 0.5, 1}, +} diff --git a/exporters/metric/cortex/config_test.go b/exporters/metric/cortex/config_test.go index 18791f0f6fb..2300e54ccf1 100644 --- a/exporters/metric/cortex/config_test.go +++ b/exporters/metric/cortex/config_test.go @@ -91,6 +91,18 @@ func TestValidate(t *testing.T) { expectedConfig: nil, expectedError: cortex.ErrConflictingAuthorization, }, + { + testName: "Config with Invalid Quantiles", + config: &exampleInvalidQuantilesConfig, + expectedConfig: nil, + expectedError: cortex.ErrInvalidQuantiles, + }, + { + testName: "Config with Valid Quantiles", + config: &exampleValidQuantilesConfig, + expectedConfig: &validatedQuantilesConfig, + expectedError: nil, + }, } for _, test := range tests { t.Run(test.testName, func(t *testing.T) { diff --git a/exporters/metric/cortex/cortex_test.go b/exporters/metric/cortex/cortex_test.go index 0d4ca1a9c1a..662f0616d4b 100644 --- a/exporters/metric/cortex/cortex_test.go +++ b/exporters/metric/cortex/cortex_test.go @@ -62,7 +62,8 @@ var validConfig = Config{ "x-prometheus-remote-write-version": "0.1.0", "tenant-id": "123", }, - Client: http.DefaultClient, + Client: http.DefaultClient, + Quantiles: []float64{0, 0.25, 0.5, 0.75, 1}, } var testResource = resource.New(label.String("R", "V")) From 56664e0d5ef23abaae032020ddd630b140a8ff43 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Thu, 3 Sep 2020 03:24:13 -0700 Subject: [PATCH 02/16] Change sendRequest test to use non-empty request and verify the payload --- exporters/metric/cortex/cortex_test.go | 56 ++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/exporters/metric/cortex/cortex_test.go b/exporters/metric/cortex/cortex_test.go index 662f0616d4b..00aebe5bb1c 100644 --- a/exporters/metric/cortex/cortex_test.go +++ b/exporters/metric/cortex/cortex_test.go @@ -255,7 +255,7 @@ func verifyExporterRequest(req *http.Request) error { return fmt.Errorf("Request does not contain the three required headers") } - // Check body format and headers. + // Check whether request body is in the correct format. compressed, err := ioutil.ReadAll(req.Body) if err != nil { return fmt.Errorf("Failed to read request body") @@ -270,6 +270,29 @@ func verifyExporterRequest(req *http.Request) error { return fmt.Errorf("Failed to unmarshal message into WriteRequest struct") } + // Check whether the request contains the correct data. + expectedWriteRequest := &prompb.WriteRequest{ + Timeseries: []*prompb.TimeSeries{ + { + Samples: []prompb.Sample{ + prompb.Sample{ + Value: float64(123), + Timestamp: int64(time.Nanosecond) * time.Time{}.UnixNano() / int64(time.Millisecond), + }, + }, + Labels: []*prompb.Label{ + &prompb.Label{ + Name: "__name__", + Value: "test_name", + }, + }, + }, + }, + } + if !cmp.Equal(wr, expectedWriteRequest) { + return fmt.Errorf("request does not contain the expected contents") + } + return nil } @@ -298,9 +321,9 @@ func TestSendRequest(t *testing.T) { } // Set up a test server to receive the request. The server responds with a 400 Bad - // Request status code if any headers are missing or if the body is not of the correct - // format. Additionally, the server can respond with status code 404 Not Found to - // simulate send failures. + // Request status code if any headers are missing or if the body does not have the + // correct contents. Additionally, the server can respond with status code 404 Not + // Found to simulate send failures. handler := func(rw http.ResponseWriter, req *http.Request) { err := verifyExporterRequest(req) if err != nil { @@ -308,7 +331,8 @@ func TestSendRequest(t *testing.T) { return } - // Return a status code 400 if header isStatusNotFound is "true", 200 otherwise. + // Return a status code 400 if header isStatusNotFound is "true". Otherwise, + // return status code 200. if req.Header.Get("isStatusNotFound") == "true" { rw.WriteHeader(http.StatusNotFound) } else { @@ -328,8 +352,26 @@ func TestSendRequest(t *testing.T) { } exporter := Exporter{*test.config} - // Create an empty Snappy-compressed message. - msg, err := exporter.buildMessage([]*prompb.TimeSeries{}) + // Create a test TimeSeries struct. + timeSeries := []*prompb.TimeSeries{ + { + Samples: []prompb.Sample{ + prompb.Sample{ + Value: float64(123), + Timestamp: int64(time.Nanosecond) * time.Time{}.UnixNano() / int64(time.Millisecond), + }, + }, + Labels: []*prompb.Label{ + &prompb.Label{ + Name: "__name__", + Value: "test_name", + }, + }, + }, + } + + // Create a Snappy-compressed message. + msg, err := exporter.buildMessage(timeSeries) require.NoError(t, err) // Create a http POST request with the compressed message. From bb7bd784b8c632a4db9c35fb97dee97bf8028e5e Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Thu, 3 Sep 2020 03:46:27 -0700 Subject: [PATCH 03/16] Refactor createLabelSet to use label.KeyValue instead of strings --- exporters/metric/cortex/cortex.go | 57 +++++++++++++++---------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/exporters/metric/cortex/cortex.go b/exporters/metric/cortex/cortex.go index 2311b9f7b31..ff945342a41 100644 --- a/exporters/metric/cortex/cortex.go +++ b/exporters/metric/cortex/cortex.go @@ -187,7 +187,7 @@ func (e *Exporter) ConvertToTimeSeries(checkpointSet export.CheckpointSet) ([]*p } // createTimeSeries is a helper function to create a timeseries from a value and labels -func createTimeSeries(record metric.Record, value apimetric.Number, extraLabels ...string) *prompb.TimeSeries { +func createTimeSeries(record metric.Record, value apimetric.Number, extraLabels ...label.KeyValue) *prompb.TimeSeries { sample := prompb.Sample{ Value: value.CoerceToFloat64(record.Descriptor().NumberKind()), Timestamp: int64(time.Nanosecond) * record.EndTime().UnixNano() / int64(time.Millisecond), @@ -213,7 +213,7 @@ func convertFromSum(record metric.Record, sum aggregation.Sum) (*prompb.TimeSeri name := sanitize(record.Descriptor().Name()) // Note: Cortex requires the name label to be in the format "__name__". // This is the case for all time series created by this exporter. - tSeries := createTimeSeries(record, value, "__name__", name) + tSeries := createTimeSeries(record, value, label.String("__name__", name)) return tSeries, nil } @@ -228,7 +228,7 @@ func convertFromLastValue(record metric.Record, lastValue aggregation.LastValue) // Create TimeSeries name := sanitize(record.Descriptor().Name()) - tSeries := createTimeSeries(record, value, "__name__", name) + tSeries := createTimeSeries(record, value, label.String("__name__", name)) return tSeries, nil } @@ -241,7 +241,7 @@ func convertFromMinMaxSumCount(record metric.Record, minMaxSumCount aggregation. return nil, err } name := sanitize(record.Descriptor().Name() + "_min") - minTimeSeries := createTimeSeries(record, min, "__name__", name) + minTimeSeries := createTimeSeries(record, min, label.String("__name__", name)) // Convert Max max, err := minMaxSumCount.Max() @@ -249,7 +249,7 @@ func convertFromMinMaxSumCount(record metric.Record, minMaxSumCount aggregation. return nil, err } name = sanitize(record.Descriptor().Name() + "_max") - maxTimeSeries := createTimeSeries(record, max, "__name__", name) + maxTimeSeries := createTimeSeries(record, max, label.String("__name__", name)) // Convert Count // TODO: Refactor this to use createTimeSeries helper function @@ -264,7 +264,7 @@ func convertFromMinMaxSumCount(record metric.Record, minMaxSumCount aggregation. // Create labels, including metric name name = sanitize(record.Descriptor().Name() + "_count") - labels := createLabelSet(record, "__name__", name) + labels := createLabelSet(record, label.String("__name__", name)) // Create TimeSeries countTimeSeries := &prompb.TimeSeries{ @@ -296,7 +296,7 @@ func convertFromDistribution(record metric.Record, distribution aggregation.Dist quantileStr := strconv.FormatFloat(q, 'f', -1, 64) // Create TimeSeries - tSeries := createTimeSeries(record, value, "__name__", metricName, "quantile", quantileStr) + tSeries := createTimeSeries(record, value, label.String("__name__", metricName), label.String("quantile", quantileStr)) timeSeries = append(timeSeries, tSeries) } @@ -313,7 +313,7 @@ func convertFromHistogram(record metric.Record, histogram aggregation.Histogram) if err != nil { return nil, err } - sumTimeSeries := createTimeSeries(record, sum, "__name__", metricName+"_sum") + sumTimeSeries := createTimeSeries(record, sum, label.String("__name__", metricName+"_sum")) timeSeries = append(timeSeries, sumTimeSeries) // Handle Histogram buckets @@ -333,7 +333,7 @@ func convertFromHistogram(record metric.Record, histogram aggregation.Histogram) boundaryStr := strconv.FormatFloat(boundary, 'f', -1, 64) // Create timeSeries and append - tSeries := createTimeSeries(record, apimetric.NewFloat64Number(totalCount), "__name__", metricName, "le", boundaryStr) + tSeries := createTimeSeries(record, apimetric.NewFloat64Number(totalCount), label.String("__name__", metricName), label.String("le", boundaryStr)) timeSeries = append(timeSeries, tSeries) } @@ -342,23 +342,23 @@ func convertFromHistogram(record metric.Record, histogram aggregation.Histogram) // Create a timeSeries for the +inf bucket and total count // These are the same and are both required by Prometheus-based backends - upperBoundTimeSeries := createTimeSeries(record, apimetric.NewFloat64Number(totalCount), "__name__", metricName, "le", "+inf") + upperBoundTimeSeries := createTimeSeries(record, apimetric.NewFloat64Number(totalCount), label.String("__name__", metricName), label.String("le", "+inf")) timeSeries = append(timeSeries, upperBoundTimeSeries) - countTimeSeries := createTimeSeries(record, apimetric.NewFloat64Number(totalCount), "__name__", metricName+"_count") + countTimeSeries := createTimeSeries(record, apimetric.NewFloat64Number(totalCount), label.String("__name__", metricName+"_count")) timeSeries = append(timeSeries, countTimeSeries) return timeSeries, nil } -// createLabelSet combines labels from a Record, resource, and extra labels to -// create a slice of prompb.Label -func createLabelSet(record metric.Record, extras ...string) []*prompb.Label { - // Map ensure no duplicate label names +// createLabelSet combines labels from a Record, resource, and extra labels to create a +// slice of prompb.Label. +func createLabelSet(record metric.Record, extraLabels ...label.KeyValue) []*prompb.Label { + // Map ensure no duplicate label names. labelMap := map[string]prompb.Label{} - // mergeLabels merges Record and Resource labels into a single set, giving - // precedence to the record's labels. + // mergeLabels merges Record and Resource labels into a single set, giving precedence + // to the record's labels. mi := label.NewMergeIterator(record.Labels(), record.Resource().LabelSet()) for mi.Next() { label := mi.Label() @@ -369,23 +369,20 @@ func createLabelSet(record metric.Record, extras ...string) []*prompb.Label { } } - // Add extra labels created by the exporter like the metric name - // or labels to represent histogram buckets - for i := 0; i < len(extras); i += 2 { - // Ensure even number of extras (key : value) - if i+1 >= len(extras) { - break - } - + // Add extra labels created by the exporter like the metric name or labels to + // represent histogram buckets. + for _, label := range extraLabels { // Ensure label doesn't exist. If it does, notify user that a user created label // is being overwritten by a Prometheus reserved label (e.g. 'le' for histograms) - _, found := labelMap[extras[i]] + key := string(label.Key) + value := label.Value.AsString() + _, found := labelMap[key] if found { - log.Printf("Label %s is overwritten. Check if Prometheus reserved labels are used.\n", extras[i]) + log.Printf("Label %s is overwritten. Check if Prometheus reserved labels are used.\n", key) } - labelMap[extras[i]] = prompb.Label{ - Name: extras[i], - Value: extras[i+1], + labelMap[key] = prompb.Label{ + Name: key, + Value: value, } } From c859bd3b714afd1081843f273c124b7497403cc3 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Thu, 3 Sep 2020 03:55:12 -0700 Subject: [PATCH 04/16] Refactor ConvertToTimeSeries to add the correct labels for histogram and distribution timeseries --- exporters/metric/cortex/cortex.go | 110 +++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 23 deletions(-) diff --git a/exporters/metric/cortex/cortex.go b/exporters/metric/cortex/cortex.go index ff945342a41..5cee43937f6 100644 --- a/exporters/metric/cortex/cortex.go +++ b/exporters/metric/cortex/cortex.go @@ -127,48 +127,44 @@ func (e *Exporter) ConvertToTimeSeries(checkpointSet export.CheckpointSet) ([]*p // Convert based on aggregation type agg := record.Aggregation() - // Check if aggregation has Histogram value + // The following section uses loose type checking to determine how to + // convert aggregations to timeseries. More "expensive" timeseries are + // checked first. For example, because a Distribution has a Sum value, + // we must check for Distribution first or else only the Sum would be + // converted and the other values like Quantiles would not be. + // + // See the Aggregator Kind for more information + // https://github.com/open-telemetry/opentelemetry-go/blob/master/sdk/export/metric/aggregation/aggregation.go#L123-L138 if histogram, ok := agg.(aggregation.Histogram); ok { tSeries, err := convertFromHistogram(record, histogram) if err != nil { return err } timeSeries = append(timeSeries, tSeries...) - // Check if aggregation has sum value + } else if distribution, ok := agg.(aggregation.Distribution); ok && len(e.config.Quantiles) != 0 { + tSeries, err := convertFromDistribution(record, distribution, e.config.Quantiles) + if err != nil { + return err + } + timeSeries = append(timeSeries, tSeries...) } else if sum, ok := agg.(aggregation.Sum); ok { tSeries, err := convertFromSum(record, sum) if err != nil { return err } - timeSeries = append(timeSeries, tSeries) - - // Check if aggregation has MinMaxSumCount value if minMaxSumCount, ok := agg.(aggregation.MinMaxSumCount); ok { tSeries, err := convertFromMinMaxSumCount(record, minMaxSumCount) if err != nil { return err } - timeSeries = append(timeSeries, tSeries...) - - // Check if aggregation has a Distribution value - if distribution, ok := agg.(aggregation.Distribution); ok && len(e.config.Quantiles) != 0 { - tSeries, err := convertFromDistribution(record, distribution, e.config.Quantiles) - if err != nil { - return err - } - - timeSeries = append(timeSeries, tSeries...) - } } - // Check if aggregation has lastValue } else if lastValue, ok := agg.(aggregation.LastValue); ok { tSeries, err := convertFromLastValue(record, lastValue) if err != nil { return err } - timeSeries = append(timeSeries, tSeries) } else { // Report to the user when no conversion was found @@ -285,6 +281,55 @@ func convertFromDistribution(record metric.Record, distribution aggregation.Dist var timeSeries []*prompb.TimeSeries metricName := sanitize(record.Descriptor().Name()) + // Convert Min + min, err := distribution.Min() + if err != nil { + return nil, err + } + name := sanitize(metricName + "_min") + minTimeSeries := createTimeSeries(record, min, label.String("__name__", name)) + timeSeries = append(timeSeries, minTimeSeries) + + // Convert Max + max, err := distribution.Max() + if err != nil { + return nil, err + } + name = sanitize(metricName + "_max") + maxTimeSeries := createTimeSeries(record, max, label.String("__name__", name)) + timeSeries = append(timeSeries, maxTimeSeries) + + // Convert Sum + sum, err := distribution.Sum() + if err != nil { + return nil, err + } + name = sanitize(metricName + "_sum") + sumTimeSeries := createTimeSeries(record, sum, label.String("__name__", name)) + timeSeries = append(timeSeries, sumTimeSeries) + + // Convert Count + // TODO: Refactor this to use createTimeSeries helper function + count, err := distribution.Count() + if err != nil { + return nil, err + } + countSample := prompb.Sample{ + Value: float64(count), + Timestamp: int64(time.Nanosecond) * record.EndTime().UnixNano() / int64(time.Millisecond), + } + + // Create labels, including metric name + name = sanitize(metricName + "_count") + labels := createLabelSet(record, label.String("__name__", name)) + + // Create TimeSeries + countTimeSeries := &prompb.TimeSeries{ + Samples: []prompb.Sample{countSample}, + Labels: labels, + } + timeSeries = append(timeSeries, countTimeSeries) + // For each configured quantile, get the value and create a timeseries for _, q := range quantiles { value, err := distribution.Quantile(q) @@ -325,15 +370,25 @@ func convertFromHistogram(record metric.Record, histogram aggregation.Histogram) var totalCount float64 // counts maps from the bucket upper-bound to the cumulative count. // The bucket with upper-bound +inf is not included. + counts := make(map[float64]float64, len(buckets.Boundaries)) for i, boundary := range buckets.Boundaries { // Add bucket count to totalCount and record in map totalCount += buckets.Counts[i] + counts[boundary] = totalCount - // Add lowerbound as a label. e.g. {le="5"} + // Add upper boundary as a label. e.g. {le="5"} boundaryStr := strconv.FormatFloat(boundary, 'f', -1, 64) // Create timeSeries and append - tSeries := createTimeSeries(record, apimetric.NewFloat64Number(totalCount), label.String("__name__", metricName), label.String("le", boundaryStr)) + sample := prompb.Sample{ + Value: totalCount, + Timestamp: record.EndTime().UnixNano() / int64(time.Millisecond), + } + labels := createLabelSet(record, label.String("__name__", metricName), label.String("le", boundaryStr)) + tSeries := &prompb.TimeSeries{ + Samples: []prompb.Sample{sample}, + Labels: labels, + } timeSeries = append(timeSeries, tSeries) } @@ -342,10 +397,19 @@ func convertFromHistogram(record metric.Record, histogram aggregation.Histogram) // Create a timeSeries for the +inf bucket and total count // These are the same and are both required by Prometheus-based backends - upperBoundTimeSeries := createTimeSeries(record, apimetric.NewFloat64Number(totalCount), label.String("__name__", metricName), label.String("le", "+inf")) + sample := prompb.Sample{ + Value: totalCount, + Timestamp: record.EndTime().UnixNano() / int64(time.Millisecond), + } + upperBoundTimeSeries := &prompb.TimeSeries{ + Samples: []prompb.Sample{sample}, + Labels: createLabelSet(record, label.String("__name__", metricName), label.String("le", "+inf")), + } + countTimeSeries := &prompb.TimeSeries{ + Samples: []prompb.Sample{sample}, + Labels: createLabelSet(record, label.String("__name__", metricName+"_count")), + } timeSeries = append(timeSeries, upperBoundTimeSeries) - - countTimeSeries := createTimeSeries(record, apimetric.NewFloat64Number(totalCount), label.String("__name__", metricName+"_count")) timeSeries = append(timeSeries, countTimeSeries) return timeSeries, nil From 39afe9b974655e9b5a06060c1f2476b001ce48dd Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Thu, 3 Sep 2020 04:58:20 -0700 Subject: [PATCH 05/16] Change createTimeSeries to use specified NumberKind and update conversion functions --- exporters/metric/cortex/cortex.go | 94 ++++++++++--------------------- 1 file changed, 29 insertions(+), 65 deletions(-) diff --git a/exporters/metric/cortex/cortex.go b/exporters/metric/cortex/cortex.go index 5cee43937f6..0f689f4803f 100644 --- a/exporters/metric/cortex/cortex.go +++ b/exporters/metric/cortex/cortex.go @@ -183,9 +183,9 @@ func (e *Exporter) ConvertToTimeSeries(checkpointSet export.CheckpointSet) ([]*p } // createTimeSeries is a helper function to create a timeseries from a value and labels -func createTimeSeries(record metric.Record, value apimetric.Number, extraLabels ...label.KeyValue) *prompb.TimeSeries { +func createTimeSeries(record metric.Record, value apimetric.Number, valueNumberKind apimetric.NumberKind, extraLabels ...label.KeyValue) *prompb.TimeSeries { sample := prompb.Sample{ - Value: value.CoerceToFloat64(record.Descriptor().NumberKind()), + Value: value.CoerceToFloat64(valueNumberKind), Timestamp: int64(time.Nanosecond) * record.EndTime().UnixNano() / int64(time.Millisecond), } @@ -205,11 +205,11 @@ func convertFromSum(record metric.Record, sum aggregation.Sum) (*prompb.TimeSeri return nil, err } - // Create TimeSeries + // Create TimeSeries. Note that Cortex requires the name label to be in the format + // "__name__". This is the case for all time series created by this exporter. name := sanitize(record.Descriptor().Name()) - // Note: Cortex requires the name label to be in the format "__name__". - // This is the case for all time series created by this exporter. - tSeries := createTimeSeries(record, value, label.String("__name__", name)) + numberKind := record.Descriptor().NumberKind() + tSeries := createTimeSeries(record, value, numberKind, label.String("__name__", name)) return tSeries, nil } @@ -224,20 +224,23 @@ func convertFromLastValue(record metric.Record, lastValue aggregation.LastValue) // Create TimeSeries name := sanitize(record.Descriptor().Name()) - tSeries := createTimeSeries(record, value, label.String("__name__", name)) + numberKind := record.Descriptor().NumberKind() + tSeries := createTimeSeries(record, value, numberKind, label.String("__name__", name)) return tSeries, nil } // convertFromMinMaxSumCount returns 4 TimeSeries for the min, max, sum, and count from the mmsc aggregation func convertFromMinMaxSumCount(record metric.Record, minMaxSumCount aggregation.MinMaxSumCount) ([]*prompb.TimeSeries, error) { + numberKind := record.Descriptor().NumberKind() + // Convert Min min, err := minMaxSumCount.Min() if err != nil { return nil, err } name := sanitize(record.Descriptor().Name() + "_min") - minTimeSeries := createTimeSeries(record, min, label.String("__name__", name)) + minTimeSeries := createTimeSeries(record, min, numberKind, label.String("__name__", name)) // Convert Max max, err := minMaxSumCount.Max() @@ -245,28 +248,15 @@ func convertFromMinMaxSumCount(record metric.Record, minMaxSumCount aggregation. return nil, err } name = sanitize(record.Descriptor().Name() + "_max") - maxTimeSeries := createTimeSeries(record, max, label.String("__name__", name)) + maxTimeSeries := createTimeSeries(record, max, numberKind, label.String("__name__", name)) // Convert Count - // TODO: Refactor this to use createTimeSeries helper function count, err := minMaxSumCount.Count() if err != nil { return nil, err } - countSample := prompb.Sample{ - Value: float64(count), - Timestamp: int64(time.Nanosecond) * record.EndTime().UnixNano() / int64(time.Millisecond), - } - - // Create labels, including metric name name = sanitize(record.Descriptor().Name() + "_count") - labels := createLabelSet(record, label.String("__name__", name)) - - // Create TimeSeries - countTimeSeries := &prompb.TimeSeries{ - Samples: []prompb.Sample{countSample}, - Labels: labels, - } + countTimeSeries := createTimeSeries(record, apimetric.NewInt64Number(count), apimetric.Int64NumberKind, label.String("__name__", name)) // Return all timeSeries tSeries := []*prompb.TimeSeries{ @@ -280,6 +270,7 @@ func convertFromMinMaxSumCount(record metric.Record, minMaxSumCount aggregation. func convertFromDistribution(record metric.Record, distribution aggregation.Distribution, quantiles []float64) ([]*prompb.TimeSeries, error) { var timeSeries []*prompb.TimeSeries metricName := sanitize(record.Descriptor().Name()) + numberKind := record.Descriptor().NumberKind() // Convert Min min, err := distribution.Min() @@ -287,7 +278,7 @@ func convertFromDistribution(record metric.Record, distribution aggregation.Dist return nil, err } name := sanitize(metricName + "_min") - minTimeSeries := createTimeSeries(record, min, label.String("__name__", name)) + minTimeSeries := createTimeSeries(record, min, numberKind, label.String("__name__", name)) timeSeries = append(timeSeries, minTimeSeries) // Convert Max @@ -296,7 +287,7 @@ func convertFromDistribution(record metric.Record, distribution aggregation.Dist return nil, err } name = sanitize(metricName + "_max") - maxTimeSeries := createTimeSeries(record, max, label.String("__name__", name)) + maxTimeSeries := createTimeSeries(record, max, numberKind, label.String("__name__", name)) timeSeries = append(timeSeries, maxTimeSeries) // Convert Sum @@ -305,29 +296,16 @@ func convertFromDistribution(record metric.Record, distribution aggregation.Dist return nil, err } name = sanitize(metricName + "_sum") - sumTimeSeries := createTimeSeries(record, sum, label.String("__name__", name)) + sumTimeSeries := createTimeSeries(record, sum, numberKind, label.String("__name__", name)) timeSeries = append(timeSeries, sumTimeSeries) // Convert Count - // TODO: Refactor this to use createTimeSeries helper function count, err := distribution.Count() if err != nil { return nil, err } - countSample := prompb.Sample{ - Value: float64(count), - Timestamp: int64(time.Nanosecond) * record.EndTime().UnixNano() / int64(time.Millisecond), - } - - // Create labels, including metric name - name = sanitize(metricName + "_count") - labels := createLabelSet(record, label.String("__name__", name)) - - // Create TimeSeries - countTimeSeries := &prompb.TimeSeries{ - Samples: []prompb.Sample{countSample}, - Labels: labels, - } + name = sanitize(record.Descriptor().Name() + "_count") + countTimeSeries := createTimeSeries(record, apimetric.NewInt64Number(count), apimetric.Int64NumberKind, label.String("__name__", name)) timeSeries = append(timeSeries, countTimeSeries) // For each configured quantile, get the value and create a timeseries @@ -341,7 +319,7 @@ func convertFromDistribution(record metric.Record, distribution aggregation.Dist quantileStr := strconv.FormatFloat(q, 'f', -1, 64) // Create TimeSeries - tSeries := createTimeSeries(record, value, label.String("__name__", metricName), label.String("quantile", quantileStr)) + tSeries := createTimeSeries(record, value, numberKind, label.String("__name__", metricName), label.String("quantile", quantileStr)) timeSeries = append(timeSeries, tSeries) } @@ -352,13 +330,14 @@ func convertFromDistribution(record metric.Record, distribution aggregation.Dist func convertFromHistogram(record metric.Record, histogram aggregation.Histogram) ([]*prompb.TimeSeries, error) { var timeSeries []*prompb.TimeSeries metricName := sanitize(record.Descriptor().Name()) + numberKind := record.Descriptor().NumberKind() // Create Sum TimeSeries sum, err := histogram.Sum() if err != nil { return nil, err } - sumTimeSeries := createTimeSeries(record, sum, label.String("__name__", metricName+"_sum")) + sumTimeSeries := createTimeSeries(record, sum, numberKind, label.String("__name__", metricName+"_sum")) timeSeries = append(timeSeries, sumTimeSeries) // Handle Histogram buckets @@ -380,16 +359,8 @@ func convertFromHistogram(record metric.Record, histogram aggregation.Histogram) boundaryStr := strconv.FormatFloat(boundary, 'f', -1, 64) // Create timeSeries and append - sample := prompb.Sample{ - Value: totalCount, - Timestamp: record.EndTime().UnixNano() / int64(time.Millisecond), - } - labels := createLabelSet(record, label.String("__name__", metricName), label.String("le", boundaryStr)) - tSeries := &prompb.TimeSeries{ - Samples: []prompb.Sample{sample}, - Labels: labels, - } - timeSeries = append(timeSeries, tSeries) + boundaryTimeSeries := createTimeSeries(record, apimetric.NewFloat64Number(totalCount), apimetric.Float64NumberKind, label.String("__name__", metricName), label.String("le", boundaryStr)) + timeSeries = append(timeSeries, boundaryTimeSeries) } // Include the +inf boundary in the total count @@ -397,18 +368,11 @@ func convertFromHistogram(record metric.Record, histogram aggregation.Histogram) // Create a timeSeries for the +inf bucket and total count // These are the same and are both required by Prometheus-based backends - sample := prompb.Sample{ - Value: totalCount, - Timestamp: record.EndTime().UnixNano() / int64(time.Millisecond), - } - upperBoundTimeSeries := &prompb.TimeSeries{ - Samples: []prompb.Sample{sample}, - Labels: createLabelSet(record, label.String("__name__", metricName), label.String("le", "+inf")), - } - countTimeSeries := &prompb.TimeSeries{ - Samples: []prompb.Sample{sample}, - Labels: createLabelSet(record, label.String("__name__", metricName+"_count")), - } + + upperBoundTimeSeries := createTimeSeries(record, apimetric.NewFloat64Number(totalCount), apimetric.Float64NumberKind, label.String("__name__", metricName), label.String("le", "+inf")) + + countTimeSeries := createTimeSeries(record, apimetric.NewFloat64Number(totalCount), apimetric.Float64NumberKind, label.String("__name__", metricName+"_count")) + timeSeries = append(timeSeries, upperBoundTimeSeries) timeSeries = append(timeSeries, countTimeSeries) From 68804b5eb0479785bae964697f79bd7fe7abe4e8 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Thu, 3 Sep 2020 05:01:37 -0700 Subject: [PATCH 06/16] Remove validCheckpointSet test --- exporters/metric/cortex/cortex_test.go | 6 ------ exporters/metric/cortex/testutil_test.go | 22 ++-------------------- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/exporters/metric/cortex/cortex_test.go b/exporters/metric/cortex/cortex_test.go index 00aebe5bb1c..19d2d840160 100644 --- a/exporters/metric/cortex/cortex_test.go +++ b/exporters/metric/cortex/cortex_test.go @@ -94,12 +94,6 @@ func TestConvertToTimeSeries(t *testing.T) { want []*prompb.TimeSeries wantLength int }{ - { - name: "validCheckpointSet", - input: getValidCheckpointSet(t), - want: wantValidCheckpointSet, - wantLength: 1, - }, { name: "convertFromSum", input: getSumCheckpoint(t, 321), diff --git a/exporters/metric/cortex/testutil_test.go b/exporters/metric/cortex/testutil_test.go index a4258c24c71..52abc1ff8c7 100644 --- a/exporters/metric/cortex/testutil_test.go +++ b/exporters/metric/cortex/testutil_test.go @@ -118,26 +118,8 @@ func getHistogramCheckpoint(t *testing.T) export.CheckpointSet { return checkpointSet } -// The following variables hold expected TimeSeries values to be used in ConvertToTimeSeries tests -var wantValidCheckpointSet = []*prompb.TimeSeries{ - { - Labels: []*prompb.Label{ - { - Name: "R", - Value: "V", - }, - { - Name: "__name__", - Value: "metric_name", - }, - }, - Samples: []prompb.Sample{{ - Value: 321, - Timestamp: mockTime, - }}, - }, -} - +// The following variables hold expected TimeSeries values to be used in +// ConvertToTimeSeries tests. var wantSumCheckpointSet = []*prompb.TimeSeries{ { Labels: []*prompb.Label{ From 35bea1e1f5de5a0d29ee3bf62373faf135eb4cc8 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Thu, 3 Sep 2020 05:03:38 -0700 Subject: [PATCH 07/16] Change mock time to milliseconds for conversion tests --- exporters/metric/cortex/cortex_test.go | 39 +++++++++++++------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/exporters/metric/cortex/cortex_test.go b/exporters/metric/cortex/cortex_test.go index 19d2d840160..f63bd394104 100644 --- a/exporters/metric/cortex/cortex_test.go +++ b/exporters/metric/cortex/cortex_test.go @@ -67,7 +67,7 @@ var validConfig = Config{ } var testResource = resource.New(label.String("R", "V")) -var mockTime int64 = time.Time{}.Unix() +var mockTime int64 = int64(time.Nanosecond) * time.Time{}.UnixNano() / int64(time.Millisecond) func TestExportKindFor(t *testing.T) { exporter := Exporter{} @@ -106,24 +106,24 @@ func TestConvertToTimeSeries(t *testing.T) { want: wantLastValueCheckpointSet, wantLength: 1, }, - { - name: "convertFromMinMaxSumCount", - input: getMMSCCheckpoint(t, 123.456, 876.543), - want: wantMMSCCheckpointSet, - wantLength: 4, - }, - { - name: "convertFromDistribution", - input: getDistributionCheckpoint(t), - want: wantDistributionCheckpointSet, - wantLength: 7, - }, - { - name: "convertFromHistogram", - input: getHistogramCheckpoint(t), - want: wantHistogramCheckpointSet, - wantLength: 6, - }, + // { + // name: "convertFromMinMaxSumCount", + // input: getMMSCCheckpoint(t, 123.456, 876.543), + // want: wantMMSCCheckpointSet, + // wantLength: 4, + // }, + // { + // name: "convertFromDistribution", + // input: getDistributionCheckpoint(t), + // want: wantDistributionCheckpointSet, + // wantLength: 7, + // }, + // { + // name: "convertFromHistogram", + // input: getHistogramCheckpoint(t), + // want: wantHistogramCheckpointSet, + // wantLength: 6, + // }, } for _, tt := range tests { @@ -133,6 +133,7 @@ func TestConvertToTimeSeries(t *testing.T) { assert.Nil(t, err, "ConvertToTimeSeries error") assert.Len(t, got, tt.wantLength, "Incorrect number of timeseries") + assert.Equal(t, got, want) cmp.Equal(got, want) }) } From 9301e7a7ae165add45fceec3ce51e365cd9ada79 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Thu, 3 Sep 2020 06:11:00 -0700 Subject: [PATCH 08/16] Fix error where results in TestConvertToTimeSeries weren't being compared --- exporters/metric/cortex/cortex_test.go | 66 +++++++++++++++++------- exporters/metric/cortex/testutil_test.go | 22 ++++---- 2 files changed, 57 insertions(+), 31 deletions(-) diff --git a/exporters/metric/cortex/cortex_test.go b/exporters/metric/cortex/cortex_test.go index f63bd394104..3746871ab1a 100644 --- a/exporters/metric/cortex/cortex_test.go +++ b/exporters/metric/cortex/cortex_test.go @@ -106,24 +106,24 @@ func TestConvertToTimeSeries(t *testing.T) { want: wantLastValueCheckpointSet, wantLength: 1, }, - // { - // name: "convertFromMinMaxSumCount", - // input: getMMSCCheckpoint(t, 123.456, 876.543), - // want: wantMMSCCheckpointSet, - // wantLength: 4, - // }, - // { - // name: "convertFromDistribution", - // input: getDistributionCheckpoint(t), - // want: wantDistributionCheckpointSet, - // wantLength: 7, - // }, - // { - // name: "convertFromHistogram", - // input: getHistogramCheckpoint(t), - // want: wantHistogramCheckpointSet, - // wantLength: 6, - // }, + { + name: "convertFromMinMaxSumCount", + input: getMMSCCheckpoint(t, 123.456, 876.543), + want: wantMMSCCheckpointSet, + wantLength: 4, + }, + { + name: "convertFromDistribution", + input: getDistributionCheckpoint(t), + want: wantDistributionCheckpointSet, + wantLength: 7, + }, + { + name: "convertFromHistogram", + input: getHistogramCheckpoint(t), + want: wantHistogramCheckpointSet, + wantLength: 6, + }, } for _, tt := range tests { @@ -131,10 +131,36 @@ func TestConvertToTimeSeries(t *testing.T) { got, err := exporter.ConvertToTimeSeries(tt.input) want := tt.want + // Check for errors and for the correct number of timeseries. assert.Nil(t, err, "ConvertToTimeSeries error") assert.Len(t, got, tt.wantLength, "Incorrect number of timeseries") - assert.Equal(t, got, want) - cmp.Equal(got, want) + + // The TimeSeries cannot be compared easily using assert.ElementsMatch or + // cmp.Equal since both the ordering of the timeseries and the ordering of the + // labels inside each timeseries can change. To get around this, all the + // labels and samples are added to maps first. There aren't many labels or + // samples, so this nested loop shouldn't be a bottleneck. + gotLabels := make(map[string]bool) + wantLabels := make(map[string]bool) + gotSamples := make(map[string]bool) + wantSamples := make(map[string]bool) + + for i := 0; i < len(got); i++ { + for _, label := range got[i].Labels { + gotLabels[label.String()] = true + } + for _, label := range want[i].Labels { + wantLabels[label.String()] = true + } + for _, sample := range got[i].Samples { + gotSamples[sample.String()] = true + } + for _, sample := range want[i].Samples { + wantSamples[sample.String()] = true + } + } + assert.Equal(t, gotLabels, wantLabels) + assert.Equal(t, gotSamples, wantSamples) }) } } diff --git a/exporters/metric/cortex/testutil_test.go b/exporters/metric/cortex/testutil_test.go index 52abc1ff8c7..1ed91cf4220 100644 --- a/exporters/metric/cortex/testutil_test.go +++ b/exporters/metric/cortex/testutil_test.go @@ -193,14 +193,14 @@ var wantMMSCCheckpointSet = []*prompb.TimeSeries{ }, { Labels: []*prompb.Label{ - { - Name: "__name__", - Value: "metric_name_max", - }, { Name: "R", Value: "V", }, + { + Name: "__name__", + Value: "metric_name_max", + }, }, Samples: []prompb.Sample{{ Value: 876.543, @@ -234,7 +234,7 @@ var wantDistributionCheckpointSet = []*prompb.TimeSeries{ }, { Name: "__name__", - Value: "metric_name", + Value: "metric_name_sum", }, }, Samples: []prompb.Sample{{ @@ -260,14 +260,14 @@ var wantDistributionCheckpointSet = []*prompb.TimeSeries{ }, { Labels: []*prompb.Label{ - { - Name: "__name__", - Value: "metric_name_max", - }, { Name: "R", Value: "V", }, + { + Name: "__name__", + Value: "metric_name_max", + }, }, Samples: []prompb.Sample{{ Value: 999.5, @@ -318,7 +318,7 @@ var wantDistributionCheckpointSet = []*prompb.TimeSeries{ }, { Name: "__name__", - Value: "metric_name_count", + Value: "metric_name", }, { Name: "quantile", @@ -338,7 +338,7 @@ var wantDistributionCheckpointSet = []*prompb.TimeSeries{ }, { Name: "__name__", - Value: "metric_name_count", + Value: "metric_name", }, { Name: "quantile", From 18afe4fc19ee83a4aaf6374a21bee3e6f39456c0 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Thu, 3 Sep 2020 06:13:39 -0700 Subject: [PATCH 09/16] Update convertFromSum, convertFromLastValue tests to use multiple values --- exporters/metric/cortex/cortex_test.go | 4 ++-- exporters/metric/cortex/testutil_test.go | 16 ++++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/exporters/metric/cortex/cortex_test.go b/exporters/metric/cortex/cortex_test.go index 3746871ab1a..c871800848f 100644 --- a/exporters/metric/cortex/cortex_test.go +++ b/exporters/metric/cortex/cortex_test.go @@ -96,13 +96,13 @@ func TestConvertToTimeSeries(t *testing.T) { }{ { name: "convertFromSum", - input: getSumCheckpoint(t, 321), + input: getSumCheckpoint(t, 1, 2, 3, 4, 5), want: wantSumCheckpointSet, wantLength: 1, }, { name: "convertFromLastValue", - input: getLastValueCheckpoint(t, 123), + input: getLastValueCheckpoint(t, 1, 2, 3, 4, 5), want: wantLastValueCheckpointSet, wantLength: 1, }, diff --git a/exporters/metric/cortex/testutil_test.go b/exporters/metric/cortex/testutil_test.go index 1ed91cf4220..81c720a35e3 100644 --- a/exporters/metric/cortex/testutil_test.go +++ b/exporters/metric/cortex/testutil_test.go @@ -37,14 +37,16 @@ func getValidCheckpointSet(t *testing.T) export.CheckpointSet { } // getSumCheckpoint returns a checkpoint set with a sum aggregation record -func getSumCheckpoint(t *testing.T, value int64) export.CheckpointSet { +func getSumCheckpoint(t *testing.T, values ...int64) export.CheckpointSet { // Create checkpoint set with resource and descriptor checkpointSet := metrictest.NewCheckpointSet(testResource) desc := metric.NewDescriptor("metric_name", metric.CounterKind, metric.Int64NumberKind) // Create aggregation, add value, and update checkpointset agg, ckpt := metrictest.Unslice2(sum.New(2)) - aggregatortest.CheckedUpdate(t, agg, metric.NewInt64Number(value), &desc) + for _, value := range values { + aggregatortest.CheckedUpdate(t, agg, metric.NewInt64Number(value), &desc) + } require.NoError(t, agg.SynchronizedMove(ckpt, &desc)) checkpointSet.Add(&desc, ckpt) @@ -52,14 +54,16 @@ func getSumCheckpoint(t *testing.T, value int64) export.CheckpointSet { } // getLastValueCheckpoint returns a checkpoint set with a last value aggregation record -func getLastValueCheckpoint(t *testing.T, value int64) export.CheckpointSet { +func getLastValueCheckpoint(t *testing.T, values ...int64) export.CheckpointSet { // Create checkpoint set with resource and descriptor checkpointSet := metrictest.NewCheckpointSet(testResource) desc := metric.NewDescriptor("metric_name", metric.ValueObserverKind, metric.Int64NumberKind) // Create aggregation, add value, and update checkpointset agg, ckpt := metrictest.Unslice2(lastvalue.New(2)) - aggregatortest.CheckedUpdate(t, agg, metric.NewInt64Number(value), &desc) + for _, value := range values { + aggregatortest.CheckedUpdate(t, agg, metric.NewInt64Number(value), &desc) + } require.NoError(t, agg.SynchronizedMove(ckpt, &desc)) checkpointSet.Add(&desc, ckpt) @@ -133,7 +137,7 @@ var wantSumCheckpointSet = []*prompb.TimeSeries{ }, }, Samples: []prompb.Sample{{ - Value: 321, + Value: 15, Timestamp: mockTime, }}, }, @@ -152,7 +156,7 @@ var wantLastValueCheckpointSet = []*prompb.TimeSeries{ }, }, Samples: []prompb.Sample{{ - Value: 123, + Value: 5, Timestamp: mockTime, }}, }, From 8d0eec4ac55315e1448e71cbeed611eddcb14609 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Thu, 3 Sep 2020 09:24:36 -0700 Subject: [PATCH 10/16] Add tests for quantiles and distributions in utils module --- .../cortex/utils/config_utils_data_test.go | 109 +++++++++++++++++- .../metric/cortex/utils/config_utils_test.go | 16 +++ exporters/metric/cortex/utils/go.sum | 3 + 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/exporters/metric/cortex/utils/config_utils_data_test.go b/exporters/metric/cortex/utils/config_utils_data_test.go index fa0d38cef69..a7977e6339e 100644 --- a/exporters/metric/cortex/utils/config_utils_data_test.go +++ b/exporters/metric/cortex/utils/config_utils_data_test.go @@ -72,8 +72,8 @@ headers: test: header `) -// YAML file with both password and password_file properties. It should fail to produce a Config -// struct since password and password_file are mutually exclusive. +// YAML file with both password and password_file properties. It should fail to produce a +// Config struct since password and password_file are mutually exclusive. var twoPasswordsYAML = []byte(`url: /api/prom/push remote_timeout: 30s name: Valid Config Example @@ -91,8 +91,9 @@ headers: test: header `) -// YAML file with both bearer_token and bearer_token_file properties. It should fail to produce a -// Config struct since bearer_token and bearer_token_file are mutually exclusive. +// YAML file with both bearer_token and bearer_token_file properties. It should fail to +// produce a Config struct since bearer_token and bearer_token_file are mutually +// exclusive. var twoBearerTokensYAML = []byte(`url: /api/prom/push remote_timeout: 30s name: Valid Config Example @@ -108,6 +109,51 @@ headers: test: header `) +// YAML file that sets custom quantiles and produces a Config struct without errors. +var quantilesYAML = []byte(`url: /api/prom/push +remote_timeout: 30s +push_interval: 5s +name: Valid Config Example +basic_auth: + username: user + password: password +tls_config: + ca_file: cafile + cert_file: certfile + key_file: keyfile + server_name: server + insecure_skip_verify: true +headers: + test: header +quantiles: + - 0 + - 0.5 + - 1 +`) + +// YAML file that sets custom histogram bucket boundaries and produces a Config struct +// without errors. +var bucketBoundariesYAML = []byte(`url: /api/prom/push +remote_timeout: 30s +push_interval: 5s +name: Valid Config Example +basic_auth: + username: user + password: password +tls_config: + ca_file: cafile + cert_file: certfile + key_file: keyfile + server_name: server + insecure_skip_verify: true +headers: + test: header +histogram_boundaries: + - 100 + - 300 + - 500 +`) + // ValidConfig is the resulting Config struct from reading validYAML. var validConfig = cortex.Config{ Endpoint: "/api/prom/push", @@ -131,4 +177,59 @@ var validConfig = cortex.Config{ Headers: map[string]string{ "test": "header", }, + Quantiles: []float64{0, 0.25, 0.5, 0.75, 1}, +} + +// customQuantilesConfig is the resulting Config struct from reading quantilesYAML. +var customQuantilesConfig = cortex.Config{ + Endpoint: "/api/prom/push", + RemoteTimeout: 30 * time.Second, + Name: "Valid Config Example", + BasicAuth: map[string]string{ + "username": "user", + "password": "password", + }, + BearerToken: "", + BearerTokenFile: "", + TLSConfig: map[string]string{ + "ca_file": "cafile", + "cert_file": "certfile", + "key_file": "keyfile", + "server_name": "server", + "insecure_skip_verify": "1", + }, + ProxyURL: nil, + PushInterval: 5 * time.Second, + Headers: map[string]string{ + "test": "header", + }, + Quantiles: []float64{0, 0.5, 1}, +} + +// customBucketBoundariesConfig is the resulting Config struct from reading +// bucketBoundariesYAML. +var customBucketBoundariesConfig = cortex.Config{ + Endpoint: "/api/prom/push", + RemoteTimeout: 30 * time.Second, + Name: "Valid Config Example", + BasicAuth: map[string]string{ + "username": "user", + "password": "password", + }, + BearerToken: "", + BearerTokenFile: "", + TLSConfig: map[string]string{ + "ca_file": "cafile", + "cert_file": "certfile", + "key_file": "keyfile", + "server_name": "server", + "insecure_skip_verify": "1", + }, + ProxyURL: nil, + PushInterval: 5 * time.Second, + Headers: map[string]string{ + "test": "header", + }, + Quantiles: []float64{0, 0.25, 0.5, 0.75, 1}, + HistogramBoundaries: []float64{100, 300, 500}, } diff --git a/exporters/metric/cortex/utils/config_utils_test.go b/exporters/metric/cortex/utils/config_utils_test.go index 72d99bfed21..da89a2e7665 100644 --- a/exporters/metric/cortex/utils/config_utils_test.go +++ b/exporters/metric/cortex/utils/config_utils_test.go @@ -98,6 +98,22 @@ func TestNewConfig(t *testing.T) { expectedConfig: nil, expectedError: cortex.ErrTwoBearerTokens, }, + { + testName: "Custom Quantiles", + yamlByteString: quantilesYAML, + fileName: "config.yml", + directoryPath: "/test", + expectedConfig: &customQuantilesConfig, + expectedError: nil, + }, + { + testName: "Custom Histogram Boundaries", + yamlByteString: bucketBoundariesYAML, + fileName: "config.yml", + directoryPath: "/test", + expectedConfig: &customBucketBoundariesConfig, + expectedError: nil, + }, } for _, test := range tests { diff --git a/exporters/metric/cortex/utils/go.sum b/exporters/metric/cortex/utils/go.sum index afe45a242b7..97a662f96f4 100644 --- a/exporters/metric/cortex/utils/go.sum +++ b/exporters/metric/cortex/utils/go.sum @@ -198,6 +198,7 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -211,6 +212,8 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opentelemetry.io v0.1.0 h1:EANZoRCOP+A3faIlw/iN6YEWoYb1vleZRKm1EvH8T48= +go.opentelemetry.io/contrib v0.11.0 h1:EQOdk+fxs7qp3wVIS5wCinwqNHfhD/DreQRY/VADO8s= go.opentelemetry.io/otel v0.11.0 h1:IN2tzQa9Gc4ZVKnTaMbPVcHjvzOdg5n9QfnmlqiET7E= go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= go.opentelemetry.io/otel/sdk v0.11.0 h1:bkDMymVj6gIkPfgC5ci5atq0OYbfUHSn8NvsmyfyMq4= From bb48b6914c0b285357d25bb46e18bd1b0e553af0 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Thu, 3 Sep 2020 09:40:59 -0700 Subject: [PATCH 11/16] Update docker-compose and reduce sleep time in main.go for example project --- exporters/metric/cortex/example/docker-compose.yml | 3 +++ exporters/metric/cortex/example/go.mod | 4 ++-- exporters/metric/cortex/example/main.go | 10 +++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/exporters/metric/cortex/example/docker-compose.yml b/exporters/metric/cortex/example/docker-compose.yml index 0f1c892dfce..fdbc7890bf4 100644 --- a/exporters/metric/cortex/example/docker-compose.yml +++ b/exporters/metric/cortex/example/docker-compose.yml @@ -19,8 +19,11 @@ services: image: golang:1.14-alpine volumes: - .:/app + - ../:/cortex/ working_dir: /app command: go run main.go + ports: + - 8888:8888 cortex: image: quay.io/cortexproject/cortex:v1.3.0-rc.1 command: diff --git a/exporters/metric/cortex/example/go.mod b/exporters/metric/cortex/example/go.mod index 8561ca41733..476f1bd6633 100644 --- a/exporters/metric/cortex/example/go.mod +++ b/exporters/metric/cortex/example/go.mod @@ -3,8 +3,8 @@ module go.opentelemetry.io/contrib/exporters/metric/cortex/example go 1.14 replace ( - go.opentelemetry.io/contrib/exporters/metric/cortex => ../ - go.opentelemetry.io/contrib/exporters/metric/cortex/utils => ../utils + go.opentelemetry.io/contrib/exporters/metric/cortex => ../cortex/ + go.opentelemetry.io/contrib/exporters/metric/cortex/utils => ../cortex/utils ) require ( diff --git a/exporters/metric/cortex/example/main.go b/exporters/metric/cortex/example/main.go index 349f177c357..2dde52d21ce 100644 --- a/exporters/metric/cortex/example/main.go +++ b/exporters/metric/cortex/example/main.go @@ -52,22 +52,22 @@ func main() { ctx := context.Background() recorder := metric.Must(meter).NewInt64ValueRecorder( - "pipeline.valuerecorder", + "example.valuerecorder", metric.WithDescription("Records values"), ) counter := metric.Must(meter).NewInt64Counter( - "pipeline.counter", + "example.counter", metric.WithDescription("Counts things"), ) - fmt.Println("Success: Created Int64ValueRecorder and Int64Counter instruments") + fmt.Println("Success: Created Int64ValueRecorder and Int64Counter instruments!") // Record random values to the instruments in a loop - fmt.Println("Starting to write data to the instruments") + fmt.Println("Starting to write data to the instruments!") seed := rand.NewSource(time.Now().UnixNano()) random := rand.New(seed) for { - time.Sleep(5 * time.Second) + time.Sleep(1 * time.Second) randomValue := random.Intn(100) value := int64(randomValue * 10) recorder.Record(ctx, value, label.String("key", "value")) From 9a6a00f7e505335498ed59393df7334def397b43 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Thu, 3 Sep 2020 10:23:51 -0700 Subject: [PATCH 12/16] Fix docker-compose to pass CI test --- exporters/metric/cortex/example/docker-compose.yml | 3 --- exporters/metric/cortex/example/go.mod | 5 ----- exporters/metric/cortex/example/go.sum | 4 ++++ 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/exporters/metric/cortex/example/docker-compose.yml b/exporters/metric/cortex/example/docker-compose.yml index fdbc7890bf4..0f1c892dfce 100644 --- a/exporters/metric/cortex/example/docker-compose.yml +++ b/exporters/metric/cortex/example/docker-compose.yml @@ -19,11 +19,8 @@ services: image: golang:1.14-alpine volumes: - .:/app - - ../:/cortex/ working_dir: /app command: go run main.go - ports: - - 8888:8888 cortex: image: quay.io/cortexproject/cortex:v1.3.0-rc.1 command: diff --git a/exporters/metric/cortex/example/go.mod b/exporters/metric/cortex/example/go.mod index 476f1bd6633..1bfad619d67 100644 --- a/exporters/metric/cortex/example/go.mod +++ b/exporters/metric/cortex/example/go.mod @@ -2,11 +2,6 @@ module go.opentelemetry.io/contrib/exporters/metric/cortex/example go 1.14 -replace ( - go.opentelemetry.io/contrib/exporters/metric/cortex => ../cortex/ - go.opentelemetry.io/contrib/exporters/metric/cortex/utils => ../cortex/utils -) - require ( go.opentelemetry.io/contrib/exporters/metric/cortex v0.11.0 go.opentelemetry.io/contrib/exporters/metric/cortex/utils v0.11.0 diff --git a/exporters/metric/cortex/example/go.sum b/exporters/metric/cortex/example/go.sum index 6c1d9aa7ab2..4a529d14040 100644 --- a/exporters/metric/cortex/example/go.sum +++ b/exporters/metric/cortex/example/go.sum @@ -212,6 +212,10 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opentelemetry.io/contrib/exporters/metric/cortex v0.11.0 h1:JXXC9Q6ykERkE36BoXcpEVPxU145dj+RbZASMltM124= +go.opentelemetry.io/contrib/exporters/metric/cortex v0.11.0/go.mod h1:pgvKTEzZZYj7KDPJos0dGHJDg+VHnXij66qUBNFaWVI= +go.opentelemetry.io/contrib/exporters/metric/cortex/utils v0.11.0 h1:3Ih/kE7c35n41cyfJ7sUbCmxzgd+bHjYiYEJURJxXhs= +go.opentelemetry.io/contrib/exporters/metric/cortex/utils v0.11.0/go.mod h1:M/DWtSlAAWkh8gir+ZvY0Sz+L/V8ekVxRsxfOB68WZc= go.opentelemetry.io/otel v0.11.0 h1:IN2tzQa9Gc4ZVKnTaMbPVcHjvzOdg5n9QfnmlqiET7E= go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= go.opentelemetry.io/otel/sdk v0.11.0 h1:bkDMymVj6gIkPfgC5ci5atq0OYbfUHSn8NvsmyfyMq4= From c974102f68e4f5e243569fb5528d6f86463d0f22 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Thu, 3 Sep 2020 10:24:52 -0700 Subject: [PATCH 13/16] Run make precommit --- exporters/metric/cortex/cortex_test.go | 8 ++++---- exporters/metric/cortex/example/go.sum | 2 -- exporters/metric/cortex/testutil_test.go | 5 ----- exporters/metric/cortex/utils/go.sum | 2 -- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/exporters/metric/cortex/cortex_test.go b/exporters/metric/cortex/cortex_test.go index c871800848f..151228e40c9 100644 --- a/exporters/metric/cortex/cortex_test.go +++ b/exporters/metric/cortex/cortex_test.go @@ -296,13 +296,13 @@ func verifyExporterRequest(req *http.Request) error { Timeseries: []*prompb.TimeSeries{ { Samples: []prompb.Sample{ - prompb.Sample{ + { Value: float64(123), Timestamp: int64(time.Nanosecond) * time.Time{}.UnixNano() / int64(time.Millisecond), }, }, Labels: []*prompb.Label{ - &prompb.Label{ + { Name: "__name__", Value: "test_name", }, @@ -377,13 +377,13 @@ func TestSendRequest(t *testing.T) { timeSeries := []*prompb.TimeSeries{ { Samples: []prompb.Sample{ - prompb.Sample{ + { Value: float64(123), Timestamp: int64(time.Nanosecond) * time.Time{}.UnixNano() / int64(time.Millisecond), }, }, Labels: []*prompb.Label{ - &prompb.Label{ + { Name: "__name__", Value: "test_name", }, diff --git a/exporters/metric/cortex/example/go.sum b/exporters/metric/cortex/example/go.sum index 4a529d14040..f377a059a89 100644 --- a/exporters/metric/cortex/example/go.sum +++ b/exporters/metric/cortex/example/go.sum @@ -79,8 +79,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= diff --git a/exporters/metric/cortex/testutil_test.go b/exporters/metric/cortex/testutil_test.go index 81c720a35e3..93bb1207bc6 100644 --- a/exporters/metric/cortex/testutil_test.go +++ b/exporters/metric/cortex/testutil_test.go @@ -31,11 +31,6 @@ import ( "go.opentelemetry.io/otel/sdk/metric/aggregator/sum" ) -// getValidCheckpointSet returns a valid checkpointset with several records -func getValidCheckpointSet(t *testing.T) export.CheckpointSet { - return getSumCheckpoint(t, 321) -} - // getSumCheckpoint returns a checkpoint set with a sum aggregation record func getSumCheckpoint(t *testing.T, values ...int64) export.CheckpointSet { // Create checkpoint set with resource and descriptor diff --git a/exporters/metric/cortex/utils/go.sum b/exporters/metric/cortex/utils/go.sum index 97a662f96f4..207e6be06d6 100644 --- a/exporters/metric/cortex/utils/go.sum +++ b/exporters/metric/cortex/utils/go.sum @@ -212,8 +212,6 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opentelemetry.io v0.1.0 h1:EANZoRCOP+A3faIlw/iN6YEWoYb1vleZRKm1EvH8T48= -go.opentelemetry.io/contrib v0.11.0 h1:EQOdk+fxs7qp3wVIS5wCinwqNHfhD/DreQRY/VADO8s= go.opentelemetry.io/otel v0.11.0 h1:IN2tzQa9Gc4ZVKnTaMbPVcHjvzOdg5n9QfnmlqiET7E= go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= go.opentelemetry.io/otel/sdk v0.11.0 h1:bkDMymVj6gIkPfgC5ci5atq0OYbfUHSn8NvsmyfyMq4= From 2a31989e445d5475f1591d7b30a5318a12922a25 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Tue, 8 Sep 2020 10:37:37 -0700 Subject: [PATCH 14/16] Update default Config quantiles --- exporters/metric/cortex/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/metric/cortex/config.go b/exporters/metric/cortex/config.go index d03e97fd36c..b49984c67ba 100644 --- a/exporters/metric/cortex/config.go +++ b/exporters/metric/cortex/config.go @@ -110,7 +110,7 @@ func (c *Config) Validate() error { c.PushInterval = 10 * time.Second } if c.Quantiles == nil { - c.Quantiles = []float64{0, 0.25, 0.5, 0.75, 1} + c.Quantiles = []float64{0.5, 0.9, 0.95, 0.99} } return nil From d3f5a986ea67f183ae2f31cdb599efeceafdb1d4 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Tue, 8 Sep 2020 10:43:46 -0700 Subject: [PATCH 15/16] Update tests to match new quantile defaults --- exporters/metric/cortex/config_data_test.go | 4 ++-- exporters/metric/cortex/utils/config_utils_data_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/exporters/metric/cortex/config_data_test.go b/exporters/metric/cortex/config_data_test.go index 6a04eae36d2..c479ad14a63 100644 --- a/exporters/metric/cortex/config_data_test.go +++ b/exporters/metric/cortex/config_data_test.go @@ -26,7 +26,7 @@ var validatedStandardConfig = cortex.Config{ Name: "Config", RemoteTimeout: 30 * time.Second, PushInterval: 10 * time.Second, - Quantiles: []float64{0, 0.25, 0.5, 0.75, 1}, + Quantiles: []float64{0.5, 0.9, 0.95, 0.99}, } // Config struct with default values other than the remote timeout. This is used to verify @@ -36,7 +36,7 @@ var validatedCustomTimeoutConfig = cortex.Config{ Name: "Config", RemoteTimeout: 10 * time.Second, PushInterval: 10 * time.Second, - Quantiles: []float64{0, 0.25, 0.5, 0.75, 1}, + Quantiles: []float64{0.5, 0.9, 0.95, 0.99}, } // Config struct with default values other than the quantiles. This is used to verify diff --git a/exporters/metric/cortex/utils/config_utils_data_test.go b/exporters/metric/cortex/utils/config_utils_data_test.go index a7977e6339e..7accb65f99f 100644 --- a/exporters/metric/cortex/utils/config_utils_data_test.go +++ b/exporters/metric/cortex/utils/config_utils_data_test.go @@ -177,7 +177,7 @@ var validConfig = cortex.Config{ Headers: map[string]string{ "test": "header", }, - Quantiles: []float64{0, 0.25, 0.5, 0.75, 1}, + Quantiles: []float64{0.5, 0.9, 0.95, 0.99}, } // customQuantilesConfig is the resulting Config struct from reading quantilesYAML. @@ -230,6 +230,6 @@ var customBucketBoundariesConfig = cortex.Config{ Headers: map[string]string{ "test": "header", }, - Quantiles: []float64{0, 0.25, 0.5, 0.75, 1}, + Quantiles: []float64{0.5, 0.9, 0.95, 0.99}, HistogramBoundaries: []float64{100, 300, 500}, } From 732a4570720486097164b705dc0ecc9f1d34736f Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Tue, 8 Sep 2020 11:01:36 -0700 Subject: [PATCH 16/16] Run make precommit --- exporters/metric/cortex/example/go.sum | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exporters/metric/cortex/example/go.sum b/exporters/metric/cortex/example/go.sum index 179d8d1f9e3..f377a059a89 100644 --- a/exporters/metric/cortex/example/go.sum +++ b/exporters/metric/cortex/example/go.sum @@ -186,8 +186,8 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.3.5 h1:AWZ/w4lcfxuh52NVL78p9Eh8j6r1mCTEGSRFBJyIHAE= -github.com/spf13/afero v1.3.5/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.3.4 h1:8q6vk3hthlpb2SouZcnBVKboxWQWMDNF38bwholZrJc= +github.com/spf13/afero v1.3.4/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=