Merge pull request #13589 from grafana/cp_v5.3.0
Cherry-picks for v5.3.0 stable
This commit is contained in:
@@ -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}}`
|
||||
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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": "",
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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~")`)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,8 +37,7 @@
|
||||
Id
|
||||
<info-popover mode="right-normal ">Id can include numbers, letters, and underscore, and must start with a lowercase letter.</info-popover>
|
||||
</label>
|
||||
<input type="text " class="gf-form-input " ng-model="target.id " spellcheck='false' ng-pattern='/^[a-z][A-Z0-9_]*/' ng-model-onblur
|
||||
ng-change="onChange() ">
|
||||
<input type="text " class="gf-form-input " ng-model="target.id " spellcheck='false' ng-pattern='/^[a-z][a-zA-Z0-9_]*$/' ng-model-onblur ng-change="onChange() ">
|
||||
</div>
|
||||
<div class="gf-form max-width-30 ">
|
||||
<label class="gf-form-label query-keyword width-7 ">Expression</label>
|
||||
|
||||
@@ -60,6 +60,7 @@ describe('CloudWatchDatasource', () => {
|
||||
A: {
|
||||
error: '',
|
||||
refId: 'A',
|
||||
meta: {},
|
||||
series: [
|
||||
{
|
||||
name: 'CPUUtilization_Average',
|
||||
@@ -221,6 +222,7 @@ describe('CloudWatchDatasource', () => {
|
||||
A: {
|
||||
error: '',
|
||||
refId: 'A',
|
||||
meta: {},
|
||||
series: [
|
||||
{
|
||||
name: 'TargetResponseTime_p90.00',
|
||||
|
||||
@@ -19,7 +19,7 @@ export const alignOptions = [
|
||||
{
|
||||
text: 'delta',
|
||||
value: 'ALIGN_DELTA',
|
||||
valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY],
|
||||
valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION],
|
||||
metricKinds: [MetricKind.CUMULATIVE, MetricKind.DELTA],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -241,7 +241,17 @@ export default class StackdriverDatasource {
|
||||
try {
|
||||
const metricsApiPath = `v3/projects/${projectId}/metricDescriptors`;
|
||||
const { data } = await this.doRequest(`${this.baseUrl}${metricsApiPath}`);
|
||||
return data.metricDescriptors;
|
||||
|
||||
const metrics = data.metricDescriptors.map(m => {
|
||||
const [service] = m.type.split('/');
|
||||
const [serviceShortName] = service.split('.');
|
||||
m.service = service;
|
||||
m.serviceShortName = serviceShortName;
|
||||
m.displayName = m.displayName || m.type;
|
||||
return m;
|
||||
});
|
||||
|
||||
return metrics;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -40,21 +40,33 @@
|
||||
<div class="gf-form" ng-show="ctrl.showLastQuery">
|
||||
<pre class="gf-form-pre">{{ctrl.lastQueryMeta.rawQueryString}}</pre>
|
||||
</div>
|
||||
<div class="gf-form grafana-info-box" style="padding: 0" ng-show="ctrl.showHelp">
|
||||
<pre class="gf-form-pre alert alert-info" style="margin-right: 0"><h5>Alias Patterns</h5>Format the legend keys any way you want by using alias patterns.
|
||||
<div class="grafana-info-box m-t-2 markdown-html" ng-show="ctrl.showHelp">
|
||||
<h5>Alias Patterns</h5>
|
||||
|
||||
<label>Example: </label><code ng-non-bindable>{{metric.name}} - {{metric.label.instance_name}}</code>
|
||||
Format the legend keys any way you want by using alias patterns.<br /> <br />
|
||||
|
||||
<label>Result: </label><code ng-non-bindable>cpu/usage_time - server1-europe-west-1</code>
|
||||
Example: <code ng-non-bindable>{{metric.name}} - {{metric.label.instance_name}}</code><br />
|
||||
Result: <code ng-non-bindable>cpu/usage_time - server1-europe-west-1</code><br /><br />
|
||||
|
||||
<label>Patterns:</label>
|
||||
<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. instance/cpu/usage_time
|
||||
<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>
|
||||
<strong>Patterns</strong><br />
|
||||
<ul>
|
||||
<li>
|
||||
<code ng-non-bindable>{{metric.type}}</code> = metric type e.g. compute.googleapis.com/instance/cpu/usage_time
|
||||
</li>
|
||||
<li>
|
||||
<code ng-non-bindable>{{metric.name}}</code> = name part of metric e.g. instance/cpu/usage_time
|
||||
</li>
|
||||
<li>
|
||||
<code ng-non-bindable>{{metric.service}}</code> = service part of metric e.g. compute
|
||||
</li>
|
||||
<li>
|
||||
<code ng-non-bindable>{{metric.label.label_name}}</code> = Metric label metadata e.g.
|
||||
metric.label.instance_name
|
||||
</li>
|
||||
<li>
|
||||
<code ng-non-bindable>{{resource.label.label_name}}</code> = Resource label metadata e.g. resource.label.zone
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.lastQueryError">
|
||||
<pre class="gf-form-pre alert alert-error">{{ctrl.lastQueryError}}</pre>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user