Skip to content

Commit 098cde0

Browse files
Sanitize metric type prefixes (#319)
* 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("<prefix>") 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 <[email protected]> * Remove condition that will never be true In alphanumerically sorted list of strings, abcdef will never come before abc. Signed-off-by: Edwin Mackenzie-Owen <[email protected]> * Add test for metrics prefix sanitization Signed-off-by: Edwin Mackenzie-Owen <[email protected]> * Use slices.Compact() for initial deduplication Signed-off-by: Edwin Mackenzie-Owen <[email protected]> * Improve comments Signed-off-by: Edwin Mackenzie-Owen <[email protected]> * Always initialize return slice Signed-off-by: Edwin Mackenzie-Owen <[email protected]> * Improve if condition Signed-off-by: Edwin Mackenzie-Owen <[email protected]> --------- Signed-off-by: Edwin Mackenzie-Owen <[email protected]>
1 parent 8ae3d33 commit 098cde0

File tree

2 files changed

+64
-1
lines changed

2 files changed

+64
-1
lines changed

stackdriver_exporter.go

+27-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
stdlog "log"
1919
"net/http"
2020
"os"
21+
"slices"
2122
"strings"
2223

2324
"github.com/PuerkitoBio/rehttp"
@@ -311,7 +312,8 @@ func main() {
311312
"projectsFilter", *projectsFilter,
312313
)
313314

314-
metricsTypePrefixes := strings.Split(*monitoringMetricsTypePrefixes, ",")
315+
inputPrefixes := strings.Split(*monitoringMetricsTypePrefixes, ",")
316+
metricsTypePrefixes := parseMetricTypePrefixes(inputPrefixes)
315317
metricExtraFilters := parseMetricExtraFilters()
316318

317319
if *metricsPath == *stackdriverMetricsPath {
@@ -361,6 +363,30 @@ func main() {
361363
}
362364
}
363365

366+
func parseMetricTypePrefixes(inputPrefixes []string) []string {
367+
metricTypePrefixes := []string{}
368+
369+
// Drop duplicate prefixes.
370+
slices.Sort(inputPrefixes)
371+
uniquePrefixes := slices.Compact(inputPrefixes)
372+
373+
// Drop prefixes that start with another existing prefix to avoid error:
374+
// "collected metric xxx was collected before with the same name and label values".
375+
for i, prefix := range uniquePrefixes {
376+
if i != 0 {
377+
previousIndex := len(metricTypePrefixes) - 1
378+
379+
// Drop current prefix if it starts with the previous one.
380+
if strings.HasPrefix(prefix, metricTypePrefixes[previousIndex]) {
381+
continue
382+
}
383+
}
384+
metricTypePrefixes = append(metricTypePrefixes, prefix)
385+
}
386+
387+
return metricTypePrefixes
388+
}
389+
364390
func parseMetricExtraFilters() []collectors.MetricFilter {
365391
var extraFilters []collectors.MetricFilter
366392
for _, ef := range *monitoringMetricsExtraFilter {

stackdriver_exporter_test.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2024 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package main
15+
16+
import "testing"
17+
import "reflect"
18+
19+
func TestParseMetricTypePrefixes(t *testing.T) {
20+
inputPrefixes := []string{
21+
"redis.googleapis.com/stats/memory/usage",
22+
"loadbalancing.googleapis.com/https/request_count",
23+
"loadbalancing.googleapis.com",
24+
"redis.googleapis.com/stats/memory/usage_ratio",
25+
"redis.googleapis.com/stats/memory/usage_ratio",
26+
}
27+
expectedOutputPrefixes := []string{
28+
"loadbalancing.googleapis.com",
29+
"redis.googleapis.com/stats/memory/usage",
30+
}
31+
32+
outputPrefixes := parseMetricTypePrefixes(inputPrefixes)
33+
34+
if !reflect.DeepEqual(outputPrefixes, expectedOutputPrefixes) {
35+
t.Errorf("Metric type prefix sanitization did not produce expected output. Expected:\n%s\nGot:\n%s", expectedOutputPrefixes, outputPrefixes)
36+
}
37+
}

0 commit comments

Comments
 (0)