From b3247d4d7fb06456cbcb7a3de6be5680dee557a5 Mon Sep 17 00:00:00 2001 From: Edwin Mackenzie-Owen Date: Tue, 19 Mar 2024 18:39:35 +0100 Subject: [PATCH] Sanitize metric type prefixes When more than one prefix matches the same metric descriptor, this will throw the error "collected metric xxx was collected before with the same name and label values". For example, using the metric type prefixes foo.googleapis.com/bar (a prefix) and foo.googleapis.com/bar/baz (a metric) will result in an error because both match the metric foo.googleapis.com/bar/baz. Further, using the metric type prefixes foo.googleapis.com/bar/baz (a metric) and foo.googleapis.com/bar/baz_count (a metric) will result in an error because both match the metric foo.googleapis.com/bar/baz_count. While the first pitfall could be expected by the user, the latter will come as a complete surprise to anyone who is not aware that stackdriver-exporter internally uses an MQL query in the form of metric.type = starts_with("") to filter the metrics. Avoid this by sanitizing the provided metric type prefixes in the following way: - Drop any duplicate prefixes - Sort the prefixes (required by the next step) - Drop any prefixes that start with another prefix present in the input Signed-off-by: Edwin Mackenzie-Owen --- stackdriver_exporter.go | 44 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/stackdriver_exporter.go b/stackdriver_exporter.go index f71c4f4..a699da7 100644 --- a/stackdriver_exporter.go +++ b/stackdriver_exporter.go @@ -17,6 +17,7 @@ import ( "fmt" "net/http" "os" + "slices" "strings" "github.com/PuerkitoBio/rehttp" @@ -303,7 +304,7 @@ func main() { level.Info(logger).Log("msg", "Using Google Cloud Project IDs", "projectIDs", fmt.Sprintf("%v", projectIDs)) - metricsTypePrefixes := strings.Split(*monitoringMetricsTypePrefixes, ",") + metricsTypePrefixes := parseMetricTypePrefixes() metricExtraFilters := parseMetricExtraFilters() if *metricsPath == *stackdriverMetricsPath { @@ -353,6 +354,47 @@ func main() { } } +func parseMetricTypePrefixes() (metricTypePrefixes []string) { + inputPrefixes := strings.Split(*monitoringMetricsTypePrefixes, ",") + + // only keep unique prefixes + uniqueKeys := make(map[string]bool) + uniquePrefixes := []string{} + for _, prefix := range inputPrefixes { + if _, ok := uniqueKeys[prefix]; !ok { + uniqueKeys[prefix] = true + uniquePrefixes = append(uniquePrefixes, prefix) + } + } + + // drop prefixes that start with another existing prefix to avoid error: + // "collected metric xxx was collected before with the same name and label values" + slices.Sort(uniquePrefixes) + for i, prefix := range uniquePrefixes { + if i == 0 { + metricTypePrefixes = []string{prefix} + } else { + previousIndex := len(metricTypePrefixes) - 1 + + // current prefix starts with previous one + if strings.HasPrefix(prefix, metricTypePrefixes[previousIndex]) { + // drop current prefix + continue + } + + // previous prefix starts with current prefix + if strings.HasPrefix(metricTypePrefixes[previousIndex], prefix) { + // drop previous prefix + metricTypePrefixes = metricTypePrefixes[:previousIndex] + } + + metricTypePrefixes = append(metricTypePrefixes, prefix) + } + } + + return metricTypePrefixes +} + func parseMetricExtraFilters() []collectors.MetricFilter { var extraFilters []collectors.MetricFilter for _, ef := range *monitoringMetricsExtraFilter {