Skip to content

Commit

Permalink
Sanitize metric type prefixes (#319)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
sysedwinistrator authored Aug 30, 2024
1 parent 8ae3d33 commit 098cde0
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 1 deletion.
28 changes: 27 additions & 1 deletion stackdriver_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
stdlog "log"
"net/http"
"os"
"slices"
"strings"

"github.com/PuerkitoBio/rehttp"
Expand Down Expand Up @@ -311,7 +312,8 @@ func main() {
"projectsFilter", *projectsFilter,
)

metricsTypePrefixes := strings.Split(*monitoringMetricsTypePrefixes, ",")
inputPrefixes := strings.Split(*monitoringMetricsTypePrefixes, ",")
metricsTypePrefixes := parseMetricTypePrefixes(inputPrefixes)
metricExtraFilters := parseMetricExtraFilters()

if *metricsPath == *stackdriverMetricsPath {
Expand Down Expand Up @@ -361,6 +363,30 @@ func main() {
}
}

func parseMetricTypePrefixes(inputPrefixes []string) []string {
metricTypePrefixes := []string{}

// Drop duplicate prefixes.
slices.Sort(inputPrefixes)
uniquePrefixes := slices.Compact(inputPrefixes)

// Drop prefixes that start with another existing prefix to avoid error:
// "collected metric xxx was collected before with the same name and label values".
for i, prefix := range uniquePrefixes {
if i != 0 {
previousIndex := len(metricTypePrefixes) - 1

// Drop current prefix if it starts with the previous one.
if strings.HasPrefix(prefix, metricTypePrefixes[previousIndex]) {
continue
}
}
metricTypePrefixes = append(metricTypePrefixes, prefix)
}

return metricTypePrefixes
}

func parseMetricExtraFilters() []collectors.MetricFilter {
var extraFilters []collectors.MetricFilter
for _, ef := range *monitoringMetricsExtraFilter {
Expand Down
37 changes: 37 additions & 0 deletions stackdriver_exporter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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 main

import "testing"
import "reflect"

func TestParseMetricTypePrefixes(t *testing.T) {
inputPrefixes := []string{
"redis.googleapis.com/stats/memory/usage",
"loadbalancing.googleapis.com/https/request_count",
"loadbalancing.googleapis.com",
"redis.googleapis.com/stats/memory/usage_ratio",
"redis.googleapis.com/stats/memory/usage_ratio",
}
expectedOutputPrefixes := []string{
"loadbalancing.googleapis.com",
"redis.googleapis.com/stats/memory/usage",
}

outputPrefixes := parseMetricTypePrefixes(inputPrefixes)

if !reflect.DeepEqual(outputPrefixes, expectedOutputPrefixes) {
t.Errorf("Metric type prefix sanitization did not produce expected output. Expected:\n%s\nGot:\n%s", expectedOutputPrefixes, outputPrefixes)
}
}

0 comments on commit 098cde0

Please sign in to comment.