diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md
index 96f3ba3382e..c525130aebb 100644
--- a/docs/sources/features/datasources/stackdriver.md
+++ b/docs/sources/features/datasources/stackdriver.md
@@ -74,7 +74,17 @@ Click on the links above and click the `Enable` button:
Choose a metric from the `Metric` dropdown.
-To add a filter, click the plus icon and choose a field to filter by and enter a filter value e.g. `instance_name = grafana-1`
+### Filter
+
+To add a filter, click the plus icon and choose a field to filter by and enter a filter value e.g. `instance_name = grafana-1`. You can remove the filter by clicking on the filter name and select `--remove filter--`.
+
+#### Simple wildcards
+
+When the operator is set to `=` or `!=` it is possible to add wildcards to the filter value field. E.g `us-*` will capture all values that starts with "us-" and `*central-a` will capture all values that ends with "central-a". `*-central-*` captures all values that has the substring of -central-. Simple wildcards are less expensive than regular expressions.
+
+#### Regular expressions
+
+When the operator is set to `=~` or `!=~` it is possible to add regular expressions to the filter value field. E.g `us-central[1-3]-[af]` would match all values that starts with "us-central", is followed by a number in the range of 1 to 3, a dash and then either an "a" or an "f". Leading and trailing slashes are not needed when creating regular expressions.
### Aggregation
@@ -105,20 +115,20 @@ The Alias By field allows you to control the format of the legend keys. The defa
#### Metric Type Patterns
-Alias Pattern | Description | Example Result
------------------ | ---------------------------- | -------------
-`{{metric.type}}` | returns the full Metric Type | `compute.googleapis.com/instance/cpu/utilization`
-`{{metric.name}}` | returns the metric name part | `instance/cpu/utilization`
-`{{metric.service}}` | returns the service part | `compute`
+| Alias Pattern | Description | Example Result |
+| -------------------- | ---------------------------- | ------------------------------------------------- |
+| `{{metric.type}}` | returns the full Metric Type | `compute.googleapis.com/instance/cpu/utilization` |
+| `{{metric.name}}` | returns the metric name part | `instance/cpu/utilization` |
+| `{{metric.service}}` | returns the service part | `compute` |
#### Label Patterns
In the Group By dropdown, you can see a list of metric and resource labels for a metric. These can be included in the legend key using alias patterns.
-Alias Pattern Format | Description | Alias Pattern Example | Example Result
----------------------- | ---------------------------------- | ---------------------------- | -------------
-`{{metric.label.xxx}}` | returns the metric label value | `{{metric.label.instance_name}}` | `grafana-1-prod`
-`{{resource.label.xxx}}` | returns the resource label value | `{{resource.label.zone}}` | `us-east1-b`
+| Alias Pattern Format | Description | Alias Pattern Example | Example Result |
+| ------------------------ | -------------------------------- | -------------------------------- | ---------------- |
+| `{{metric.label.xxx}}` | returns the metric label value | `{{metric.label.instance_name}}` | `grafana-1-prod` |
+| `{{resource.label.xxx}}` | returns the resource label value | `{{resource.label.zone}}` | `us-east1-b` |
Example Alias By: `{{metric.type}} - {{metric.labels.instance_name}}`
diff --git a/package.json b/package.json
index a6db5bdc1e5..333cd361ac4 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
- "version": "5.3.0-beta3",
+ "version": "5.3.0",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"
diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go
index be14c6f96ec..61bbc04394a 100644
--- a/pkg/tsdb/cloudwatch/cloudwatch.go
+++ b/pkg/tsdb/cloudwatch/cloudwatch.go
@@ -129,10 +129,13 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" {
return err
}
- result.Results[queryRes.RefId] = queryRes
if err != nil {
- result.Results[queryRes.RefId].Error = err
+ result.Results[query.RefId] = &tsdb.QueryResult{
+ Error: err,
+ }
+ return nil
}
+ result.Results[queryRes.RefId] = queryRes
return nil
})
}
@@ -269,7 +272,7 @@ func (e *CloudWatchExecutor) executeGetMetricDataQuery(ctx context.Context, regi
for _, query := range queries {
// 1 minutes resolution metrics is stored for 15 days, 15 * 24 * 60 = 21600
if query.HighResolution && (((endTime.Unix() - startTime.Unix()) / int64(query.Period)) > 21600) {
- return nil, errors.New("too long query period")
+ return queryResponses, errors.New("too long query period")
}
mdq := &cloudwatch.MetricDataQuery{
@@ -362,6 +365,7 @@ func (e *CloudWatchExecutor) executeGetMetricDataQuery(ctx context.Context, regi
}
queryRes.Series = append(queryRes.Series, &series)
+ queryRes.Meta = simplejson.New()
queryResponses = append(queryResponses, queryRes)
}
@@ -565,6 +569,12 @@ func parseResponse(resp *cloudwatch.GetMetricStatisticsOutput, query *CloudWatch
}
queryRes.Series = append(queryRes.Series, &series)
+ queryRes.Meta = simplejson.New()
+ if len(resp.Datapoints) > 0 && resp.Datapoints[0].Unit != nil {
+ if unit, ok := cloudwatchUnitMappings[*resp.Datapoints[0].Unit]; ok {
+ queryRes.Meta.Set("unit", unit)
+ }
+ }
}
return queryRes, nil
diff --git a/pkg/tsdb/cloudwatch/cloudwatch_test.go b/pkg/tsdb/cloudwatch/cloudwatch_test.go
index 719edba08ba..32b8c910f2b 100644
--- a/pkg/tsdb/cloudwatch/cloudwatch_test.go
+++ b/pkg/tsdb/cloudwatch/cloudwatch_test.go
@@ -71,6 +71,7 @@ func TestCloudWatch(t *testing.T) {
"p50.00": aws.Float64(30.0),
"p90.00": aws.Float64(40.0),
},
+ Unit: aws.String("Seconds"),
},
},
}
@@ -103,6 +104,7 @@ func TestCloudWatch(t *testing.T) {
So(queryRes.Series[1].Points[0][0].String(), ShouldEqual, null.FloatFrom(20.0).String())
So(queryRes.Series[2].Points[0][0].String(), ShouldEqual, null.FloatFrom(30.0).String())
So(queryRes.Series[3].Points[0][0].String(), ShouldEqual, null.FloatFrom(40.0).String())
+ So(queryRes.Meta.Get("unit").MustString(), ShouldEqual, "s")
})
Convey("terminate gap of data points", func() {
@@ -118,6 +120,7 @@ func TestCloudWatch(t *testing.T) {
"p50.00": aws.Float64(30.0),
"p90.00": aws.Float64(40.0),
},
+ Unit: aws.String("Seconds"),
},
{
Timestamp: aws.Time(timestamp.Add(60 * time.Second)),
@@ -127,6 +130,7 @@ func TestCloudWatch(t *testing.T) {
"p50.00": aws.Float64(40.0),
"p90.00": aws.Float64(50.0),
},
+ Unit: aws.String("Seconds"),
},
{
Timestamp: aws.Time(timestamp.Add(180 * time.Second)),
@@ -136,6 +140,7 @@ func TestCloudWatch(t *testing.T) {
"p50.00": aws.Float64(50.0),
"p90.00": aws.Float64(60.0),
},
+ Unit: aws.String("Seconds"),
},
},
}
diff --git a/pkg/tsdb/cloudwatch/constants.go b/pkg/tsdb/cloudwatch/constants.go
new file mode 100644
index 00000000000..23817b1d133
--- /dev/null
+++ b/pkg/tsdb/cloudwatch/constants.go
@@ -0,0 +1,30 @@
+package cloudwatch
+
+var cloudwatchUnitMappings = map[string]string{
+ "Seconds": "s",
+ "Microseconds": "µs",
+ "Milliseconds": "ms",
+ "Bytes": "bytes",
+ "Kilobytes": "kbytes",
+ "Megabytes": "mbytes",
+ "Gigabytes": "gbytes",
+ //"Terabytes": "",
+ "Bits": "bits",
+ //"Kilobits": "",
+ //"Megabits": "",
+ //"Gigabits": "",
+ //"Terabits": "",
+ "Percent": "percent",
+ //"Count": "",
+ "Bytes/Second": "Bps",
+ "Kilobytes/Second": "KBs",
+ "Megabytes/Second": "MBs",
+ "Gigabytes/Second": "GBs",
+ //"Terabytes/Second": "",
+ "Bits/Second": "bps",
+ "Kilobits/Second": "Kbits",
+ "Megabits/Second": "Mbits",
+ "Gigabits/Second": "Gbits",
+ //"Terabits/Second": "",
+ //"Count/Second": "",
+}
diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go
index 586e154cd5d..96242dfdec4 100644
--- a/pkg/tsdb/stackdriver/stackdriver.go
+++ b/pkg/tsdb/stackdriver/stackdriver.go
@@ -159,6 +159,39 @@ func (e *StackdriverExecutor) buildQueries(tsdbQuery *tsdb.TsdbQuery) ([]*Stackd
return stackdriverQueries, nil
}
+func reverse(s string) string {
+ chars := []rune(s)
+ for i, j := 0, len(chars)-1; i < j; i, j = i+1, j-1 {
+ chars[i], chars[j] = chars[j], chars[i]
+ }
+ return string(chars)
+}
+
+func interpolateFilterWildcards(value string) string {
+ re := regexp.MustCompile("[*]")
+ matches := len(re.FindAllStringIndex(value, -1))
+ if matches == 2 && strings.HasSuffix(value, "*") && strings.HasPrefix(value, "*") {
+ value = strings.Replace(value, "*", "", -1)
+ value = fmt.Sprintf(`has_substring("%s")`, value)
+ } else if matches == 1 && strings.HasPrefix(value, "*") {
+ value = strings.Replace(value, "*", "", 1)
+ value = fmt.Sprintf(`ends_with("%s")`, value)
+ } else if matches == 1 && strings.HasSuffix(value, "*") {
+ 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 {
+ return []byte(strings.Replace(string(in), string(in), `\\`+string(in), 1))
+ }))
+ value = strings.Replace(value, "*", ".*", -1)
+ value = strings.Replace(value, `"`, `\\"`, -1)
+ value = fmt.Sprintf(`monitoring.regex.full_match("^%s$")`, value)
+ }
+
+ return value
+}
+
func buildFilterString(metricType string, filterParts []interface{}) string {
filterString := ""
for i, part := range filterParts {
@@ -166,7 +199,15 @@ func buildFilterString(metricType string, filterParts []interface{}) string {
if part == "AND" {
filterString += " "
} else if mod == 2 {
- filterString += fmt.Sprintf(`"%s"`, part)
+ operator := filterParts[i-1]
+ if operator == "=~" || operator == "!=~" {
+ filterString = reverse(strings.Replace(reverse(filterString), "~", "", 1))
+ filterString += fmt.Sprintf(`monitoring.regex.full_match("%s")`, part)
+ } else if strings.Contains(part.(string), "*") {
+ filterString += interpolateFilterWildcards(part.(string))
+ } else {
+ filterString += fmt.Sprintf(`"%s"`, part)
+ }
} else {
filterString += part.(string)
}
@@ -300,29 +341,6 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
for _, series := range data.TimeSeries {
points := make([]tsdb.TimePoint, 0)
- // reverse the order to be ascending
- for i := len(series.Points) - 1; i >= 0; i-- {
- point := series.Points[i]
- value := point.Value.DoubleValue
-
- if series.ValueType == "INT64" {
- parsedValue, err := strconv.ParseFloat(point.Value.IntValue, 64)
- if err == nil {
- value = parsedValue
- }
- }
-
- if series.ValueType == "BOOL" {
- if point.Value.BoolValue {
- value = 1
- } else {
- value = 0
- }
- }
-
- points = append(points, tsdb.NewTimePoint(null.FloatFrom(value), float64((point.Interval.EndTime).Unix())*1000))
- }
-
defaultMetricName := series.Metric.Type
for key, value := range series.Metric.Labels {
@@ -338,18 +356,87 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
if !containsLabel(resourceLabels[key], value) {
resourceLabels[key] = append(resourceLabels[key], value)
}
-
if containsLabel(query.GroupBys, "resource.label."+key) {
defaultMetricName += " " + value
}
}
- metricName := formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, query)
+ // reverse the order to be ascending
+ if series.ValueType != "DISTRIBUTION" {
+ for i := len(series.Points) - 1; i >= 0; i-- {
+ point := series.Points[i]
+ value := point.Value.DoubleValue
- queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
- Name: metricName,
- Points: points,
- })
+ if series.ValueType == "INT64" {
+ parsedValue, err := strconv.ParseFloat(point.Value.IntValue, 64)
+ if err == nil {
+ value = parsedValue
+ }
+ }
+
+ if series.ValueType == "BOOL" {
+ if point.Value.BoolValue {
+ value = 1
+ } else {
+ value = 0
+ }
+ }
+
+ points = append(points, tsdb.NewTimePoint(null.FloatFrom(value), float64((point.Interval.EndTime).Unix())*1000))
+ }
+
+ metricName := formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, make(map[string]string), query)
+
+ queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
+ Name: metricName,
+ Points: points,
+ })
+ } else {
+ buckets := make(map[int]*tsdb.TimeSeries)
+
+ for i := len(series.Points) - 1; i >= 0; i-- {
+ point := series.Points[i]
+ if len(point.Value.DistributionValue.BucketCounts) == 0 {
+ continue
+ }
+ maxKey := 0
+ for i := 0; i < len(point.Value.DistributionValue.BucketCounts); i++ {
+ value, err := strconv.ParseFloat(point.Value.DistributionValue.BucketCounts[i], 64)
+ if err != nil {
+ continue
+ }
+ if _, ok := buckets[i]; !ok {
+ // set lower bounds
+ // https://cloud.google.com/monitoring/api/ref_v3/rest/v3/TimeSeries#Distribution
+ bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
+ additionalLabels := map[string]string{"bucket": bucketBound}
+ buckets[i] = &tsdb.TimeSeries{
+ Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
+ Points: make([]tsdb.TimePoint, 0),
+ }
+ if maxKey < i {
+ maxKey = i
+ }
+ }
+ buckets[i].Points = append(buckets[i].Points, tsdb.NewTimePoint(null.FloatFrom(value), float64((point.Interval.EndTime).Unix())*1000))
+ }
+
+ // fill empty bucket
+ for i := 0; i < maxKey; i++ {
+ if _, ok := buckets[i]; !ok {
+ bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
+ additionalLabels := map[string]string{"bucket": bucketBound}
+ buckets[i] = &tsdb.TimeSeries{
+ Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
+ Points: make([]tsdb.TimePoint, 0),
+ }
+ }
+ }
+ }
+ for i := 0; i < len(buckets); i++ {
+ queryRes.Series = append(queryRes.Series, buckets[i])
+ }
+ }
}
queryRes.Meta.Set("resourceLabels", resourceLabels)
@@ -368,7 +455,7 @@ 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 {
+func formatLegendKeys(metricType string, defaultMetricName string, metricLabels map[string]string, resourceLabels map[string]string, additionalLabels map[string]string, query *StackdriverQuery) string {
if query.AliasBy == "" {
return defaultMetricName
}
@@ -400,6 +487,10 @@ func formatLegendKeys(metricType string, defaultMetricName string, metricLabels
return []byte(val)
}
+ if val, exists := additionalLabels[metaPartName]; exists {
+ return []byte(val)
+ }
+
return in
})
@@ -425,6 +516,22 @@ func replaceWithMetricPart(metaPartName string, metricType string) []byte {
return nil
}
+func calcBucketBound(bucketOptions StackdriverBucketOptions, n int) string {
+ bucketBound := "0"
+ if n == 0 {
+ return bucketBound
+ }
+
+ if bucketOptions.LinearBuckets != nil {
+ bucketBound = strconv.FormatInt(bucketOptions.LinearBuckets.Offset+(bucketOptions.LinearBuckets.Width*int64(n-1)), 10)
+ } else if bucketOptions.ExponentialBuckets != nil {
+ bucketBound = strconv.FormatInt(int64(bucketOptions.ExponentialBuckets.Scale*math.Pow(bucketOptions.ExponentialBuckets.GrowthFactor, float64(n-1))), 10)
+ } else if bucketOptions.ExplicitBuckets != nil {
+ bucketBound = strconv.FormatInt(bucketOptions.ExplicitBuckets.Bounds[(n-1)], 10)
+ }
+ return bucketBound
+}
+
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 da4d6890207..784bf4a7fbb 100644
--- a/pkg/tsdb/stackdriver/stackdriver_test.go
+++ b/pkg/tsdb/stackdriver/stackdriver_test.go
@@ -4,6 +4,8 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
+ "math"
+ "strconv"
"testing"
"time"
@@ -341,6 +343,137 @@ func TestStackdriver(t *testing.T) {
})
})
})
+
+ Convey("when data from query is distribution", func() {
+ data, err := loadTestFile("./test-data/3-series-response-distribution.json")
+ So(err, ShouldBeNil)
+ So(len(data.TimeSeries), ShouldEqual, 1)
+
+ res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
+ query := &StackdriverQuery{AliasBy: "{{bucket}}"}
+ err = executor.parseResponse(res, data, query)
+ So(err, ShouldBeNil)
+
+ So(len(res.Series), ShouldEqual, 11)
+ for i := 0; i < 11; i++ {
+ if i == 0 {
+ So(res.Series[i].Name, ShouldEqual, "0")
+ } else {
+ So(res.Series[i].Name, ShouldEqual, strconv.FormatInt(int64(math.Pow(float64(2), float64(i-1))), 10))
+ }
+ So(len(res.Series[i].Points), ShouldEqual, 3)
+ }
+
+ Convey("timestamps should be in ascending order", func() {
+ So(res.Series[0].Points[0][1].Float64, ShouldEqual, 1536668940000)
+ So(res.Series[0].Points[1][1].Float64, ShouldEqual, 1536669000000)
+ So(res.Series[0].Points[2][1].Float64, ShouldEqual, 1536669060000)
+ })
+
+ Convey("value should be correct", func() {
+ So(res.Series[8].Points[0][0].Float64, ShouldEqual, 1)
+ So(res.Series[9].Points[0][0].Float64, ShouldEqual, 1)
+ So(res.Series[10].Points[0][0].Float64, ShouldEqual, 1)
+ So(res.Series[8].Points[1][0].Float64, ShouldEqual, 0)
+ So(res.Series[9].Points[1][0].Float64, ShouldEqual, 0)
+ So(res.Series[10].Points[1][0].Float64, ShouldEqual, 1)
+ So(res.Series[8].Points[2][0].Float64, ShouldEqual, 0)
+ So(res.Series[9].Points[2][0].Float64, ShouldEqual, 1)
+ So(res.Series[10].Points[2][0].Float64, ShouldEqual, 0)
+ })
+ })
+
+ })
+
+ Convey("when interpolating filter wildcards", func() {
+ Convey("and wildcard is used in the beginning and the end of the word", func() {
+ Convey("and theres no wildcard in the middle of the word", func() {
+ value := interpolateFilterWildcards("*-central1*")
+ So(value, ShouldEqual, `has_substring("-central1")`)
+ })
+ Convey("and there is a wildcard in the middle of the word", func() {
+ value := interpolateFilterWildcards("*-cent*ral1*")
+ So(value, ShouldNotStartWith, `has_substring`)
+ })
+ })
+
+ Convey("and wildcard is used in the beginning of the word", func() {
+ Convey("and there is not a wildcard elsewhere in the word", func() {
+ value := interpolateFilterWildcards("*-central1")
+ So(value, ShouldEqual, `ends_with("-central1")`)
+ })
+ Convey("and there is a wildcard elsewhere in the word", func() {
+ value := interpolateFilterWildcards("*-cent*al1")
+ So(value, ShouldNotStartWith, `ends_with`)
+ })
+ })
+
+ Convey("and wildcard is used at the end of the word", func() {
+ Convey("and there is not a wildcard elsewhere in the word", func() {
+ value := interpolateFilterWildcards("us-central*")
+ So(value, ShouldEqual, `starts_with("us-central")`)
+ })
+ Convey("and there is a wildcard elsewhere in the word", func() {
+ value := interpolateFilterWildcards("*us-central*")
+ So(value, ShouldNotStartWith, `starts_with`)
+ })
+ })
+
+ Convey("and wildcard is used in the middle of the word", func() {
+ Convey("and there is only one wildcard", func() {
+ value := interpolateFilterWildcards("us-ce*tral1-b")
+ So(value, ShouldEqual, `monitoring.regex.full_match("^us\\-ce.*tral1\\-b$")`)
+ })
+
+ Convey("and there is more than one wildcard", func() {
+ value := interpolateFilterWildcards("us-ce*tra*1-b")
+ So(value, ShouldEqual, `monitoring.regex.full_match("^us\\-ce.*tra.*1\\-b$")`)
+ })
+ })
+
+ Convey("and wildcard is used in the middle of the word and in the beginning of the word", func() {
+ value := interpolateFilterWildcards("*s-ce*tral1-b")
+ So(value, ShouldEqual, `monitoring.regex.full_match("^.*s\\-ce.*tral1\\-b$")`)
+ })
+
+ Convey("and wildcard is used in the middle of the word and in the ending of the word", func() {
+ value := interpolateFilterWildcards("us-ce*tral1-*")
+ So(value, ShouldEqual, `monitoring.regex.full_match("^us\\-ce.*tral1\\-.*$")`)
+ })
+
+ Convey("and no wildcard is used", func() {
+ value := interpolateFilterWildcards("us-central1-a}")
+ So(value, ShouldEqual, `us-central1-a}`)
+ })
+ })
+
+ Convey("when building filter string", func() {
+ Convey("and theres no regex operator", func() {
+ Convey("and there are wildcards in a filter value", func() {
+ filterParts := []interface{}{"zone", "=", "*-central1*"}
+ value := buildFilterString("somemetrictype", filterParts)
+ So(value, ShouldEqual, `metric.type="somemetrictype" zone=has_substring("-central1")`)
+ })
+
+ Convey("and there are no wildcards in any filter value", func() {
+ filterParts := []interface{}{"zone", "!=", "us-central1-a"}
+ value := buildFilterString("somemetrictype", filterParts)
+ So(value, ShouldEqual, `metric.type="somemetrictype" zone!="us-central1-a"`)
+ })
+ })
+
+ Convey("and there is a regex operator", func() {
+ filterParts := []interface{}{"zone", "=~", "us-central1-a~"}
+ value := buildFilterString("somemetrictype", filterParts)
+ Convey("it should remove the ~ character from the operator that belongs to the value", func() {
+ So(value, ShouldNotContainSubstring, `=~`)
+ So(value, ShouldContainSubstring, `zone=`)
+ })
+
+ Convey("it should insert monitoring.regex.full_match before filter value", func() {
+ So(value, ShouldContainSubstring, `zone=monitoring.regex.full_match("us-central1-a~")`)
+ })
+ })
})
})
}
diff --git a/pkg/tsdb/stackdriver/test-data/3-series-response-distribution.json b/pkg/tsdb/stackdriver/test-data/3-series-response-distribution.json
new file mode 100644
index 00000000000..8603f78eab4
--- /dev/null
+++ b/pkg/tsdb/stackdriver/test-data/3-series-response-distribution.json
@@ -0,0 +1,112 @@
+{
+ "timeSeries": [
+ {
+ "metric": {
+ "type": "loadbalancing.googleapis.com\/https\/backend_latencies"
+ },
+ "resource": {
+ "type": "https_lb_rule",
+ "labels": {
+ "project_id": "grafana-prod"
+ }
+ },
+ "metricKind": "DELTA",
+ "valueType": "DISTRIBUTION",
+ "points": [
+ {
+ "interval": {
+ "startTime": "2018-09-11T12:30:00Z",
+ "endTime": "2018-09-11T12:31:00Z"
+ },
+ "value": {
+ "distributionValue": {
+ "count": "1",
+ "bucketOptions": {
+ "exponentialBuckets": {
+ "numFiniteBuckets": 10,
+ "growthFactor": 2,
+ "scale": 1
+ }
+ },
+ "bucketCounts": [
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "1",
+ "0"
+ ]
+ }
+ }
+ },
+ {
+ "interval": {
+ "startTime": "2018-09-11T12:29:00Z",
+ "endTime": "2018-09-11T12:30:00Z"
+ },
+ "value": {
+ "distributionValue": {
+ "count": "1",
+ "bucketOptions": {
+ "exponentialBuckets": {
+ "numFiniteBuckets": 10,
+ "growthFactor": 2,
+ "scale": 1
+ }
+ },
+ "bucketCounts": [
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "1"
+ ]
+ }
+ }
+ },
+ {
+ "interval": {
+ "startTime": "2018-09-11T12:28:00Z",
+ "endTime": "2018-09-11T12:29:00Z"
+ },
+ "value": {
+ "distributionValue": {
+ "count": "3",
+ "bucketOptions": {
+ "exponentialBuckets": {
+ "numFiniteBuckets": 10,
+ "growthFactor": 2,
+ "scale": 1
+ }
+ },
+ "bucketCounts": [
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "1",
+ "1",
+ "1"
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/pkg/tsdb/stackdriver/types.go b/pkg/tsdb/stackdriver/types.go
index c58ac2968f2..3821ce7ceda 100644
--- a/pkg/tsdb/stackdriver/types.go
+++ b/pkg/tsdb/stackdriver/types.go
@@ -14,6 +14,22 @@ type StackdriverQuery struct {
AliasBy string
}
+type StackdriverBucketOptions struct {
+ LinearBuckets *struct {
+ NumFiniteBuckets int64 `json:"numFiniteBuckets"`
+ Width int64 `json:"width"`
+ Offset int64 `json:"offset"`
+ } `json:"linearBuckets"`
+ ExponentialBuckets *struct {
+ NumFiniteBuckets int64 `json:"numFiniteBuckets"`
+ GrowthFactor float64 `json:"growthFactor"`
+ Scale float64 `json:"scale"`
+ } `json:"exponentialBuckets"`
+ ExplicitBuckets *struct {
+ Bounds []int64 `json:"bounds"`
+ } `json:"explicitBuckets"`
+}
+
// StackdriverResponse is the data returned from the external Google Stackdriver API
type StackdriverResponse struct {
TimeSeries []struct {
@@ -33,10 +49,26 @@ type StackdriverResponse struct {
EndTime time.Time `json:"endTime"`
} `json:"interval"`
Value struct {
- DoubleValue float64 `json:"doubleValue"`
- StringValue string `json:"stringValue"`
- BoolValue bool `json:"boolValue"`
- IntValue string `json:"int64Value"`
+ DoubleValue float64 `json:"doubleValue"`
+ StringValue string `json:"stringValue"`
+ BoolValue bool `json:"boolValue"`
+ IntValue string `json:"int64Value"`
+ DistributionValue struct {
+ Count string `json:"count"`
+ Mean float64 `json:"mean"`
+ SumOfSquaredDeviation float64 `json:"sumOfSquaredDeviation"`
+ Range struct {
+ Min int `json:"min"`
+ Max int `json:"max"`
+ } `json:"range"`
+ BucketOptions StackdriverBucketOptions `json:"bucketOptions"`
+ BucketCounts []string `json:"bucketCounts"`
+ Examplars []struct {
+ Value float64 `json:"value"`
+ Timestamp string `json:"timestamp"`
+ // attachments
+ } `json:"examplars"`
+ } `json:"distributionValue"`
} `json:"value"`
} `json:"points"`
} `json:"timeSeries"`
diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts
index e2b99d69df9..e096e44ac25 100644
--- a/public/app/plugins/datasource/cloudwatch/datasource.ts
+++ b/public/app/plugins/datasource/cloudwatch/datasource.ts
@@ -131,7 +131,7 @@ export default class CloudWatchDatasource {
if (res.results) {
_.forEach(res.results, queryRes => {
_.forEach(queryRes.series, series => {
- data.push({ target: series.name, datapoints: series.points });
+ data.push({ target: series.name, datapoints: series.points, unit: queryRes.meta.unit || 'none' });
});
});
}
diff --git a/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html b/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html
index 7da6e7d2a83..2a951bc9257 100644
--- a/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html
+++ b/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html
@@ -37,8 +37,7 @@
Id
{{ctrl.lastQueryMeta.rawQueryString}}
Alias Patterns
Format the legend keys any way you want by using alias patterns. ++Alias Patterns
-{{metric.name}} - {{metric.label.instance_name}}+ Format the legend keys any way you want by using alias patterns.
-cpu/usage_time - server1-europe-west-1+ Example:{{metric.name}} - {{metric.label.instance_name}}
+ Result:cpu/usage_time - server1-europe-west-1
- -{{metric.type}}= metric type e.g. compute.googleapis.com/instance/cpu/usage_time -{{metric.name}}= name part of metric e.g. instance/cpu/usage_time -{{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 - + Patterns
++
- +
+{{metric.type}}= metric type e.g. compute.googleapis.com/instance/cpu/usage_time +- +
+{{metric.name}}= name part of metric e.g. instance/cpu/usage_time +- +
+{{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 +{{ctrl.lastQueryError}}diff --git a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts index ac279eec0d5..786b2831e89 100644 --- a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts @@ -96,11 +96,9 @@ export class StackdriverFilterCtrl { getServicesList() { const defaultValue = { value: this.$scope.defaultServiceValue, text: this.$scope.defaultServiceValue }; const services = this.metricDescriptors.map(m => { - const [service] = m.type.split('/'); - const [serviceShortName] = service.split('.'); return { - value: service, - text: serviceShortName, + value: m.service, + text: m.serviceShortName, }; }); @@ -113,12 +111,10 @@ export class StackdriverFilterCtrl { getMetricsList() { const metrics = this.metricDescriptors.map(m => { - const [service] = m.type.split('/'); - const [serviceShortName] = service.split('.'); return { - service, + service: m.service, value: m.type, - serviceShortName, + serviceShortName: m.serviceShortName, text: m.displayName, title: m.description, }; diff --git a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts index 80830fd4d68..3117be402a9 100644 --- a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts @@ -164,11 +164,11 @@ describe('StackdriverDataSource', () => { metricDescriptors: [ { displayName: 'test metric name 1', - type: 'test metric type 1', + type: 'compute.googleapis.com/instance/cpu/test-metric-type-1', + description: 'A description', }, { - displayName: 'test metric name 2', - type: 'test metric type 2', + type: 'logging.googleapis.com/user/logbased-metric-with-no-display-name', }, ], }, @@ -180,8 +180,13 @@ describe('StackdriverDataSource', () => { }); it('should return successfully', () => { expect(result.length).toBe(2); - expect(result[0].type).toBe('test metric type 1'); + expect(result[0].service).toBe('compute.googleapis.com'); + expect(result[0].serviceShortName).toBe('compute'); + expect(result[0].type).toBe('compute.googleapis.com/instance/cpu/test-metric-type-1'); expect(result[0].displayName).toBe('test metric name 1'); + expect(result[0].description).toBe('A description'); + expect(result[1].type).toBe('logging.googleapis.com/user/logbased-metric-with-no-display-name'); + expect(result[1].displayName).toBe('logging.googleapis.com/user/logbased-metric-with-no-display-name'); }); }); diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 01590ace585..ae52fbc5ab5 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -115,8 +115,8 @@ $tight-form-func-bg: #333334; $tight-form-func-highlight-bg: #444445; $modal-backdrop-bg: #353c42; -$code-tag-bg: $gray-1; -$code-tag-border: lighten($code-tag-bg, 2%); +$code-tag-bg: $dark-1; +$code-tag-border: $dark-4; // cards $card-background: linear-gradient(135deg, #2f2f32, #262628);