2019-11-14 17:59:41 +08:00
package cloudwatch
import (
2024-04-05 23:57:56 +08:00
"context"
2019-11-14 17:59:41 +08:00
"fmt"
2024-03-19 05:30:59 +08:00
"regexp"
2020-01-17 22:27:03 +08:00
"sort"
2019-11-14 17:59:41 +08:00
"strings"
"time"
2025-06-26 21:56:50 +08:00
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
2021-09-08 22:06:43 +08:00
"github.com/grafana/grafana-plugin-sdk-go/backend"
2020-10-06 19:45:58 +08:00
"github.com/grafana/grafana-plugin-sdk-go/data"
2024-04-05 23:57:56 +08:00
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
2022-10-20 17:21:13 +08:00
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
2019-11-14 17:59:41 +08:00
)
2024-03-19 05:30:59 +08:00
// matches a dynamic label
var dynamicLabel = regexp . MustCompile ( ` \$\ { .+\} ` )
2025-06-26 21:56:50 +08:00
func ( ds * DataSource ) parseResponse ( ctx context . Context , metricDataOutputs [ ] * cloudwatch . GetMetricDataOutput ,
2022-10-20 17:21:13 +08:00
queries [ ] * models . CloudWatchQuery ) ( [ ] * responseWrapper , error ) {
2021-09-08 22:06:43 +08:00
aggregatedResponse := aggregateResponse ( metricDataOutputs )
2022-10-20 17:21:13 +08:00
queriesById := map [ string ] * models . CloudWatchQuery { }
2021-09-08 22:06:43 +08:00
for _ , query := range queries {
queriesById [ query . Id ] = query
}
results := [ ] * responseWrapper { }
for id , response := range aggregatedResponse {
queryRow := queriesById [ id ]
dataRes := backend . DataResponse { }
if response . HasArithmeticError {
dataRes . Error = fmt . Errorf ( "ArithmeticError in query %q: %s" , queryRow . RefId , response . ArithmeticErrorMessage )
}
2024-06-01 02:36:38 +08:00
if response . HasPermissionError {
dataRes . Error = fmt . Errorf ( "PermissionError in query %q: %s" , queryRow . RefId , response . PermissionErrorMessage )
}
2021-09-08 22:06:43 +08:00
var err error
2025-01-03 23:22:14 +08:00
dataRes . Frames , err = buildDataFrames ( ctx , response , queryRow )
2021-09-08 22:06:43 +08:00
if err != nil {
return nil , err
}
results = append ( results , & responseWrapper {
DataResponse : & dataRes ,
RefId : queryRow . RefId ,
} )
}
return results , nil
}
2022-12-02 17:21:46 +08:00
func aggregateResponse ( getMetricDataOutputs [ ] * cloudwatch . GetMetricDataOutput ) map [ string ] models . QueryRowResponse {
responseByID := make ( map [ string ] models . QueryRowResponse )
errors := map [ string ] bool {
models . MaxMetricsExceeded : false ,
models . MaxQueryTimeRangeExceeded : false ,
models . MaxQueryResultsExceeded : false ,
models . MaxMatchingResultsExceeded : false ,
2022-04-04 21:44:19 +08:00
}
2022-12-02 17:21:46 +08:00
// first check if any of the getMetricDataOutputs has any errors related to the request. if so, store the errors so they can be added to each query response
2021-09-08 22:06:43 +08:00
for _ , gmdo := range getMetricDataOutputs {
for _ , message := range gmdo . Messages {
2022-12-02 17:21:46 +08:00
if _ , exists := errors [ * message . Code ] ; exists {
errors [ * message . Code ] = true
2019-11-14 17:59:41 +08:00
}
}
2022-12-02 17:21:46 +08:00
}
for _ , gmdo := range getMetricDataOutputs {
2021-09-08 22:06:43 +08:00
for _ , r := range gmdo . MetricDataResults {
2020-08-07 17:48:40 +08:00
id := * r . Id
2021-09-08 22:06:43 +08:00
2022-12-02 17:21:46 +08:00
response := models . NewQueryRowResponse ( errors )
2021-09-08 22:06:43 +08:00
if _ , exists := responseByID [ id ] ; exists {
response = responseByID [ id ]
}
for _ , message := range r . Messages {
if * message . Code == "ArithmeticError" {
2022-12-02 17:21:46 +08:00
response . AddArithmeticError ( message . Value )
2024-06-01 02:36:38 +08:00
}
if * message . Code == "Forbidden" {
response . AddPermissionError ( message . Value )
2019-11-19 20:36:32 +08:00
}
2019-11-14 17:59:41 +08:00
}
2021-09-08 22:06:43 +08:00
2025-06-26 21:56:50 +08:00
response . AddMetricDataResult ( & r )
2021-09-08 22:06:43 +08:00
responseByID [ id ] = response
2019-11-14 17:59:41 +08:00
}
}
2021-09-08 22:06:43 +08:00
return responseByID
}
2019-11-14 17:59:41 +08:00
2024-04-05 23:57:56 +08:00
func parseLabels ( cloudwatchLabel string , query * models . CloudWatchQuery ) ( string , data . Labels ) {
dims := make ( [ ] string , 0 , len ( query . Dimensions ) )
for k := range query . Dimensions {
dims = append ( dims , k )
}
sort . Strings ( dims )
splitLabels := strings . Split ( cloudwatchLabel , keySeparator )
// The first part is the name of the time series, followed by the labels
2024-06-08 05:37:19 +08:00
name := splitLabels [ 0 ]
2024-04-05 23:57:56 +08:00
labelsIndex := 1
2024-06-08 05:37:19 +08:00
// set Series to the name of the time series as a fallback
labels := data . Labels { "Series" : name }
2024-07-13 02:24:09 +08:00
// do not parse labels for raw queries
if query . MetricEditorMode == models . MetricEditorModeRaw {
return name , labels
}
2024-04-05 23:57:56 +08:00
for _ , dim := range dims {
values := query . Dimensions [ dim ]
if isSingleValue ( values ) {
labels [ dim ] = values [ 0 ]
continue
}
labels [ dim ] = splitLabels [ labelsIndex ]
labelsIndex ++
}
2024-06-08 05:37:19 +08:00
return name , labels
2024-04-05 23:57:56 +08:00
}
2024-05-01 00:06:16 +08:00
func getLabels ( cloudwatchLabel string , query * models . CloudWatchQuery , addSeriesLabelAsFallback bool ) data . Labels {
2021-09-08 22:06:43 +08:00
dims := make ( [ ] string , 0 , len ( query . Dimensions ) )
for k := range query . Dimensions {
dims = append ( dims , k )
}
sort . Strings ( dims )
labels := data . Labels { }
2024-05-01 00:06:16 +08:00
if addSeriesLabelAsFallback {
labels [ "Series" ] = cloudwatchLabel
}
2021-09-08 22:06:43 +08:00
for _ , dim := range dims {
values := query . Dimensions [ dim ]
if len ( values ) == 1 && values [ 0 ] != "*" {
labels [ dim ] = values [ 0 ]
2024-05-01 00:06:16 +08:00
} else if len ( values ) == 0 {
// Metric Insights metrics might not have a value for a dimension specified in the `GROUP BY` clause for Metric Query type queries. When this happens, CloudWatch returns "Other" in the label for the dimension so `len(values)` would be 0.
// We manually add "Other" as the value for the dimension to match what CloudWatch returns in the label.
// See the note under `GROUP BY` in https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-metrics-insights-querylanguage.html
labels [ dim ] = "Other"
continue
2021-09-08 22:06:43 +08:00
} else {
for _ , value := range values {
if value == cloudwatchLabel || value == "*" {
labels [ dim ] = cloudwatchLabel
} else if strings . Contains ( cloudwatchLabel , value ) {
labels [ dim ] = value
}
}
2020-05-18 18:25:58 +08:00
}
2019-11-14 17:59:41 +08:00
}
2021-09-08 22:06:43 +08:00
return labels
2019-11-14 17:59:41 +08:00
}
2025-01-03 23:22:14 +08:00
func buildDataFrames ( ctx context . Context , aggregatedResponse models . QueryRowResponse ,
2023-04-27 17:19:45 +08:00
query * models . CloudWatchQuery ) ( data . Frames , error ) {
2020-10-06 19:45:58 +08:00
frames := data . Frames { }
2024-03-19 05:30:59 +08:00
hasStaticLabel := query . Label != "" && ! dynamicLabel . MatchString ( query . Label )
2022-05-18 15:16:38 +08:00
for _ , metric := range aggregatedResponse . Metrics {
label := * metric . Label
2019-11-14 17:59:41 +08:00
2025-01-03 23:22:14 +08:00
deepLink , err := query . BuildDeepLink ( query . StartTime , query . EndTime )
2021-09-08 22:06:43 +08:00
if err != nil {
return nil , err
2019-11-14 17:59:41 +08:00
}
2020-10-06 19:45:58 +08:00
// In case a multi-valued dimension is used and the cloudwatch query yields no values, create one empty time
// series for each dimension value. Use that dimension value to expand the alias field
2022-10-20 17:21:13 +08:00
if len ( metric . Values ) == 0 && query . IsMultiValuedDimensionExpression ( ) {
2024-04-05 23:57:56 +08:00
if features . IsEnabled ( ctx , features . FlagCloudWatchNewLabelParsing ) {
label , _ , _ = strings . Cut ( label , keySeparator )
}
2020-03-11 04:14:58 +08:00
series := 0
multiValuedDimension := ""
for key , values := range query . Dimensions {
if len ( values ) > series {
series = len ( values )
multiValuedDimension = key
}
}
2019-11-14 17:59:41 +08:00
2020-03-11 04:14:58 +08:00
for _ , value := range query . Dimensions [ multiValuedDimension ] {
2021-09-08 22:06:43 +08:00
labels := map [ string ] string { multiValuedDimension : value }
2020-03-11 04:14:58 +08:00
for key , values := range query . Dimensions {
if key != multiValuedDimension && len ( values ) > 0 {
2021-09-08 22:06:43 +08:00
labels [ key ] = values [ 0 ]
2020-03-11 04:14:58 +08:00
}
}
2020-01-17 22:27:03 +08:00
2020-11-03 18:24:26 +08:00
timeField := data . NewField ( data . TimeSeriesTimeFieldName , nil , [ ] * time . Time { } )
2021-09-08 22:06:43 +08:00
valueField := data . NewField ( data . TimeSeriesValueFieldName , labels , [ ] * float64 { } )
2020-10-06 19:45:58 +08:00
2023-04-27 17:19:45 +08:00
valueField . SetConfig ( & data . FieldConfig { DisplayNameFromDS : label , Links : createDataLinks ( deepLink ) } )
2020-10-06 19:45:58 +08:00
emptyFrame := data . Frame {
2023-04-27 17:19:45 +08:00
Name : label ,
2020-10-06 19:45:58 +08:00
Fields : [ ] * data . Field {
timeField ,
valueField ,
} ,
RefID : query . RefId ,
2021-09-08 22:06:43 +08:00
Meta : createMeta ( query ) ,
2020-10-06 19:45:58 +08:00
}
frames = append ( frames , & emptyFrame )
2020-03-11 04:14:58 +08:00
}
2021-09-08 22:06:43 +08:00
continue
}
2019-11-14 17:59:41 +08:00
2024-04-05 23:57:56 +08:00
name := label
var labels data . Labels
2024-05-01 00:06:16 +08:00
if query . GetGetMetricDataAPIMode ( ) == models . GMDApiModeSQLExpression {
labels = getLabels ( label , query , true )
} else if features . IsEnabled ( ctx , features . FlagCloudWatchNewLabelParsing ) {
2024-04-05 23:57:56 +08:00
name , labels = parseLabels ( label , query )
} else {
2024-05-01 00:06:16 +08:00
labels = getLabels ( label , query , false )
2024-04-05 23:57:56 +08:00
}
2020-10-06 19:45:58 +08:00
2025-06-26 21:56:50 +08:00
timeField := data . NewField ( data . TimeSeriesTimeFieldName , nil , metric . Timestamps )
valueField := data . NewField ( data . TimeSeriesValueFieldName , labels , metric . Values )
2020-10-06 19:45:58 +08:00
2024-03-19 05:30:59 +08:00
// CloudWatch appends the dimensions to the returned label if the query label is not dynamic, so static labels need to be set
if hasStaticLabel {
name = query . Label
}
2020-10-06 19:45:58 +08:00
2024-03-19 05:30:59 +08:00
valueField . SetConfig ( & data . FieldConfig { DisplayNameFromDS : name , Links : createDataLinks ( deepLink ) } )
2021-09-08 22:06:43 +08:00
frame := data . Frame {
2024-03-19 05:30:59 +08:00
Name : name ,
2021-09-08 22:06:43 +08:00
Fields : [ ] * data . Field {
timeField ,
valueField ,
} ,
RefID : query . RefId ,
Meta : createMeta ( query ) ,
}
2025-04-09 20:20:53 +08:00
frame . Meta . Type = data . FrameTypeTimeSeriesMulti
2021-09-08 22:06:43 +08:00
2022-04-04 21:44:19 +08:00
for code := range aggregatedResponse . ErrorCodes {
if aggregatedResponse . ErrorCodes [ code ] {
frame . AppendNotices ( data . Notice {
Severity : data . NoticeSeverityWarning ,
2022-12-02 17:21:46 +08:00
Text : "cloudwatch GetMetricData error: " + models . ErrorMessages [ code ] ,
2022-04-04 21:44:19 +08:00
} )
}
2019-11-14 17:59:41 +08:00
}
2021-09-08 22:06:43 +08:00
if aggregatedResponse . StatusCode != "Complete" {
frame . AppendNotices ( data . Notice {
Severity : data . NoticeSeverityWarning ,
Text : "cloudwatch GetMetricData error: Too many datapoints requested - your search has been limited. Please try to reduce the time range" ,
} )
}
frames = append ( frames , & frame )
2019-11-14 17:59:41 +08:00
}
2020-10-06 19:45:58 +08:00
2021-09-08 22:06:43 +08:00
return frames , nil
2019-11-14 17:59:41 +08:00
}
2021-09-08 22:06:43 +08:00
func createDataLinks ( link string ) [ ] data . DataLink {
dataLinks := [ ] data . DataLink { }
if link != "" {
dataLinks = append ( dataLinks , data . DataLink {
Title : "View in CloudWatch console" ,
TargetBlank : true ,
URL : link ,
} )
}
return dataLinks
}
2022-10-20 17:21:13 +08:00
func createMeta ( query * models . CloudWatchQuery ) * data . FrameMeta {
2021-09-08 22:06:43 +08:00
return & data . FrameMeta {
ExecutedQueryString : query . UsedExpression ,
2022-07-09 03:39:53 +08:00
Custom : fmt . Sprintf ( ` {
"period" : % d ,
"id" : % s ,
} ` , query . Period , query . Id ) ,
2021-09-08 22:06:43 +08:00
}
}