mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user