فهرست منبع

Merge pull request #13496 from grafana/stackdriver-filter-wildcards

Stackdriver filter wildcards
Daniel Lee 7 سال پیش
والد
کامیت
98071ccd17
3فایلهای تغییر یافته به همراه153 افزوده شده و 11 حذف شده
  1. 20 10
      docs/sources/features/datasources/stackdriver.md
  2. 42 1
      pkg/tsdb/stackdriver/stackdriver.go
  3. 91 0
      pkg/tsdb/stackdriver/stackdriver_test.go

+ 20 - 10
docs/sources/features/datasources/stackdriver.md

@@ -74,7 +74,17 @@ Click on the links above and click the `Enable` button:
 
 Choose a metric from the `Metric` dropdown.
 
-To add a filter, click the plus icon and choose a field to filter by and enter a filter value e.g. `instance_name = grafana-1`
+### Filter
+
+To add a filter, click the plus icon and choose a field to filter by and enter a filter value e.g. `instance_name = grafana-1`. You can remove the filter by clicking on the filter name and select `--remove filter--`.
+
+#### Simple wildcards
+
+When the operator is set to `=` or `!=` it is possible to add wildcards to the filter value field. E.g `us-*` will capture all values that starts with "us-" and `*central-a` will capture all values that ends with "central-a". `*-central-*` captures all values that has the substring of -central-. Simple wildcards are less expensive than regular expressions. 
+
+#### Regular expressions
+
+When the operator is set to `=~` or `!=~` it is possible to add regular expressions to the filter value field. E.g `us-central[1-3]-[af]` would match all values that starts with "us-central", is followed by a number in the range of 1 to 3, a dash and then either an "a" or an "f". Leading and trailing slashes are not needed when creating regular expressions.
 
 ### Aggregation
 
@@ -105,20 +115,20 @@ The Alias By field allows you to control the format of the legend keys. The defa
 
 #### Metric Type Patterns
 
-Alias Pattern | Description | Example Result
------------------ | ---------------------------- | -------------
-`{{metric.type}}` | returns the full Metric Type | `compute.googleapis.com/instance/cpu/utilization`
-`{{metric.name}}` | returns the metric name part | `instance/cpu/utilization`
-`{{metric.service}}` | returns the service part | `compute`
+| Alias Pattern        | Description                  | Example Result                                    |
+| -------------------- | ---------------------------- | ------------------------------------------------- |
+| `{{metric.type}}`    | returns the full Metric Type | `compute.googleapis.com/instance/cpu/utilization` |
+| `{{metric.name}}`    | returns the metric name part | `instance/cpu/utilization`                        |
+| `{{metric.service}}` | returns the service part     | `compute`                                         |
 
 #### Label Patterns
 
 In the Group By dropdown, you can see a list of metric and resource labels for a metric. These can be included in the legend key using alias patterns.
 
-Alias Pattern Format | Description | Alias Pattern Example | Example Result
----------------------- | ---------------------------------- | ---------------------------- | -------------
-`{{metric.label.xxx}}` | returns the metric label value | `{{metric.label.instance_name}}` | `grafana-1-prod`
-`{{resource.label.xxx}}` | returns the resource label value | `{{resource.label.zone}}` | `us-east1-b`
+| Alias Pattern Format     | Description                      | Alias Pattern Example            | Example Result   |
+| ------------------------ | -------------------------------- | -------------------------------- | ---------------- |
+| `{{metric.label.xxx}}`   | returns the metric label value   | `{{metric.label.instance_name}}` | `grafana-1-prod` |
+| `{{resource.label.xxx}}` | returns the resource label value | `{{resource.label.zone}}`        | `us-east1-b`     |
 
 Example Alias By: `{{metric.type}} - {{metric.labels.instance_name}}`
 

+ 42 - 1
pkg/tsdb/stackdriver/stackdriver.go

@@ -159,6 +159,39 @@ func (e *StackdriverExecutor) buildQueries(tsdbQuery *tsdb.TsdbQuery) ([]*Stackd
 	return stackdriverQueries, nil
 }
 
+func reverse(s string) string {
+	chars := []rune(s)
+	for i, j := 0, len(chars)-1; i < j; i, j = i+1, j-1 {
+		chars[i], chars[j] = chars[j], chars[i]
+	}
+	return string(chars)
+}
+
+func interpolateFilterWildcards(value string) string {
+	re := regexp.MustCompile("[*]")
+	matches := len(re.FindAllStringIndex(value, -1))
+	if matches == 2 && strings.HasSuffix(value, "*") && strings.HasPrefix(value, "*") {
+		value = strings.Replace(value, "*", "", -1)
+		value = fmt.Sprintf(`has_substring("%s")`, value)
+	} else if matches == 1 && strings.HasPrefix(value, "*") {
+		value = strings.Replace(value, "*", "", 1)
+		value = fmt.Sprintf(`ends_with("%s")`, value)
+	} else if matches == 1 && strings.HasSuffix(value, "*") {
+		value = reverse(strings.Replace(reverse(value), "*", "", 1))
+		value = fmt.Sprintf(`starts_with("%s")`, value)
+	} else if matches != 0 {
+		re := regexp.MustCompile(`[-\/^$+?.()|[\]{}]`)
+		value = string(re.ReplaceAllFunc([]byte(value), func(in []byte) []byte {
+			return []byte(strings.Replace(string(in), string(in), `\\`+string(in), 1))
+		}))
+		value = strings.Replace(value, "*", ".*", -1)
+		value = strings.Replace(value, `"`, `\\"`, -1)
+		value = fmt.Sprintf(`monitoring.regex.full_match("^%s$")`, value)
+	}
+
+	return value
+}
+
 func buildFilterString(metricType string, filterParts []interface{}) string {
 	filterString := ""
 	for i, part := range filterParts {
@@ -166,7 +199,15 @@ func buildFilterString(metricType string, filterParts []interface{}) string {
 		if part == "AND" {
 			filterString += " "
 		} else if mod == 2 {
-			filterString += fmt.Sprintf(`"%s"`, part)
+			operator := filterParts[i-1]
+			if operator == "=~" || operator == "!=~" {
+				filterString = reverse(strings.Replace(reverse(filterString), "~", "", 1))
+				filterString += fmt.Sprintf(`monitoring.regex.full_match("%s")`, part)
+			} else if strings.Contains(part.(string), "*") {
+				filterString += interpolateFilterWildcards(part.(string))
+			} else {
+				filterString += fmt.Sprintf(`"%s"`, part)
+			}
 		} else {
 			filterString += part.(string)
 		}

+ 91 - 0
pkg/tsdb/stackdriver/stackdriver_test.go

@@ -342,6 +342,97 @@ func TestStackdriver(t *testing.T) {
 				})
 			})
 		})
+
+		Convey("when interpolating filter wildcards", func() {
+			Convey("and wildcard is used in the beginning and the end of the word", func() {
+				Convey("and theres no wildcard in the middle of the word", func() {
+					value := interpolateFilterWildcards("*-central1*")
+					So(value, ShouldEqual, `has_substring("-central1")`)
+				})
+				Convey("and there is a wildcard in the middle of the word", func() {
+					value := interpolateFilterWildcards("*-cent*ral1*")
+					So(value, ShouldNotStartWith, `has_substring`)
+				})
+			})
+
+			Convey("and wildcard is used in the beginning of the word", func() {
+				Convey("and there is not a wildcard elsewhere in the word", func() {
+					value := interpolateFilterWildcards("*-central1")
+					So(value, ShouldEqual, `ends_with("-central1")`)
+				})
+				Convey("and there is a wildcard elsewhere in the word", func() {
+					value := interpolateFilterWildcards("*-cent*al1")
+					So(value, ShouldNotStartWith, `ends_with`)
+				})
+			})
+
+			Convey("and wildcard is used at the end of the word", func() {
+				Convey("and there is not a wildcard elsewhere in the word", func() {
+					value := interpolateFilterWildcards("us-central*")
+					So(value, ShouldEqual, `starts_with("us-central")`)
+				})
+				Convey("and there is a wildcard elsewhere in the word", func() {
+					value := interpolateFilterWildcards("*us-central*")
+					So(value, ShouldNotStartWith, `starts_with`)
+				})
+			})
+
+			Convey("and wildcard is used in the middle of the word", func() {
+				Convey("and there is only one wildcard", func() {
+					value := interpolateFilterWildcards("us-ce*tral1-b")
+					So(value, ShouldEqual, `monitoring.regex.full_match("^us\\-ce.*tral1\\-b$")`)
+				})
+
+				Convey("and there is more than one wildcard", func() {
+					value := interpolateFilterWildcards("us-ce*tra*1-b")
+					So(value, ShouldEqual, `monitoring.regex.full_match("^us\\-ce.*tra.*1\\-b$")`)
+				})
+			})
+
+			Convey("and wildcard is used in the middle of the word and in the beginning of the word", func() {
+				value := interpolateFilterWildcards("*s-ce*tral1-b")
+				So(value, ShouldEqual, `monitoring.regex.full_match("^.*s\\-ce.*tral1\\-b$")`)
+			})
+
+			Convey("and wildcard is used in the middle of the word and in the ending of the word", func() {
+				value := interpolateFilterWildcards("us-ce*tral1-*")
+				So(value, ShouldEqual, `monitoring.regex.full_match("^us\\-ce.*tral1\\-.*$")`)
+			})
+
+			Convey("and no wildcard is used", func() {
+				value := interpolateFilterWildcards("us-central1-a}")
+				So(value, ShouldEqual, `us-central1-a}`)
+			})
+		})
+
+		Convey("when building filter string", func() {
+			Convey("and theres no regex operator", func() {
+				Convey("and there are wildcards in a filter value", func() {
+					filterParts := []interface{}{"zone", "=", "*-central1*"}
+					value := buildFilterString("somemetrictype", filterParts)
+					So(value, ShouldEqual, `metric.type="somemetrictype" zone=has_substring("-central1")`)
+				})
+
+				Convey("and there are no wildcards in any filter value", func() {
+					filterParts := []interface{}{"zone", "!=", "us-central1-a"}
+					value := buildFilterString("somemetrictype", filterParts)
+					So(value, ShouldEqual, `metric.type="somemetrictype" zone!="us-central1-a"`)
+				})
+			})
+
+			Convey("and there is a regex operator", func() {
+				filterParts := []interface{}{"zone", "=~", "us-central1-a~"}
+				value := buildFilterString("somemetrictype", filterParts)
+				Convey("it should remove the ~ character from the operator that belongs to the value", func() {
+					So(value, ShouldNotContainSubstring, `=~`)
+					So(value, ShouldContainSubstring, `zone=`)
+				})
+
+				Convey("it should insert monitoring.regex.full_match before filter value", func() {
+					So(value, ShouldContainSubstring, `zone=monitoring.regex.full_match("us-central1-a~")`)
+				})
+			})
+		})
 	})
 }