From 681cd7496e8cb303574f2370ec296e1476ccd7bd Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Sun, 23 Sep 2018 22:04:24 +0200 Subject: [PATCH] stackdriver: alias patterns WIP This is using the {{}} syntax for alias patterns. Might switch to the syntax instead. --- pkg/tsdb/stackdriver/stackdriver.go | 90 ++++++++++++++++++- pkg/tsdb/stackdriver/stackdriver_test.go | 54 +++++++---- pkg/tsdb/stackdriver/types.go | 2 + .../stackdriver/partials/query.editor.html | 18 +++- 4 files changed, 140 insertions(+), 24 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 47761a3c865..20df58f87d7 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -28,7 +28,12 @@ import ( "github.com/opentracing/opentracing-go" ) -var slog log.Logger +var ( + slog log.Logger + legendKeyFormat *regexp.Regexp + longMetricNameFormat *regexp.Regexp + shortMetricNameFormat *regexp.Regexp +) // StackdriverExecutor executes queries for the Stackdriver datasource type StackdriverExecutor struct { @@ -52,6 +57,9 @@ func NewStackdriverExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, func init() { slog = log.New("tsdb.stackdriver") tsdb.RegisterTsdbQueryEndpoint("stackdriver", NewStackdriverExecutor) + legendKeyFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`) + longMetricNameFormat = regexp.MustCompile(`([\w\d_]+)\.googleapis\.com/([\w\d_]+)/(.+)`) + shortMetricNameFormat = regexp.MustCompile(`([\w\d_]+)\.googleapis\.com/(.+)`) } // Query takes in the frontend queries, parses them into the Stackdriver query format @@ -132,11 +140,14 @@ func (e *StackdriverExecutor) buildQueries(tsdbQuery *tsdb.TsdbQuery) ([]*Stackd groupBysAsStrings = append(groupBysAsStrings, groupBy.(string)) } + aliasBy := query.Model.Get("aliasBy").MustString() + stackdriverQueries = append(stackdriverQueries, &StackdriverQuery{ Target: target, Params: params, RefID: query.RefId, GroupBys: groupBysAsStrings, + AliasBy: aliasBy, }) } @@ -260,14 +271,15 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta point := series.Points[i] points = append(points, tsdb.NewTimePoint(null.FloatFrom(point.Value.DoubleValue), float64((point.Interval.EndTime).Unix())*1000)) } - metricName := series.Metric.Type + + defaultMetricName := series.Metric.Type for key, value := range series.Metric.Labels { if !containsLabel(metricLabels[key], value) { metricLabels[key] = append(metricLabels[key], value) } if len(query.GroupBys) == 0 || containsLabel(query.GroupBys, "metric.label."+key) { - metricName += " " + value + defaultMetricName += " " + value } } @@ -277,10 +289,12 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta } if containsLabel(query.GroupBys, "resource.label."+key) { - metricName += " " + value + defaultMetricName += " " + value } } + metricName := formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, query) + queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{ Name: metricName, Points: points, @@ -303,6 +317,74 @@ func containsLabel(labels []string, newLabel string) bool { return false } +func formatLegendKeys(metricType string, defaultMetricName string, metricLabels map[string]string, resourceLabels map[string]string, query *StackdriverQuery) string { + if query.AliasBy == "" { + return defaultMetricName + } + + result := legendKeyFormat.ReplaceAllFunc([]byte(query.AliasBy), func(in []byte) []byte { + metaPartName := strings.Replace(string(in), "{{", "", 1) + metaPartName = strings.Replace(metaPartName, "}}", "", 1) + metaPartName = strings.TrimSpace(metaPartName) + + if metaPartName == "metric.type" { + return []byte(metricType) + } + + 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 { + return []byte(val) + } + + return in + }) + + return string(result) +} + +func replaceWithMetricPart(metaPartName string, metricType string) []byte { + // https://cloud.google.com/monitoring/api/v3/metrics-details#label_names + longMatches := longMetricNameFormat.FindStringSubmatch(metricType) + shortMatches := shortMetricNameFormat.FindStringSubmatch(metricType) + + if metaPartName == "metric.name" { + if len(longMatches) > 0 { + return []byte(longMatches[3]) + } else if len(shortMatches) > 0 { + return []byte(shortMatches[2]) + } + } + + if metaPartName == "metric.category" { + if len(longMatches) > 0 { + return []byte(longMatches[2]) + } + } + + if metaPartName == "metric.service" { + if len(longMatches) > 0 { + return []byte(longMatches[1]) + } else if len(shortMatches) > 0 { + return []byte(shortMatches[1]) + } + } + + return nil +} + func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.DataSource) (*http.Request, error) { u, _ := url.Parse(dsInfo.Url) u.Path = path.Join(u.Path, "render") diff --git a/pkg/tsdb/stackdriver/stackdriver_test.go b/pkg/tsdb/stackdriver/stackdriver_test.go index f30795bf84c..8d52cd25c91 100644 --- a/pkg/tsdb/stackdriver/stackdriver_test.go +++ b/pkg/tsdb/stackdriver/stackdriver_test.go @@ -30,6 +30,7 @@ func TestStackdriver(t *testing.T) { "target": "target", "metricType": "a/metric/type", "view": "FULL", + "aliasBy": "testalias", }), RefId: "A", }, @@ -49,6 +50,7 @@ func TestStackdriver(t *testing.T) { So(queries[0].Params["aggregation.perSeriesAligner"][0], ShouldEqual, "ALIGN_MEAN") So(queries[0].Params["filter"][0], ShouldEqual, "metric.type=\"a/metric/type\"") So(queries[0].Params["view"][0], ShouldEqual, "FULL") + So(queries[0].AliasBy, ShouldEqual, "testalias") }) Convey("and query has filters", func() { @@ -255,23 +257,41 @@ func TestStackdriver(t *testing.T) { }) }) - // Convey("when data from query with no aggregation and alias by", func() { - // data, err := loadTestFile("./test-data/2-series-response-no-agg.json") - // So(err, ShouldBeNil) - // So(len(data.TimeSeries), ShouldEqual, 3) - - // res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"} - // query := &StackdriverQuery{AliasBy: "{{metric.label.instance_name}}", GroupBys: []string{"metric.label.instance_name", "resource.label.zone"}} - // err = executor.parseResponse(res, data, query) - // So(err, ShouldBeNil) - - // Convey("Should use alias by formatting and only show instance name", func() { - // So(len(res.Series), ShouldEqual, 3) - // So(res.Series[0].Name, ShouldEqual, "collector-asia-east-1") - // So(res.Series[1].Name, ShouldEqual, "collector-europe-west-1") - // So(res.Series[2].Name, ShouldEqual, "collector-us-east-1") - // }) - // }) + Convey("when data from query with no aggregation and alias by", func() { + data, err := loadTestFile("./test-data/2-series-response-no-agg.json") + So(err, ShouldBeNil) + So(len(data.TimeSeries), ShouldEqual, 3) + + res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"} + + Convey("and the alias pattern is for metric type, a metric label and a resource label", func() { + + query := &StackdriverQuery{AliasBy: "{{metric.type}} - {{metric.label.instance_name}} - {{resource.label.zone}}", GroupBys: []string{"metric.label.instance_name", "resource.label.zone"}} + err = executor.parseResponse(res, data, query) + So(err, ShouldBeNil) + + Convey("Should use alias by formatting and only show instance name", func() { + So(len(res.Series), ShouldEqual, 3) + So(res.Series[0].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time - collector-asia-east-1 - asia-east1-a") + So(res.Series[1].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time - collector-europe-west-1 - europe-west1-b") + So(res.Series[2].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time - collector-us-east-1 - us-east1-b") + }) + }) + + Convey("and the alias pattern is for metric name", func() { + + query := &StackdriverQuery{AliasBy: "metric {{metric.name}} service {{metric.service}} category {{metric.category}}", GroupBys: []string{"metric.label.instance_name", "resource.label.zone"}} + err = executor.parseResponse(res, data, query) + So(err, ShouldBeNil) + + Convey("Should use alias by formatting and only show instance name", func() { + So(len(res.Series), ShouldEqual, 3) + So(res.Series[0].Name, ShouldEqual, "metric cpu/usage_time service compute category instance") + So(res.Series[1].Name, ShouldEqual, "metric cpu/usage_time service compute category instance") + So(res.Series[2].Name, ShouldEqual, "metric cpu/usage_time service compute category instance") + }) + }) + }) }) }) } diff --git a/pkg/tsdb/stackdriver/types.go b/pkg/tsdb/stackdriver/types.go index ba2ed29a39c..6e3a51f1429 100644 --- a/pkg/tsdb/stackdriver/types.go +++ b/pkg/tsdb/stackdriver/types.go @@ -5,6 +5,7 @@ import ( "time" ) +// StackdriverQuery is the query that Grafana sends from the frontend type StackdriverQuery struct { Target string Params url.Values @@ -13,6 +14,7 @@ type StackdriverQuery struct { AliasBy string } +// StackdriverResponse is the data returned from the external Google Stackdriver API type StackdriverResponse struct { TimeSeries []struct { Metric struct { diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index 1efe4388607..c6804f3f8e3 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -76,7 +76,7 @@
Alias By - +
@@ -111,8 +111,20 @@
{{ctrl.lastQueryMeta.rawQueryString}}
-
-Help text for aliasing
+    
Alias Patterns
+ Format the legend keys any way you want by using alias patterns. + + Example: {{metric.name}} - {{metric.label.instance_name}} + Result: cpu/usage_time - server1-europe-west-1 + + Patterns: + {{metric.type}} = metric type e.g. compute.googleapis.com/instance/cpu/usage_time + {{metric.name}} = name part of metric e.g. cpu/usage_time + {{metric.category}} = category part of metric e.g. instance + {{metric.service}} = service part of metric e.g. compute + + {{metric.label.label_name}} = Metric label metadata e.g. metric.label.instance_name + {{resource.label.label_name}} = Resource label metadata e.g. resource.label.zone