mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
stackdriver: alias patterns WIP
This is using the {{}} syntax for alias patterns. Might switch to the syntax instead.
This commit is contained in:
parent
6db0880fd8
commit
681cd7496e
@ -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")
|
||||
|
@ -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)
|
||||
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)
|
||||
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
|
||||
|
||||
// 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("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")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user