|
|
@@ -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
|
|
|
+
|
|
|
+ if series.ValueType == "INT64" {
|
|
|
+ parsedValue, err := strconv.ParseFloat(point.Value.IntValue, 64)
|
|
|
+ if err == nil {
|
|
|
+ value = parsedValue
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
|
|
|
- Name: metricName,
|
|
|
- Points: points,
|
|
|
- })
|
|
|
+ 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")
|