package cloudwatch import ( "context" "fmt" "regexp" "golang.org/x/sync/errgroup" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/features" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/models" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils" ) type responseWrapper struct { DataResponse *backend.DataResponse RefId string } func (ds *DataSource) executeTimeSeriesQuery(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { ds.logger.FromContext(ctx).Debug("Executing time series query") resp := backend.NewQueryDataResponse() if len(req.Queries) == 0 { return nil, backend.DownstreamError(fmt.Errorf("request contains no queries")) } timeBatches := utils.BatchDataQueriesByTimeRange(req.Queries) requestQueriesByTimeAndRegion := make(map[string][]*models.CloudWatchQuery) for i, timeBatch := range timeBatches { startTime := timeBatch[0].TimeRange.From endTime := timeBatch[0].TimeRange.To if !startTime.Before(endTime) { return nil, backend.DownstreamError(fmt.Errorf("invalid time range: start time must be before end time")) } requestQueries, err := models.ParseMetricDataQueries(timeBatch, startTime, endTime, ds.Settings.Region, ds.logger.FromContext(ctx), features.IsEnabled(ctx, features.FlagCloudWatchCrossAccountQuerying)) if err != nil { return nil, err } for _, query := range requestQueries { key := fmt.Sprintf("%d %s", i, query.Region) if _, exist := requestQueriesByTimeAndRegion[key]; !exist { requestQueriesByTimeAndRegion[key] = []*models.CloudWatchQuery{} } requestQueriesByTimeAndRegion[key] = append(requestQueriesByTimeAndRegion[key], query) } } if len(requestQueriesByTimeAndRegion) == 0 { return backend.NewQueryDataResponse(), nil } resultChan := make(chan *responseWrapper, len(req.Queries)) eg, ectx := errgroup.WithContext(ctx) for _, timeAndRegionQueries := range requestQueriesByTimeAndRegion { batches := [][]*models.CloudWatchQuery{timeAndRegionQueries} if features.IsEnabled(ctx, features.FlagCloudWatchBatchQueries) { batches = getMetricQueryBatches(timeAndRegionQueries, ds.logger.FromContext(ctx)) } // region, startTime, and endTime are the same for the set of queries region := timeAndRegionQueries[0].Region startTime := timeAndRegionQueries[0].StartTime endTime := timeAndRegionQueries[0].EndTime for _, batch := range batches { requestQueries := batch eg.Go(func() error { defer func() { if err := recover(); err != nil { ds.logger.FromContext(ctx).Error("Execute Get Metric Data Query Panic", "error", err, "stack", utils.Stack(1)) if theErr, ok := err.(error); ok { resultChan <- &responseWrapper{ DataResponse: &backend.DataResponse{ Error: theErr, }, } } } }() client, err := ds.getCWClient(ctx, region) if err != nil { return err } metricDataInput, err := ds.buildMetricDataInput(ctx, startTime, endTime, requestQueries) if err != nil { return err } mdo, err := ds.executeRequest(ectx, client, metricDataInput) if err != nil { return err } requestQueries, err = ds.getDimensionValuesForWildcards(ctx, region, client, requestQueries, ds.tagValueCache, ds.Settings.GrafanaSettings.ListMetricsPageLimit, shouldSkipFetchingWildcards) if err != nil { return err } res, err := ds.parseResponse(ctx, mdo, requestQueries) if err != nil { return err } for _, responseWrapper := range res { resultChan <- responseWrapper } return nil }) } } if err := eg.Wait(); err != nil { dataResponse := backend.ErrorResponseWithErrorSource(fmt.Errorf("metric request error: %w", err)) resultChan <- &responseWrapper{ RefId: getQueryRefIdFromErrorString(err.Error(), requestQueriesByTimeAndRegion), DataResponse: &dataResponse, } } close(resultChan) for result := range resultChan { resp.Responses[result.RefId] = *result.DataResponse } return resp, nil } func getQueryRefIdFromErrorString(err string, queriesByRegion map[string][]*models.CloudWatchQuery) string { // error can be in format "Error in expression 'test': Invalid syntax" // so we can find the query id or ref id between the quotations erroredRefId := "" for _, queries := range queriesByRegion { for _, query := range queries { if regexp.MustCompile(`'`+query.RefId+`':`).MatchString(err) || regexp.MustCompile(`'`+query.Id+`':`).MatchString(err) { erroredRefId = query.RefId } } } // if errorRefId is empty, it means the error concerns all queries (error metric limit exceeded, for example) return erroredRefId }