2019-11-14 10:59:41 +01:00
package cloudwatch
import (
"errors"
2021-07-09 13:43:22 +02:00
"fmt"
2020-01-17 13:22:43 +01:00
"math"
2019-11-14 10:59:41 +01:00
"regexp"
"sort"
"strconv"
2020-01-17 13:22:43 +01:00
"strings"
2019-11-14 10:59:41 +01:00
"time"
2022-03-14 09:44:04 -07:00
"github.com/google/uuid"
2021-03-23 16:32:12 +01:00
"github.com/grafana/grafana-plugin-sdk-go/backend"
2019-11-14 10:59:41 +01:00
"github.com/grafana/grafana/pkg/components/simplejson"
2022-05-03 14:41:51 +02:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2019-11-14 10:59:41 +01:00
)
2022-03-14 09:44:04 -07:00
var validMetricDataID = regexp . MustCompile ( `^[a-z][a-zA-Z0-9_]*$` )
2021-09-08 16:06:43 +02:00
// parseQueries parses the json queries and returns a map of cloudWatchQueries by region. The cloudWatchQuery has a 1 to 1 mapping to a query editor row
func ( e * cloudWatchExecutor ) parseQueries ( queries [] backend . DataQuery , startTime time . Time , endTime time . Time ) ( map [ string ][] * cloudWatchQuery , error ) {
requestQueries := make ( map [ string ][] * cloudWatchQuery )
2022-05-03 14:41:51 +02:00
2022-05-08 09:27:03 +02:00
migratedQueries , err := migrateLegacyQuery ( queries , e . features . IsEnabled ( featuremgmt . FlagCloudWatchDynamicLabels ))
2021-09-08 16:06:43 +02:00
if err != nil {
return nil , err
}
for _ , query := range migratedQueries {
2021-03-23 16:32:12 +01:00
model , err := simplejson . NewJson ( query . JSON )
if err != nil {
return nil , & queryError { err : err , RefID : query . RefID }
}
queryType := model . Get ( "type" ). MustString ()
2019-11-14 10:59:41 +01:00
if queryType != "timeSeriesQuery" && queryType != "" {
continue
}
2021-03-08 07:02:49 +01:00
refID := query . RefID
2021-03-23 16:32:12 +01:00
query , err := parseRequestQuery ( model , refID , startTime , endTime )
2019-11-14 10:59:41 +01:00
if err != nil {
2020-05-18 12:25:58 +02:00
return nil , & queryError { err : err , RefID : refID }
2019-11-14 10:59:41 +01:00
}
2020-05-18 12:25:58 +02:00
2019-11-14 10:59:41 +01:00
if _ , exist := requestQueries [ query . Region ]; ! exist {
2021-09-08 16:06:43 +02:00
requestQueries [ query . Region ] = [] * cloudWatchQuery {}
2019-11-14 10:59:41 +01:00
}
requestQueries [ query . Region ] = append ( requestQueries [ query . Region ], query )
}
return requestQueries , nil
}
2022-05-03 14:41:51 +02:00
// migrateLegacyQuery is also done in the frontend, so this should only ever be needed for alerting queries
2022-05-08 09:27:03 +02:00
func migrateLegacyQuery ( queries [] backend . DataQuery , dynamicLabelsEnabled bool ) ([] * backend . DataQuery , error ) {
2021-09-08 16:06:43 +02:00
migratedQueries := [] * backend . DataQuery {}
for _ , q := range queries {
query := q
2022-05-03 14:41:51 +02:00
queryJson , err := simplejson . NewJson ( query . JSON )
2021-09-08 16:06:43 +02:00
if err != nil {
return nil , err
}
2022-05-03 14:41:51 +02:00
if err := migrateStatisticsToStatistic ( queryJson ); err != nil {
return nil , err
}
_ , labelExists := queryJson . CheckGet ( "label" )
if ! labelExists && dynamicLabelsEnabled {
migrateAliasToDynamicLabel ( queryJson )
}
query . JSON , err = queryJson . MarshalJSON ()
2021-09-08 16:06:43 +02:00
if err != nil {
2022-05-03 14:41:51 +02:00
return nil , err
2021-09-08 16:06:43 +02:00
}
migratedQueries = append ( migratedQueries , & query )
}
return migratedQueries , nil
}
2022-05-03 14:41:51 +02:00
// migrateStatisticsToStatistic migrates queries that has a `statistics` field to use the `statistic` field instead.
// In case the query used more than one stat, the first stat in the slice will be used in the statistic field
// Read more here https://github.com/grafana/grafana/issues/30629
func migrateStatisticsToStatistic ( queryJson * simplejson . Json ) error {
_ , err := queryJson . Get ( "statistic" ). String ()
// If there's not a statistic property in the json, we know it's the legacy format and then it has to be migrated
if err != nil {
stats , err := queryJson . Get ( "statistics" ). StringArray ()
if err != nil {
return fmt . Errorf ( "query must have either statistic or statistics field" )
}
queryJson . Del ( "statistics" )
queryJson . Set ( "statistic" , stats [ 0 ])
}
return nil
}
var aliasPatterns = map [ string ] string {
"metric" : `${PROP('MetricName')}` ,
"namespace" : `${PROP('Namespace')}` ,
"period" : `${PROP('Period')}` ,
"region" : `${PROP('Region')}` ,
"stat" : `${PROP('Stat')}` ,
"label" : `${LABEL}` ,
}
var legacyAliasRegexp = regexp . MustCompile ( ` {{ \ s * ( . +? ) \ s * }} ` )
func migrateAliasToDynamicLabel ( queryJson * simplejson . Json ) {
fullAliasField := queryJson . Get ( "alias" ). MustString ()
if fullAliasField != "" {
matches := legacyAliasRegexp . FindAllStringSubmatch ( fullAliasField , - 1 )
for _ , groups := range matches {
fullMatch := groups [ 0 ]
subgroup := groups [ 1 ]
if dynamicLabel , ok := aliasPatterns [ subgroup ]; ok {
fullAliasField = strings . ReplaceAll ( fullAliasField , fullMatch , dynamicLabel )
} else {
fullAliasField = strings . ReplaceAll ( fullAliasField , fullMatch , fmt . Sprintf ( `${PROP('Dim.%s')}` , subgroup ))
}
}
}
queryJson . Set ( "label" , fullAliasField )
}
2021-09-08 16:06:43 +02:00
func parseRequestQuery ( model * simplejson . Json , refId string , startTime time . Time , endTime time . Time ) ( * cloudWatchQuery , error ) {
2020-10-06 13:45:58 +02:00
plog . Debug ( "Parsing request query" , "query" , model )
2020-05-18 12:25:58 +02:00
reNumber := regexp . MustCompile ( `^\d+$` )
2019-11-14 10:59:41 +01:00
region , err := model . Get ( "region" ). String ()
if err != nil {
return nil , err
}
namespace , err := model . Get ( "namespace" ). String ()
if err != nil {
2021-07-09 13:43:22 +02:00
return nil , fmt . Errorf ( "failed to get namespace: %v" , err )
2019-11-14 10:59:41 +01:00
}
metricName , err := model . Get ( "metricName" ). String ()
if err != nil {
2021-07-09 13:43:22 +02:00
return nil , fmt . Errorf ( "failed to get metricName: %v" , err )
2019-11-14 10:59:41 +01:00
}
dimensions , err := parseDimensions ( model )
if err != nil {
2021-07-09 13:43:22 +02:00
return nil , fmt . Errorf ( "failed to parse dimensions: %v" , err )
2019-11-14 10:59:41 +01:00
}
2021-09-08 16:06:43 +02:00
statistic , err := model . Get ( "statistic" ). String ()
if err != nil {
return nil , fmt . Errorf ( "failed to parse statistic: %v" , err )
}
2019-11-14 10:59:41 +01:00
p := model . Get ( "period" ). MustString ( "" )
var period int
2020-01-17 13:22:43 +01:00
if strings . ToLower ( p ) == "auto" || p == "" {
deltaInSeconds := endTime . Sub ( startTime ). Seconds ()
2021-09-01 07:44:47 +00:00
periods := getRetainedPeriods ( time . Since ( startTime ))
2020-01-22 13:43:36 +01:00
datapoints := int ( math . Ceil ( deltaInSeconds / 2000 ))
period = periods [ len ( periods ) - 1 ]
for _ , value := range periods {
if datapoints <= value {
period = value
break
}
}
2019-11-14 10:59:41 +01:00
} else {
2020-05-18 12:25:58 +02:00
if reNumber . Match ([] byte ( p )) {
2020-01-17 13:22:43 +01:00
period , err = strconv . Atoi ( p )
if err != nil {
2021-07-09 13:43:22 +02:00
return nil , fmt . Errorf ( "failed to parse period as integer: %v" , err )
2020-01-17 13:22:43 +01:00
}
} else {
d , err := time . ParseDuration ( p )
if err != nil {
2021-07-09 13:43:22 +02:00
return nil , fmt . Errorf ( "failed to parse period as duration: %v" , err )
2020-01-17 13:22:43 +01:00
}
period = int ( d . Seconds ())
2019-11-14 10:59:41 +01:00
}
}
id := model . Get ( "id" ). MustString ( "" )
2021-09-08 16:06:43 +02:00
if id == "" {
// Why not just use refId if id is not specified in the frontend? When specifying an id in the editor,
// and alphabetical must be used. The id must be unique, so if an id like for example a, b or c would be used,
// it would likely collide with some ref id. That's why the `query` prefix is used.
2022-03-14 09:44:04 -07:00
suffix := refId
if ! validMetricDataID . MatchString ( suffix ) {
uuid := uuid . NewString ()
suffix = strings . Replace ( uuid , "-" , "" , - 1 )
}
id = fmt . Sprintf ( "query%s" , suffix )
2021-09-08 16:06:43 +02:00
}
2019-11-14 10:59:41 +01:00
expression := model . Get ( "expression" ). MustString ( "" )
2021-11-30 10:53:31 +01:00
sqlExpression := model . Get ( "sqlExpression" ). MustString ( "" )
2019-11-14 10:59:41 +01:00
alias := model . Get ( "alias" ). MustString ()
2022-05-05 13:59:23 +02:00
label := model . Get ( "label" ). MustString ()
2019-11-14 10:59:41 +01:00
returnData := ! model . Get ( "hide" ). MustBool ( false )
queryType := model . Get ( "type" ). MustString ()
2022-05-16 12:15:54 +02:00
timezoneUTCOffset := model . Get ( "timezoneUTCOffset" ). MustString ( "" )
2019-11-14 10:59:41 +01:00
if queryType == "" {
// If no type is provided we assume we are called by alerting service, which requires to return data!
// Note, this is sort of a hack, but the official Grafana interfaces do not carry the information
// who (which service) called the TsdbQueryEndpoint.Query(...) function.
returnData = true
}
matchExact := model . Get ( "matchExact" ). MustBool ( true )
2021-11-30 10:53:31 +01:00
metricQueryType := metricQueryType ( model . Get ( "metricQueryType" ). MustInt ( 0 ))
var metricEditorModeValue metricEditorMode
memv , err := model . Get ( "metricEditorMode" ). Int ()
if err != nil && len ( expression ) > 0 {
// this should only ever happen if this is an alerting query that has not yet been migrated in the frontend
metricEditorModeValue = MetricEditorModeRaw
} else {
metricEditorModeValue = metricEditorMode ( memv )
}
2019-11-14 10:59:41 +01:00
2021-09-08 16:06:43 +02:00
return & cloudWatchQuery {
2022-05-16 12:15:54 +02:00
RefId : refId ,
Region : region ,
Id : id ,
Namespace : namespace ,
MetricName : metricName ,
Statistic : statistic ,
Expression : expression ,
ReturnData : returnData ,
Dimensions : dimensions ,
Period : period ,
Alias : alias ,
Label : label ,
MatchExact : matchExact ,
UsedExpression : "" ,
MetricQueryType : metricQueryType ,
MetricEditorMode : metricEditorModeValue ,
SqlExpression : sqlExpression ,
TimezoneUTCOffset : timezoneUTCOffset ,
2019-11-14 10:59:41 +01:00
}, nil
}
2021-09-01 07:44:47 +00:00
func getRetainedPeriods ( timeSince time . Duration ) [] int {
// See https://aws.amazon.com/about-aws/whats-new/2016/11/cloudwatch-extends-metrics-retention-and-new-user-interface/
if timeSince > time . Duration ( 455 ) * 24 * time . Hour {
return [] int { 21600 , 86400 }
} else if timeSince > time . Duration ( 63 ) * 24 * time . Hour {
return [] int { 3600 , 21600 , 86400 }
} else if timeSince > time . Duration ( 15 ) * 24 * time . Hour {
return [] int { 300 , 900 , 3600 , 21600 , 86400 }
} else {
return [] int { 60 , 300 , 900 , 3600 , 21600 , 86400 }
}
}
2019-11-14 10:59:41 +01:00
func parseDimensions ( model * simplejson . Json ) ( map [ string ][] string , error ) {
parsedDimensions := make ( map [ string ][] string )
for k , v := range model . Get ( "dimensions" ). MustMap () {
// This is for backwards compatibility. Before 6.5 dimensions values were stored as strings and not arrays
if value , ok := v .( string ); ok {
parsedDimensions [ k ] = [] string { value }
} else if values , ok := v .([] interface {}); ok {
for _ , value := range values {
parsedDimensions [ k ] = append ( parsedDimensions [ k ], value .( string ))
}
} else {
2021-07-09 13:43:22 +02:00
return nil , errors . New ( "unknown type as dimension value" )
2019-11-14 10:59:41 +01:00
}
}
sortedDimensions := sortDimensions ( parsedDimensions )
return sortedDimensions , nil
}
func sortDimensions ( dimensions map [ string ][] string ) map [ string ][] string {
sortedDimensions := make ( map [ string ][] string )
var keys [] string
for k := range dimensions {
keys = append ( keys , k )
}
sort . Strings ( keys )
for _ , k := range keys {
sortedDimensions [ k ] = dimensions [ k ]
}
return sortedDimensions
}