GoogleCloudMonitoring: Refactor SLO queries (#59421)

* GoogleCloudMonitoring: Refactor metricType input

* Remove preprocessor in favor of secondary inputs

* GoogleCloudMonitoring: Refactor SLO queries

* GoogleCloudMonitoring: Refactor util functions (#59482)

* GoogleCloudMonitoring: Remove unnecessary util functions

* GoogleCloudMonitoring: Refactor run function (#59505)
This commit is contained in:
Andres Martinez Gotor
2022-11-30 17:23:05 +01:00
committed by GitHub
parent 4ec08c4317
commit 2e64ea652e
9 changed files with 487 additions and 471 deletions

View File

@ -7,14 +7,13 @@ import (
"io"
"math"
"net/http"
"net/url"
"path"
"regexp"
"strconv"
"strings"
"time"
"github.com/grafana/grafana-google-sdk-go/pkg/utils"
"github.com/huandu/xstrings"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
@ -25,7 +24,6 @@ import (
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/setting"
)
var (
@ -33,11 +31,9 @@ var (
)
var (
matchAllCap = regexp.MustCompile("(.)([A-Z][a-z]*)")
legendKeyFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
metricNameFormat = regexp.MustCompile(`([\w\d_]+)\.(googleapis\.com|io)/(.+)`)
wildcardRegexRe = regexp.MustCompile(`[-\/^$+?.()|[\]{}]`)
alignmentPeriodRe = regexp.MustCompile("[0-9]+")
cloudMonitoringUnitMappings = map[string]string{
"bit": "bits",
"By": "bytes",
@ -333,33 +329,6 @@ func migrateRequest(req *backend.QueryDataRequest) error {
q.JSON = b
}
// SloQuery was merged into timeSeriesList
if rawQuery["sloQuery"] != nil {
if rawQuery["timeSeriesList"] == nil {
rawQuery["timeSeriesList"] = &timeSeriesList{}
}
tsl := rawQuery["timeSeriesList"].(*timeSeriesList)
sloq := rawQuery["sloQuery"].(map[string]interface{})
if sloq["projectName"] != nil {
tsl.ProjectName = sloq["projectName"].(string)
}
if sloq["alignmentPeriod"] != nil {
tsl.AlignmentPeriod = sloq["alignmentPeriod"].(string)
}
if sloq["perSeriesAligner"] != nil {
tsl.PerSeriesAligner = sloq["perSeriesAligner"].(string)
}
rawQuery["timeSeriesList"] = tsl
b, err := json.Marshal(rawQuery)
if err != nil {
return err
}
if q.QueryType == "" {
q.QueryType = sloQueryType
}
q.JSON = b
}
req.Queries[i] = q
}
@ -437,16 +406,7 @@ func (s *Service) buildQueryExecutors(logger log.Logger, req *backend.QueryDataR
return nil, fmt.Errorf("could not unmarshal CloudMonitoringQuery json: %w", err)
}
params := url.Values{}
params.Add("interval.startTime", startTime.UTC().Format(time.RFC3339))
params.Add("interval.endTime", endTime.UTC().Format(time.RFC3339))
var queryInterface cloudMonitoringQueryExecutor
cmtsf := &cloudMonitoringTimeSeriesList{
refID: query.RefID,
logger: logger,
aliasBy: q.AliasBy,
}
switch query.QueryType {
case metricQueryType, annotationQueryType:
if q.TimeSeriesQuery != nil {
@ -458,33 +418,33 @@ func (s *Service) buildQueryExecutors(logger log.Logger, req *backend.QueryDataR
timeRange: req.Queries[0].TimeRange,
}
} else if q.TimeSeriesList != nil {
cmtsf := &cloudMonitoringTimeSeriesList{
refID: query.RefID,
logger: logger,
aliasBy: q.AliasBy,
}
if q.TimeSeriesList.View == "" {
q.TimeSeriesList.View = "FULL"
}
cmtsf.parameters = q.TimeSeriesList
params.Add("filter", buildFilterString(q.TimeSeriesList.Filters))
params.Add("view", q.TimeSeriesList.View)
setMetricAggParams(&params, q.TimeSeriesList, durationSeconds, query.Interval.Milliseconds())
cmtsf.setParams(startTime, endTime, durationSeconds, query.Interval.Milliseconds())
queryInterface = cmtsf
} else {
return nil, fmt.Errorf("missing query info")
}
case sloQueryType:
cmtsf.sloQ = q.SloQuery
cmtsf.parameters = q.TimeSeriesList
params.Add("filter", buildSLOFilterExpression(q.TimeSeriesList.ProjectName, q.SloQuery))
setSloAggParams(&params, q.SloQuery, q.TimeSeriesList.AlignmentPeriod, durationSeconds, query.Interval.Milliseconds())
queryInterface = cmtsf
cmslo := &cloudMonitoringSLO{
refID: query.RefID,
logger: logger,
aliasBy: q.AliasBy,
parameters: q.SloQuery,
}
cmslo.setParams(startTime, endTime, durationSeconds, query.Interval.Milliseconds())
queryInterface = cmslo
default:
return nil, fmt.Errorf("unrecognized query type %q", query.QueryType)
}
cmtsf.params = params
if setting.Env == setting.Dev {
logger.Debug("CloudMonitoring request", "params", params)
}
cloudMonitoringQueryExecutors = append(cloudMonitoringQueryExecutors, queryInterface)
}
@ -501,7 +461,7 @@ func interpolateFilterWildcards(value string) string {
value = strings.Replace(value, "*", "", 1)
value = fmt.Sprintf(`ends_with("%s")`, value)
case matches == 1 && strings.HasSuffix(value, "*"):
value = reverse(strings.Replace(reverse(value), "*", "", 1))
value = xstrings.Reverse(strings.Replace(xstrings.Reverse(value), "*", "", 1))
value = fmt.Sprintf(`starts_with("%s")`, value)
case matches != 0:
value = string(wildcardRegexRe.ReplaceAllFunc([]byte(value), func(in []byte) []byte {
@ -515,87 +475,6 @@ func interpolateFilterWildcards(value string) string {
return value
}
func buildFilterString(filterParts []string) string {
filterString := ""
for i, part := range filterParts {
mod := i % 4
switch {
case part == "AND":
filterString += " "
case mod == 2:
operator := filterParts[i-1]
switch {
case operator == "=~" || operator == "!=~":
filterString = reverse(strings.Replace(reverse(filterString), "~", "", 1))
filterString += fmt.Sprintf(`monitoring.regex.full_match("%s")`, part)
case strings.Contains(part, "*"):
filterString += interpolateFilterWildcards(part)
default:
filterString += fmt.Sprintf(`"%s"`, part)
}
default:
filterString += part
}
}
return strings.Trim(filterString, " ")
}
func buildSLOFilterExpression(projectName string, q *sloQuery) string {
sloName := fmt.Sprintf("projects/%s/services/%s/serviceLevelObjectives/%s", projectName, q.ServiceId, q.SloId)
if q.SelectorName == "select_slo_burn_rate" {
return fmt.Sprintf(`%s("%s", "%s")`, q.SelectorName, sloName, q.LookbackPeriod)
} else {
return fmt.Sprintf(`%s("%s")`, q.SelectorName, sloName)
}
}
func setMetricAggParams(params *url.Values, query *timeSeriesList, durationSeconds int, intervalMs int64) {
if query.CrossSeriesReducer == "" {
query.CrossSeriesReducer = crossSeriesReducerDefault
}
if query.PerSeriesAligner == "" {
query.PerSeriesAligner = perSeriesAlignerDefault
}
alignmentPeriod := calculateAlignmentPeriod(query.AlignmentPeriod, intervalMs, durationSeconds)
params.Add("aggregation.alignmentPeriod", alignmentPeriod)
if query.CrossSeriesReducer != "" {
params.Add("aggregation.crossSeriesReducer", query.CrossSeriesReducer)
}
if query.PerSeriesAligner != "" {
params.Add("aggregation.perSeriesAligner", query.PerSeriesAligner)
}
for _, groupBy := range query.GroupBys {
params.Add("aggregation.groupByFields", groupBy)
}
if query.SecondaryAlignmentPeriod != "" {
secondaryAlignmentPeriod := calculateAlignmentPeriod(query.AlignmentPeriod, intervalMs, durationSeconds)
params.Add("secondaryAggregation.alignmentPeriod", secondaryAlignmentPeriod)
}
if query.SecondaryCrossSeriesReducer != "" {
params.Add("secondaryAggregation.crossSeriesReducer", query.SecondaryCrossSeriesReducer)
}
if query.SecondaryPerSeriesAligner != "" {
params.Add("secondaryAggregation.perSeriesAligner", query.SecondaryPerSeriesAligner)
}
for _, groupBy := range query.SecondaryGroupBys {
params.Add("secondaryAggregation.groupByFields", groupBy)
}
}
func setSloAggParams(params *url.Values, query *sloQuery, alignmentPeriod string, durationSeconds int, intervalMs int64) {
params.Add("aggregation.alignmentPeriod", calculateAlignmentPeriod(alignmentPeriod, intervalMs, durationSeconds))
if query.SelectorName == "select_slo_health" {
params.Add("aggregation.perSeriesAligner", "ALIGN_MEAN")
} else {
params.Add("aggregation.perSeriesAligner", "ALIGN_NEXT_OLDER")
}
}
func calculateAlignmentPeriod(alignmentPeriod string, intervalMs int64, durationSeconds int) string {
if alignmentPeriod == "grafana-auto" || alignmentPeriod == "" {
alignmentPeriodValue := int(math.Max(float64(intervalMs)/1000, 60.0))
@ -618,12 +497,12 @@ func calculateAlignmentPeriod(alignmentPeriod string, intervalMs int64, duration
}
func formatLegendKeys(metricType string, defaultMetricName string, labels map[string]string,
additionalLabels map[string]string, query *cloudMonitoringTimeSeriesList) string {
if query.aliasBy == "" {
additionalLabels map[string]string, query cloudMonitoringQueryExecutor) string {
if query.getAliasBy() == "" {
return defaultMetricName
}
result := legendKeyFormat.ReplaceAllFunc([]byte(query.aliasBy), func(in []byte) []byte {
result := legendKeyFormat.ReplaceAllFunc([]byte(query.getAliasBy()), func(in []byte) []byte {
metaPartName := strings.Replace(string(in), "{{", "", 1)
metaPartName = strings.Replace(metaPartName, "}}", "", 1)
metaPartName = strings.TrimSpace(metaPartName)
@ -646,20 +525,8 @@ func formatLegendKeys(metricType string, defaultMetricName string, labels map[st
return []byte(val)
}
if metaPartName == "project" && query.parameters.ProjectName != "" {
return []byte(query.parameters.ProjectName)
}
if metaPartName == "service" && query.sloQ.ServiceId != "" {
return []byte(query.sloQ.ServiceId)
}
if metaPartName == "slo" && query.sloQ.SloId != "" {
return []byte(query.sloQ.SloId)
}
if metaPartName == "selector" && query.sloQ.SelectorName != "" {
return []byte(query.sloQ.SelectorName)
if query.getParameter(metaPartName) != "" {
return []byte(query.getParameter(metaPartName))
}
return in
@ -709,27 +576,11 @@ func calcBucketBound(bucketOptions cloudMonitoringBucketOptions, n int) string {
return bucketBound
}
func (s *Service) createRequest(logger log.Logger, dsInfo *datasourceInfo, proxyPass string, body io.Reader) (*http.Request, error) {
u, err := url.Parse(dsInfo.url)
if err != nil {
return nil, err
func (s *Service) ensureProject(ctx context.Context, dsInfo datasourceInfo, projectName string) (string, error) {
if projectName != "" {
return projectName, nil
}
u.Path = path.Join(u.Path, "render")
method := http.MethodGet
if body != nil {
method = http.MethodPost
}
req, err := http.NewRequest(method, dsInfo.services[cloudMonitor].url, body)
if err != nil {
logger.Error("Failed to create request", "error", err)
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.URL.Path = proxyPass
return req, nil
return s.getDefaultProject(ctx, dsInfo)
}
func (s *Service) getDefaultProject(ctx context.Context, dsInfo datasourceInfo) (string, error) {