瀏覽代碼

stackdriver: alias patterns WIP

This is using the {{}} syntax for alias patterns. Might
switch to the  syntax instead.
Daniel Lee 7 年之前
父節點
當前提交
681cd7496e

+ 86 - 4
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")

+ 37 - 17
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")
+					})
+				})
+			})
 		})
 	})
 }

+ 2 - 0
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 {

+ 15 - 3
public/app/plugins/datasource/stackdriver/partials/query.editor.html

@@ -76,7 +76,7 @@
   <div class="gf-form-inline">
     <div class="gf-form">
       <span class="gf-form-label query-keyword width-9">Alias By</span>
-      <input type="text" class="gf-form-input width-12" ng-model="ctrl.target.aliasBy" />
+      <input type="text" class="gf-form-input width-30" ng-model="ctrl.target.aliasBy" />
     </div>
     <div class="gf-form gf-form--grow">
       <div class="gf-form-label gf-form-label--grow"></div>
@@ -111,8 +111,20 @@
     <pre class="gf-form-pre">{{ctrl.lastQueryMeta.rawQueryString}}</pre>
   </div>
   <div class="gf-form" ng-show="ctrl.showHelp">
-    <pre class="gf-form-pre alert alert-info">
-Help text for aliasing
+    <pre class="gf-form-pre alert alert-info"><h6>Alias Patterns</h6>
+  Format the legend keys any way you want by using alias patterns.
+
+  Example: <code ng-non-bindable>{{metric.name}} - {{metric.label.instance_name}}</code>
+  Result: cpu/usage_time - server1-europe-west-1
+
+  Patterns:
+  <code ng-non-bindable>{{metric.type}}</code> = metric type e.g. compute.googleapis.com/instance/cpu/usage_time
+  <code ng-non-bindable>{{metric.name}}</code> = name part of metric e.g. cpu/usage_time
+  <code ng-non-bindable>{{metric.category}}</code> = category part of metric e.g. instance
+  <code ng-non-bindable>{{metric.service}}</code> = service part of metric e.g. compute
+
+  <code ng-non-bindable>{{metric.label.label_name}}</code> = Metric label metadata e.g. metric.label.instance_name
+  <code ng-non-bindable>{{resource.label.label_name}}</code> = Resource label metadata e.g. resource.label.zone
     </pre>
   </div>
   <div class="gf-form" ng-show="ctrl.lastQueryError">