2019-11-14 17:59:41 +08:00
package cloudwatch
import (
"errors"
2021-07-09 19:43:22 +08:00
"fmt"
2020-01-17 20:22:43 +08:00
"math"
2019-11-14 17:59:41 +08:00
"regexp"
"sort"
"strconv"
2020-01-17 20:22:43 +08:00
"strings"
2019-11-14 17:59:41 +08:00
"time"
2021-03-23 23:32:12 +08:00
"github.com/grafana/grafana-plugin-sdk-go/backend"
2019-11-14 17:59:41 +08:00
"github.com/grafana/grafana/pkg/components/simplejson"
)
2021-09-08 22:06:43 +08: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 )
migratedQueries , err := migrateLegacyQuery ( queries , startTime , endTime )
if err != nil {
return nil , err
}
for _ , query := range migratedQueries {
2021-03-23 23:32:12 +08: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 17:59:41 +08:00
if queryType != "timeSeriesQuery" && queryType != "" {
continue
}
2021-03-08 14:02:49 +08:00
refID := query . RefID
2021-03-23 23:32:12 +08:00
query , err := parseRequestQuery ( model , refID , startTime , endTime )
2019-11-14 17:59:41 +08:00
if err != nil {
2020-05-18 18:25:58 +08:00
return nil , & queryError { err : err , RefID : refID }
2019-11-14 17:59:41 +08:00
}
2020-05-18 18:25:58 +08:00
2019-11-14 17:59:41 +08:00
if _ , exist := requestQueries [ query . Region ] ; ! exist {
2021-09-08 22:06:43 +08:00
requestQueries [ query . Region ] = [ ] * cloudWatchQuery { }
2019-11-14 17:59:41 +08:00
}
requestQueries [ query . Region ] = append ( requestQueries [ query . Region ] , query )
}
return requestQueries , nil
}
2021-09-08 22:06:43 +08:00
// migrateLegacyQuery migrates queries that has a `statistics` field to use the `statistic` field instead.
// This migration is also done in the frontend, so this should only ever be needed for alerting queries
// 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 migrateLegacyQuery ( queries [ ] backend . DataQuery , startTime time . Time , endTime time . Time ) ( [ ] * backend . DataQuery , error ) {
migratedQueries := [ ] * backend . DataQuery { }
for _ , q := range queries {
query := q
model , err := simplejson . NewJson ( query . JSON )
if err != nil {
return nil , err
}
_ , err = model . 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 := model . Get ( "statistics" ) . StringArray ( )
if err != nil {
return nil , fmt . Errorf ( "query must have either statistic or statistics field" )
}
model . Del ( "statistics" )
model . Set ( "statistic" , stats [ 0 ] )
query . JSON , err = model . MarshalJSON ( )
if err != nil {
return nil , err
}
}
migratedQueries = append ( migratedQueries , & query )
}
return migratedQueries , nil
}
func parseRequestQuery ( model * simplejson . Json , refId string , startTime time . Time , endTime time . Time ) ( * cloudWatchQuery , error ) {
2020-10-06 19:45:58 +08:00
plog . Debug ( "Parsing request query" , "query" , model )
2020-05-18 18:25:58 +08:00
reNumber := regexp . MustCompile ( ` ^\d+$ ` )
2019-11-14 17:59:41 +08: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 19:43:22 +08:00
return nil , fmt . Errorf ( "failed to get namespace: %v" , err )
2019-11-14 17:59:41 +08:00
}
metricName , err := model . Get ( "metricName" ) . String ( )
if err != nil {
2021-07-09 19:43:22 +08:00
return nil , fmt . Errorf ( "failed to get metricName: %v" , err )
2019-11-14 17:59:41 +08:00
}
dimensions , err := parseDimensions ( model )
if err != nil {
2021-07-09 19:43:22 +08:00
return nil , fmt . Errorf ( "failed to parse dimensions: %v" , err )
2019-11-14 17:59:41 +08:00
}
2021-09-08 22:06:43 +08:00
statistic , err := model . Get ( "statistic" ) . String ( )
if err != nil {
return nil , fmt . Errorf ( "failed to parse statistic: %v" , err )
}
2019-11-14 17:59:41 +08:00
p := model . Get ( "period" ) . MustString ( "" )
var period int
2020-01-17 20:22:43 +08:00
if strings . ToLower ( p ) == "auto" || p == "" {
deltaInSeconds := endTime . Sub ( startTime ) . Seconds ( )
2021-09-01 15:44:47 +08:00
periods := getRetainedPeriods ( time . Since ( startTime ) )
2020-01-22 20:43:36 +08: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 17:59:41 +08:00
} else {
2020-05-18 18:25:58 +08:00
if reNumber . Match ( [ ] byte ( p ) ) {
2020-01-17 20:22:43 +08:00
period , err = strconv . Atoi ( p )
if err != nil {
2021-07-09 19:43:22 +08:00
return nil , fmt . Errorf ( "failed to parse period as integer: %v" , err )
2020-01-17 20:22:43 +08:00
}
} else {
d , err := time . ParseDuration ( p )
if err != nil {
2021-07-09 19:43:22 +08:00
return nil , fmt . Errorf ( "failed to parse period as duration: %v" , err )
2020-01-17 20:22:43 +08:00
}
period = int ( d . Seconds ( ) )
2019-11-14 17:59:41 +08:00
}
}
id := model . Get ( "id" ) . MustString ( "" )
2021-09-08 22:06:43 +08: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.
id = fmt . Sprintf ( "query%s" , refId )
}
2019-11-14 17:59:41 +08:00
expression := model . Get ( "expression" ) . MustString ( "" )
alias := model . Get ( "alias" ) . MustString ( )
returnData := ! model . Get ( "hide" ) . MustBool ( false )
queryType := model . Get ( "type" ) . MustString ( )
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-09-08 22:06:43 +08:00
return & cloudWatchQuery {
RefId : refId ,
Region : region ,
Id : id ,
Namespace : namespace ,
MetricName : metricName ,
Statistic : statistic ,
Expression : expression ,
ReturnData : returnData ,
Dimensions : dimensions ,
Period : period ,
Alias : alias ,
MatchExact : matchExact ,
UsedExpression : "" ,
2019-11-14 17:59:41 +08:00
} , nil
}
2021-09-01 15:44:47 +08: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 17:59:41 +08: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 19:43:22 +08:00
return nil , errors . New ( "unknown type as dimension value" )
2019-11-14 17:59:41 +08: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
}