Stackdriver: Support meta labels (#21373)

* Rewrite angular segments for filter and group by in react

* wip: refactoring

* Update metric find queries

* Remove old maps used to create labels - use one map for all types instead

* Use value as label (again) for filters ang groupby

* Remove old filter

* Remove not used code

* Fixes after pr feedback

* Fix broken tests and add new metadata tests

* Add index file to make imports cleaner

* Cleanup. Remove old angular filter code

* Fix broken tests

* Use type switching instead of if statements

* Use globals for regex

* Updates after pr feedback

* Make sure it's possible to filter using the same key multiple times

* Replace metric select with segment component

* Pass template vars as props

* Refactor meta labels code

* Reorder template variables

* Fix broken tests

* Reset metric value when changing service

* Fix lint issue.

* Make tests independant of element order

* Include kubernetes.io in regex

* Add instruction in help section
This commit is contained in:
Erik Sundell
2020-01-17 12:25:47 +01:00
committed by GitHub
parent 72023d90bd
commit 260239d98b
33 changed files with 792 additions and 1597 deletions

View File

@@ -30,9 +30,15 @@ import (
)
var (
slog log.Logger
legendKeyFormat *regexp.Regexp
metricNameFormat *regexp.Regexp
slog log.Logger
)
var (
matchAllCap = regexp.MustCompile("(.)([A-Z][a-z]*)")
legendKeyFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
metricNameFormat = regexp.MustCompile(`([\w\d_]+)\.(googleapis\.com|io)/(.+)`)
wildcardRegexRe = regexp.MustCompile(`[-\/^$+?.()|[\]{}]`)
alignmentPeriodRe = regexp.MustCompile("[0-9]+")
)
const (
@@ -62,8 +68,6 @@ func NewStackdriverExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint,
func init() {
slog = log.New("tsdb.stackdriver")
tsdb.RegisterTsdbQueryEndpoint("stackdriver", NewStackdriverExecutor)
legendKeyFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
metricNameFormat = regexp.MustCompile(`([\w\d_]+)\.googleapis\.com/(.+)`)
}
// Query takes in the frontend queries, parses them into the Stackdriver query format
@@ -197,8 +201,7 @@ func interpolateFilterWildcards(value string) string {
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 {
value = string(wildcardRegexRe.ReplaceAllFunc([]byte(value), func(in []byte) []byte {
return []byte(strings.Replace(string(in), string(in), `\\`+string(in), 1))
}))
value = strings.Replace(value, "*", ".*", -1)
@@ -287,8 +290,7 @@ func (e *StackdriverExecutor) executeQuery(ctx context.Context, query *Stackdriv
alignmentPeriod, ok := req.URL.Query()["aggregation.alignmentPeriod"]
if ok {
re := regexp.MustCompile("[0-9]+")
seconds, err := strconv.ParseInt(re.FindString(alignmentPeriod[0]), 10, 64)
seconds, err := strconv.ParseInt(alignmentPeriodRe.FindString(alignmentPeriod[0]), 10, 64)
if err == nil {
queryResult.Meta.Set("alignmentPeriod", seconds)
}
@@ -333,8 +335,6 @@ func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (Stackdriver
return StackdriverResponse{}, err
}
// slog.Info("stackdriver", "response", string(body))
if res.StatusCode/100 != 2 {
slog.Error("Request failed", "status", res.Status, "body", string(body))
return StackdriverResponse{}, fmt.Errorf(string(body))
@@ -351,42 +351,66 @@ func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (Stackdriver
}
func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data StackdriverResponse, query *StackdriverQuery) error {
metricLabels := make(map[string][]string)
resourceLabels := make(map[string][]string)
var resourceTypes []string
for _, series := range data.TimeSeries {
if !containsLabel(resourceTypes, series.Resource.Type) {
resourceTypes = append(resourceTypes, series.Resource.Type)
}
}
labels := make(map[string]map[string]bool)
for _, series := range data.TimeSeries {
points := make([]tsdb.TimePoint, 0)
seriesLabels := make(map[string]string)
defaultMetricName := series.Metric.Type
if len(resourceTypes) > 1 {
defaultMetricName += " " + series.Resource.Type
}
labels["resource.type"] = map[string]bool{series.Resource.Type: true}
for key, value := range series.Metric.Labels {
if !containsLabel(metricLabels[key], value) {
metricLabels[key] = append(metricLabels[key], value)
if _, ok := labels["metric.label."+key]; !ok {
labels["metric.label."+key] = map[string]bool{}
}
labels["metric.label."+key][value] = true
seriesLabels["metric.label."+key] = value
if len(query.GroupBys) == 0 || containsLabel(query.GroupBys, "metric.label."+key) {
defaultMetricName += " " + value
}
}
for key, value := range series.Resource.Labels {
if !containsLabel(resourceLabels[key], value) {
resourceLabels[key] = append(resourceLabels[key], value)
if _, ok := labels["resource.label."+key]; !ok {
labels["resource.label."+key] = map[string]bool{}
}
labels["resource.label."+key][value] = true
seriesLabels["resource.label."+key] = value
if containsLabel(query.GroupBys, "resource.label."+key) {
defaultMetricName += " " + value
}
}
for labelType, labelTypeValues := range series.MetaData {
for labelKey, labelValue := range labelTypeValues {
key := toSnakeCase(fmt.Sprintf("metadata.%s.%s", labelType, labelKey))
if _, ok := labels[key]; !ok {
labels[key] = map[string]bool{}
}
switch v := labelValue.(type) {
case string:
labels[key][v] = true
seriesLabels[key] = v
case bool:
strVal := strconv.FormatBool(v)
labels[key][strVal] = true
seriesLabels[key] = strVal
case []interface{}:
for _, v := range v {
strVal := v.(string)
labels[key][strVal] = true
if len(seriesLabels[key]) > 0 {
strVal = fmt.Sprintf("%s, %s", seriesLabels[key], strVal)
}
seriesLabels[key] = strVal
}
}
}
}
// reverse the order to be ascending
if series.ValueType != "DISTRIBUTION" {
for i := len(series.Points) - 1; i >= 0; i-- {
@@ -411,7 +435,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
points = append(points, tsdb.NewTimePoint(null.FloatFrom(value), float64((point.Interval.EndTime).Unix())*1000))
}
metricName := formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, make(map[string]string), query)
metricName := formatLegendKeys(series.Metric.Type, defaultMetricName, seriesLabels, nil, query)
queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
Name: metricName,
@@ -437,7 +461,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
additionalLabels := map[string]string{"bucket": bucketBound}
buckets[i] = &tsdb.TimeSeries{
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, nil, additionalLabels, query),
Points: make([]tsdb.TimePoint, 0),
}
if maxKey < i {
@@ -453,7 +477,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
additionalLabels := map[string]string{"bucket": bucketBound}
buckets[i] = &tsdb.TimeSeries{
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, seriesLabels, additionalLabels, query),
Points: make([]tsdb.TimePoint, 0),
}
}
@@ -465,14 +489,23 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
}
}
queryRes.Meta.Set("resourceLabels", resourceLabels)
queryRes.Meta.Set("metricLabels", metricLabels)
labelsByKey := make(map[string][]string)
for key, values := range labels {
for value := range values {
labelsByKey[key] = append(labelsByKey[key], value)
}
}
queryRes.Meta.Set("labels", labelsByKey)
queryRes.Meta.Set("groupBys", query.GroupBys)
queryRes.Meta.Set("resourceTypes", resourceTypes)
return nil
}
func toSnakeCase(str string) string {
return strings.ToLower(matchAllCap.ReplaceAllString(str, "${1}_${2}"))
}
func containsLabel(labels []string, newLabel string) bool {
for _, val := range labels {
if val == newLabel {
@@ -482,7 +515,7 @@ func containsLabel(labels []string, newLabel string) bool {
return false
}
func formatLegendKeys(metricType string, defaultMetricName string, resourceType string, metricLabels map[string]string, resourceLabels map[string]string, additionalLabels map[string]string, query *StackdriverQuery) string {
func formatLegendKeys(metricType string, defaultMetricName string, labels map[string]string, additionalLabels map[string]string, query *StackdriverQuery) string {
if query.AliasBy == "" {
return defaultMetricName
}
@@ -496,25 +529,13 @@ func formatLegendKeys(metricType string, defaultMetricName string, resourceType
return []byte(metricType)
}
if metaPartName == "resource.type" && resourceType != "" {
return []byte(resourceType)
}
metricPart := replaceWithMetricPart(metaPartName, metricType)
if metricPart != nil {
return metricPart
}
metaPartName = strings.Replace(metaPartName, "metric.label.", "", 1)
if val, exists := metricLabels[metaPartName]; exists {
return []byte(val)
}
metaPartName = strings.Replace(metaPartName, "resource.label.", "", 1)
if val, exists := resourceLabels[metaPartName]; exists {
if val, exists := labels[metaPartName]; exists {
return []byte(val)
}
@@ -533,8 +554,8 @@ func replaceWithMetricPart(metaPartName string, metricType string) []byte {
shortMatches := metricNameFormat.FindStringSubmatch(metricType)
if metaPartName == "metric.name" {
if len(shortMatches) > 0 {
return []byte(shortMatches[2])
if len(shortMatches) > 2 {
return []byte(shortMatches[3])
}
}

View File

@@ -260,22 +260,20 @@ func TestStackdriver(t *testing.T) {
})
Convey("Should add meta for labels to the response", func() {
metricLabels := res.Meta.Get("metricLabels").Interface().(map[string][]string)
So(metricLabels, ShouldNotBeNil)
So(len(metricLabels["instance_name"]), ShouldEqual, 3)
So(metricLabels["instance_name"][0], ShouldEqual, "collector-asia-east-1")
So(metricLabels["instance_name"][1], ShouldEqual, "collector-europe-west-1")
So(metricLabels["instance_name"][2], ShouldEqual, "collector-us-east-1")
labels := res.Meta.Get("labels").Interface().(map[string][]string)
So(labels, ShouldNotBeNil)
So(len(labels["metric.label.instance_name"]), ShouldEqual, 3)
So(labels["metric.label.instance_name"], ShouldContain, "collector-asia-east-1")
So(labels["metric.label.instance_name"], ShouldContain, "collector-europe-west-1")
So(labels["metric.label.instance_name"], ShouldContain, "collector-us-east-1")
resourceLabels := res.Meta.Get("resourceLabels").Interface().(map[string][]string)
So(resourceLabels, ShouldNotBeNil)
So(len(resourceLabels["zone"]), ShouldEqual, 3)
So(resourceLabels["zone"][0], ShouldEqual, "asia-east1-a")
So(resourceLabels["zone"][1], ShouldEqual, "europe-west1-b")
So(resourceLabels["zone"][2], ShouldEqual, "us-east1-b")
So(len(labels["resource.label.zone"]), ShouldEqual, 3)
So(labels["resource.label.zone"], ShouldContain, "asia-east1-a")
So(labels["resource.label.zone"], ShouldContain, "europe-west1-b")
So(labels["resource.label.zone"], ShouldContain, "us-east1-b")
So(len(resourceLabels["project_id"]), ShouldEqual, 1)
So(resourceLabels["project_id"][0], ShouldEqual, "grafana-prod")
So(len(labels["resource.label.project_id"]), ShouldEqual, 1)
So(labels["resource.label.project_id"][0], ShouldEqual, "grafana-prod")
})
})
@@ -419,6 +417,72 @@ func TestStackdriver(t *testing.T) {
So(res.Series[10].Points[1][0].Float64, ShouldEqual, 56)
})
})
Convey("when data from query returns metadata system labels", func() {
data, err := loadTestFile("./test-data/5-series-response-meta-data.json")
So(err, ShouldBeNil)
So(len(data.TimeSeries), ShouldEqual, 3)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{AliasBy: "{{bucket}}"}
err = executor.parseResponse(res, data, query)
labels := res.Meta.Get("labels").Interface().(map[string][]string)
So(err, ShouldBeNil)
So(len(res.Series), ShouldEqual, 3)
Convey("and systemlabel contains key with array of string", func() {
So(len(labels["metadata.system_labels.test"]), ShouldEqual, 5)
So(labels["metadata.system_labels.test"], ShouldContain, "value1")
So(labels["metadata.system_labels.test"], ShouldContain, "value2")
So(labels["metadata.system_labels.test"], ShouldContain, "value3")
So(labels["metadata.system_labels.test"], ShouldContain, "value4")
So(labels["metadata.system_labels.test"], ShouldContain, "value5")
})
Convey("and systemlabel contains key with primitive strings", func() {
So(len(labels["metadata.system_labels.region"]), ShouldEqual, 2)
So(labels["metadata.system_labels.region"], ShouldContain, "us-central1")
So(labels["metadata.system_labels.region"], ShouldContain, "us-west1")
})
Convey("and userLabel contains key with primitive strings", func() {
So(len(labels["metadata.user_labels.region"]), ShouldEqual, 2)
So(labels["metadata.user_labels.region"], ShouldContain, "region1")
So(labels["metadata.user_labels.region"], ShouldContain, "region3")
So(len(labels["metadata.user_labels.name"]), ShouldEqual, 2)
So(labels["metadata.user_labels.name"], ShouldContain, "name1")
So(labels["metadata.user_labels.name"], ShouldContain, "name3")
})
})
Convey("when data from query returns metadata system labels and alias by is defined", func() {
data, err := loadTestFile("./test-data/5-series-response-meta-data.json")
So(err, ShouldBeNil)
So(len(data.TimeSeries), ShouldEqual, 3)
Convey("and systemlabel contains key with array of string", func() {
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{AliasBy: "{{metadata.system_labels.test}}"}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
So(len(res.Series), ShouldEqual, 3)
fmt.Println(res.Series[0].Name)
So(res.Series[0].Name, ShouldEqual, "value1, value2")
So(res.Series[1].Name, ShouldEqual, "value1, value2, value3")
So(res.Series[2].Name, ShouldEqual, "value1, value2, value4, value5")
})
Convey("and systemlabel contains key with array of string2", func() {
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{AliasBy: "{{metadata.system_labels.test2}}"}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
So(len(res.Series), ShouldEqual, 3)
fmt.Println(res.Series[0].Name)
So(res.Series[2].Name, ShouldEqual, "testvalue")
})
})
})
Convey("when interpolating filter wildcards", func() {

View File

@@ -0,0 +1,78 @@
{
"timeSeries": [{
"metric": {
"type": "compute.googleapis.com/firewall/dropped_bytes_count"
},
"resource": {
"type": "gce_instance",
"labels": {
"instance_id": "114250375703598695",
"project_id": "raintank-dev"
}
},
"metricKind": "DELTA",
"valueType": "INT64",
"metadata": {
"systemLabels": {
"region": "us-west1",
"name": "diana-debian9",
"test": ["value1", "value2"],
"spot_instance": false
},
"userLabels": {
"name": "name1",
"region": "region1"
}
}
},
{
"metric": {
"type": "compute.googleapis.com/firewall/dropped_bytes_count"
},
"resource": {
"type": "gce_instance",
"labels": {
"instance_id": "2268399396335228490",
"project_id": "raintank-dev"
}
},
"metricKind": "DELTA",
"valueType": "INT64",
"metadata": {
"systemLabels": {
"region": "us-west1",
"name": "diana-ubuntu1910",
"test": ["value1", "value2","value3"],
"spot_instance": false
}
}
},
{
"metric": {
"type": "compute.googleapis.com/firewall/dropped_bytes_count"
},
"resource": {
"type": "gce_instance",
"labels": {
"instance_id": "2390486324245305300",
"project_id": "raintank-dev"
}
},
"metricKind": "DELTA",
"valueType": "INT64",
"metadata": {
"systemLabels": {
"name": "premium-plugin-staging",
"region": "us-central1",
"test": ["value1", "value2","value4", "value5"],
"test2": ["testvalue"],
"spot_instance": true
},
"userLabels": {
"name": "name3",
"region": "region3"
}
}
}
]
}

View File

@@ -41,8 +41,9 @@ type StackdriverResponse struct {
Type string `json:"type"`
Labels map[string]string `json:"labels"`
} `json:"resource"`
MetricKind string `json:"metricKind"`
ValueType string `json:"valueType"`
MetaData map[string]map[string]interface{} `json:"metadata"`
MetricKind string `json:"metricKind"`
ValueType string `json:"valueType"`
Points []struct {
Interval struct {
StartTime time.Time `json:"startTime"`