2019-11-14 03:59:41 -06:00
package cloudwatch
import (
2022-07-08 14:39:53 -05:00
"encoding/json"
2019-11-14 03:59:41 -06:00
"errors"
2021-07-09 06:43:22 -05:00
"fmt"
2020-01-17 06:22:43 -06:00
"math"
2019-11-14 03:59:41 -06:00
"regexp"
"sort"
"strconv"
2020-01-17 06:22:43 -06:00
"strings"
2019-11-14 03:59:41 -06:00
"time"
2022-03-14 11:44:04 -05:00
"github.com/google/uuid"
2021-03-23 10:32:12 -05:00
"github.com/grafana/grafana-plugin-sdk-go/backend"
2022-05-03 07:41:51 -05:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2019-11-14 03:59:41 -06:00
)
2022-03-14 11:44:04 -05:00
var validMetricDataID = regexp . MustCompile ( ` ^[a-z][a-zA-Z0-9_]*$ ` )
2022-07-08 14:39:53 -05:00
type QueryJson struct {
Datasource map [ string ] string ` json:"datasource,omitempty" `
Dimensions map [ string ] interface { } ` json:"dimensions,omitempty" `
Expression string ` json:"expression,omitempty" `
Id string ` json:"id,omitempty" `
Label * string ` json:"label,omitempty" `
MatchExact * bool ` json:"matchExact,omitempty" `
MaxDataPoints int ` json:"maxDataPoints,omitempty" `
MetricEditorMode * int ` json:"metricEditorMode,omitempty" `
MetricName string ` json:"metricName,omitempty" `
MetricQueryType metricQueryType ` json:"metricQueryType,omitempty" `
Namespace string ` json:"namespace,omitempty" `
Period string ` json:"period,omitempty" `
RefId string ` json:"refId,omitempty" `
Region string ` json:"region,omitempty" `
SqlExpression string ` json:"sqlExpression,omitempty" `
Statistic * string ` json:"statistic,omitempty" `
Statistics [ ] * string ` json:"statistics,omitempty" `
TimezoneUTCOffset string ` json:"timezoneUTCOffset,omitempty" `
QueryType string ` json:"queryType,omitempty" `
Hide * bool ` json:"hide,omitempty" `
Alias * string ` json:"alias,omitempty" `
}
2021-09-08 09:06:43 -05: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-08 02:27:03 -05:00
migratedQueries , err := migrateLegacyQuery ( queries , e . features . IsEnabled ( featuremgmt . FlagCloudWatchDynamicLabels ) )
2021-09-08 09:06:43 -05:00
if err != nil {
return nil , err
}
for _ , query := range migratedQueries {
2022-07-08 14:39:53 -05:00
var model QueryJson
err := json . Unmarshal ( query . JSON , & model )
2021-03-23 10:32:12 -05:00
if err != nil {
return nil , & queryError { err : err , RefID : query . RefID }
}
2022-07-08 14:39:53 -05:00
queryType := model . QueryType
2019-11-14 03:59:41 -06:00
if queryType != "timeSeriesQuery" && queryType != "" {
continue
}
2022-07-08 14:39:53 -05:00
if model . MatchExact == nil {
trueBooleanValue := true
model . MatchExact = & trueBooleanValue
}
2021-03-08 00:02:49 -06:00
refID := query . RefID
2021-03-23 10:32:12 -05:00
query , err := parseRequestQuery ( model , refID , startTime , endTime )
2019-11-14 03:59:41 -06:00
if err != nil {
2020-05-18 05:25:58 -05:00
return nil , & queryError { err : err , RefID : refID }
2019-11-14 03:59:41 -06:00
}
2020-05-18 05:25:58 -05:00
2019-11-14 03:59:41 -06:00
if _ , exist := requestQueries [ query . Region ] ; ! exist {
2021-09-08 09:06:43 -05:00
requestQueries [ query . Region ] = [ ] * cloudWatchQuery { }
2019-11-14 03:59:41 -06:00
}
requestQueries [ query . Region ] = append ( requestQueries [ query . Region ] , query )
}
return requestQueries , nil
}
2022-05-03 07:41:51 -05:00
// migrateLegacyQuery is also done in the frontend, so this should only ever be needed for alerting queries
2022-05-08 02:27:03 -05:00
func migrateLegacyQuery ( queries [ ] backend . DataQuery , dynamicLabelsEnabled bool ) ( [ ] * backend . DataQuery , error ) {
2021-09-08 09:06:43 -05:00
migratedQueries := [ ] * backend . DataQuery { }
for _ , q := range queries {
query := q
2022-07-08 14:39:53 -05:00
var queryJson * QueryJson
err := json . Unmarshal ( query . JSON , & queryJson )
2021-09-08 09:06:43 -05:00
if err != nil {
return nil , err
}
2022-05-03 07:41:51 -05:00
if err := migrateStatisticsToStatistic ( queryJson ) ; err != nil {
return nil , err
}
2022-07-08 14:39:53 -05:00
if queryJson . Label == nil && dynamicLabelsEnabled {
2022-05-03 07:41:51 -05:00
migrateAliasToDynamicLabel ( queryJson )
}
2022-07-08 14:39:53 -05:00
query . JSON , err = json . Marshal ( queryJson )
2021-09-08 09:06:43 -05:00
if err != nil {
2022-05-03 07:41:51 -05:00
return nil , err
2021-09-08 09:06:43 -05:00
}
migratedQueries = append ( migratedQueries , & query )
}
return migratedQueries , nil
}
2022-05-03 07:41:51 -05: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
2022-07-08 14:39:53 -05:00
func migrateStatisticsToStatistic ( queryJson * QueryJson ) error {
2022-05-03 07:41:51 -05:00
// If there's not a statistic property in the json, we know it's the legacy format and then it has to be migrated
2022-07-08 14:39:53 -05:00
if queryJson . Statistic == nil {
if queryJson . Statistics == nil {
2022-05-03 07:41:51 -05:00
return fmt . Errorf ( "query must have either statistic or statistics field" )
}
2022-07-08 14:39:53 -05:00
queryJson . Statistic = queryJson . Statistics [ 0 ]
queryJson . Statistics = nil
2022-05-03 07:41:51 -05:00
}
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 * }} ` )
2022-07-08 14:39:53 -05:00
func migrateAliasToDynamicLabel ( queryJson * QueryJson ) {
fullAliasField := ""
if queryJson . Alias != nil && * queryJson . Alias != "" {
matches := legacyAliasRegexp . FindAllStringSubmatch ( * queryJson . Alias , - 1 )
fullAliasField = * queryJson . Alias
2022-05-03 07:41:51 -05:00
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 ) )
}
}
}
2022-07-08 14:39:53 -05:00
queryJson . Label = & fullAliasField
2022-05-03 07:41:51 -05:00
}
2022-07-08 14:39:53 -05:00
func parseRequestQuery ( model QueryJson , refId string , startTime time . Time , endTime time . Time ) ( * cloudWatchQuery , error ) {
2020-10-06 06:45:58 -05:00
plog . Debug ( "Parsing request query" , "query" , model )
2022-07-08 14:39:53 -05:00
cloudWatchQuery := cloudWatchQuery {
Alias : "" ,
Label : "" ,
MatchExact : true ,
Statistic : "" ,
ReturnData : false ,
UsedExpression : "" ,
RefId : refId ,
Id : model . Id ,
Region : model . Region ,
Namespace : model . Namespace ,
MetricName : model . MetricName ,
MetricQueryType : model . MetricQueryType ,
SqlExpression : model . SqlExpression ,
TimezoneUTCOffset : model . TimezoneUTCOffset ,
Expression : model . Expression ,
2019-11-14 03:59:41 -06:00
}
2022-07-08 14:39:53 -05:00
reNumber := regexp . MustCompile ( ` ^\d+$ ` )
dimensions , err := parseDimensions ( model . Dimensions )
2019-11-14 03:59:41 -06:00
if err != nil {
2021-07-09 06:43:22 -05:00
return nil , fmt . Errorf ( "failed to parse dimensions: %v" , err )
2019-11-14 03:59:41 -06:00
}
2022-07-08 14:39:53 -05:00
cloudWatchQuery . Dimensions = dimensions
2021-09-08 09:06:43 -05:00
2022-07-08 14:39:53 -05:00
p := model . Period
2019-11-14 03:59:41 -06:00
var period int
2020-01-17 06:22:43 -06:00
if strings . ToLower ( p ) == "auto" || p == "" {
deltaInSeconds := endTime . Sub ( startTime ) . Seconds ( )
2021-09-01 02:44:47 -05:00
periods := getRetainedPeriods ( time . Since ( startTime ) )
2020-01-22 06:43:36 -06: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 03:59:41 -06:00
} else {
2020-05-18 05:25:58 -05:00
if reNumber . Match ( [ ] byte ( p ) ) {
2020-01-17 06:22:43 -06:00
period , err = strconv . Atoi ( p )
if err != nil {
2021-07-09 06:43:22 -05:00
return nil , fmt . Errorf ( "failed to parse period as integer: %v" , err )
2020-01-17 06:22:43 -06:00
}
} else {
d , err := time . ParseDuration ( p )
if err != nil {
2021-07-09 06:43:22 -05:00
return nil , fmt . Errorf ( "failed to parse period as duration: %v" , err )
2020-01-17 06:22:43 -06:00
}
period = int ( d . Seconds ( ) )
2019-11-14 03:59:41 -06:00
}
}
2022-07-08 14:39:53 -05:00
cloudWatchQuery . Period = period
2019-11-14 03:59:41 -06:00
2022-07-08 14:39:53 -05:00
if model . Id == "" {
2021-09-08 09:06:43 -05:00
// 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 11:44:04 -05:00
suffix := refId
if ! validMetricDataID . MatchString ( suffix ) {
uuid := uuid . NewString ( )
suffix = strings . Replace ( uuid , "-" , "" , - 1 )
}
2022-07-08 14:39:53 -05:00
cloudWatchQuery . Id = fmt . Sprintf ( "query%s" , suffix )
}
if model . Hide != nil {
cloudWatchQuery . ReturnData = ! * model . Hide
2021-09-08 09:06:43 -05:00
}
2022-07-08 14:39:53 -05:00
if model . QueryType == "" {
2019-11-14 03:59:41 -06:00
// 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.
2022-07-08 14:39:53 -05:00
cloudWatchQuery . ReturnData = true
2019-11-14 03:59:41 -06:00
}
2022-07-08 14:39:53 -05:00
if model . MetricEditorMode == nil && len ( model . Expression ) > 0 {
2021-11-30 03:53:31 -06:00
// this should only ever happen if this is an alerting query that has not yet been migrated in the frontend
2022-07-08 14:39:53 -05:00
cloudWatchQuery . MetricEditorMode = MetricEditorModeRaw
2021-11-30 03:53:31 -06:00
} else {
2022-07-08 14:39:53 -05:00
if model . MetricEditorMode != nil {
cloudWatchQuery . MetricEditorMode = metricEditorMode ( * model . MetricEditorMode )
} else {
cloudWatchQuery . MetricEditorMode = metricEditorMode ( 0 )
}
2021-11-30 03:53:31 -06:00
}
2019-11-14 03:59:41 -06:00
2022-07-08 14:39:53 -05:00
if model . Statistic != nil {
cloudWatchQuery . Statistic = * model . Statistic
}
if model . MatchExact != nil {
cloudWatchQuery . MatchExact = * model . MatchExact
}
if model . Alias != nil {
cloudWatchQuery . Alias = * model . Alias
}
if model . Label != nil {
cloudWatchQuery . Label = * model . Label
}
return & cloudWatchQuery , nil
2019-11-14 03:59:41 -06:00
}
2021-09-01 02:44:47 -05: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 }
}
}
2022-07-08 14:39:53 -05:00
func parseDimensions ( dimensions map [ string ] interface { } ) ( map [ string ] [ ] string , error ) {
2019-11-14 03:59:41 -06:00
parsedDimensions := make ( map [ string ] [ ] string )
2022-07-08 14:39:53 -05:00
for k , v := range dimensions {
2019-11-14 03:59:41 -06:00
// 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 06:43:22 -05:00
return nil , errors . New ( "unknown type as dimension value" )
2019-11-14 03:59:41 -06: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
}