mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
CloudWatch: Remove simplejson in favor of 'encoding/json' (#51062)
This commit is contained in:
parent
eb6d6d0d2b
commit
05cdef5004
@ -3,13 +3,13 @@ package cloudwatch
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type annotationEvent struct {
|
type annotationEvent struct {
|
||||||
@ -19,29 +19,37 @@ type annotationEvent struct {
|
|||||||
Text string
|
Text string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *cloudWatchExecutor) executeAnnotationQuery(pluginCtx backend.PluginContext, model *simplejson.Json, query backend.DataQuery) (*backend.QueryDataResponse, error) {
|
func (e *cloudWatchExecutor) executeAnnotationQuery(pluginCtx backend.PluginContext, model DataQueryJson, query backend.DataQuery) (*backend.QueryDataResponse, error) {
|
||||||
result := backend.NewQueryDataResponse()
|
result := backend.NewQueryDataResponse()
|
||||||
|
statistic := ""
|
||||||
|
|
||||||
usePrefixMatch := model.Get("prefixMatching").MustBool(false)
|
if model.Statistic != nil {
|
||||||
region := model.Get("region").MustString("")
|
statistic = *model.Statistic
|
||||||
namespace := model.Get("namespace").MustString("")
|
}
|
||||||
metricName := model.Get("metricName").MustString("")
|
|
||||||
dimensions := model.Get("dimensions").MustMap()
|
var period int64
|
||||||
statistic := model.Get("statistic").MustString()
|
if model.Period != "" {
|
||||||
period := int64(model.Get("period").MustInt(0))
|
p, err := strconv.ParseInt(model.Period, 10, 64)
|
||||||
if period == 0 && !usePrefixMatch {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
period = p
|
||||||
|
}
|
||||||
|
|
||||||
|
if period == 0 && !model.PrefixMatching {
|
||||||
period = 300
|
period = 300
|
||||||
}
|
}
|
||||||
actionPrefix := model.Get("actionPrefix").MustString("")
|
|
||||||
alarmNamePrefix := model.Get("alarmNamePrefix").MustString("")
|
|
||||||
|
|
||||||
cli, err := e.getCWClient(pluginCtx, region)
|
actionPrefix := model.ActionPrefix
|
||||||
|
alarmNamePrefix := model.AlarmNamePrefix
|
||||||
|
|
||||||
|
cli, err := e.getCWClient(pluginCtx, model.Region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var alarmNames []*string
|
var alarmNames []*string
|
||||||
if usePrefixMatch {
|
if model.PrefixMatching {
|
||||||
params := &cloudwatch.DescribeAlarmsInput{
|
params := &cloudwatch.DescribeAlarmsInput{
|
||||||
MaxRecords: aws.Int64(100),
|
MaxRecords: aws.Int64(100),
|
||||||
ActionPrefix: aws.String(actionPrefix),
|
ActionPrefix: aws.String(actionPrefix),
|
||||||
@ -51,14 +59,14 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(pluginCtx backend.PluginCont
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%v: %w", "failed to call cloudwatch:DescribeAlarms", err)
|
return nil, fmt.Errorf("%v: %w", "failed to call cloudwatch:DescribeAlarms", err)
|
||||||
}
|
}
|
||||||
alarmNames = filterAlarms(resp, namespace, metricName, dimensions, statistic, period)
|
alarmNames = filterAlarms(resp, model.Namespace, model.MetricName, model.Dimensions, statistic, period)
|
||||||
} else {
|
} else {
|
||||||
if region == "" || namespace == "" || metricName == "" || statistic == "" {
|
if model.Region == "" || model.Namespace == "" || model.MetricName == "" || statistic == "" {
|
||||||
return result, errors.New("invalid annotations query")
|
return result, errors.New("invalid annotations query")
|
||||||
}
|
}
|
||||||
|
|
||||||
var qd []*cloudwatch.Dimension
|
var qd []*cloudwatch.Dimension
|
||||||
for k, v := range dimensions {
|
for k, v := range model.Dimensions {
|
||||||
if vv, ok := v.([]interface{}); ok {
|
if vv, ok := v.([]interface{}); ok {
|
||||||
for _, vvv := range vv {
|
for _, vvv := range vv {
|
||||||
if vvvv, ok := vvv.(string); ok {
|
if vvvv, ok := vvv.(string); ok {
|
||||||
@ -71,8 +79,8 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(pluginCtx backend.PluginCont
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
params := &cloudwatch.DescribeAlarmsForMetricInput{
|
params := &cloudwatch.DescribeAlarmsForMetricInput{
|
||||||
Namespace: aws.String(namespace),
|
Namespace: aws.String(model.Namespace),
|
||||||
MetricName: aws.String(metricName),
|
MetricName: aws.String(model.MetricName),
|
||||||
Dimensions: qd,
|
Dimensions: qd,
|
||||||
Statistic: aws.String(statistic),
|
Statistic: aws.String(statistic),
|
||||||
Period: aws.Int64(period),
|
Period: aws.Int64(period),
|
||||||
|
@ -25,7 +25,6 @@ import (
|
|||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
@ -49,6 +48,20 @@ type datasourceInfo struct {
|
|||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DataQueryJson struct {
|
||||||
|
QueryType string `json:"type,omitempty"`
|
||||||
|
QueryMode string
|
||||||
|
PrefixMatching bool
|
||||||
|
Region string
|
||||||
|
Namespace string
|
||||||
|
MetricName string
|
||||||
|
Dimensions map[string]interface{}
|
||||||
|
Statistic *string
|
||||||
|
Period string
|
||||||
|
ActionPrefix string
|
||||||
|
AlarmNamePrefix string
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
cloudWatchTSFormat = "2006-01-02 15:04:05.000"
|
cloudWatchTSFormat = "2006-01-02 15:04:05.000"
|
||||||
defaultRegion = "default"
|
defaultRegion = "default"
|
||||||
@ -59,10 +72,12 @@ const (
|
|||||||
|
|
||||||
alertMaxAttempts = 8
|
alertMaxAttempts = 8
|
||||||
alertPollPeriod = 1000 * time.Millisecond
|
alertPollPeriod = 1000 * time.Millisecond
|
||||||
|
logsQueryMode = "Logs"
|
||||||
)
|
)
|
||||||
|
|
||||||
var plog = log.New("tsdb.cloudwatch")
|
var plog = log.New("tsdb.cloudwatch")
|
||||||
var aliasFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
|
var aliasFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
|
||||||
|
var baseLimit = int64(1)
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg, httpClientProvider httpclient.Provider, features featuremgmt.FeatureToggles) *CloudWatchService {
|
func ProvideService(cfg *setting.Cfg, httpClientProvider httpclient.Provider, features featuremgmt.FeatureToggles) *CloudWatchService {
|
||||||
plog.Debug("initing")
|
plog.Debug("initing")
|
||||||
@ -188,7 +203,12 @@ func (e *cloudWatchExecutor) checkHealthLogs(ctx context.Context, pluginCtx back
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = e.handleDescribeLogGroups(ctx, logsClient, simplejson.NewFromAny(map[string]interface{}{"limit": "1"}))
|
|
||||||
|
parameters := LogQueryJson{
|
||||||
|
Limit: &baseLimit,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = e.handleDescribeLogGroups(ctx, logsClient, parameters)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,16 +302,16 @@ func (e *cloudWatchExecutor) getRGTAClient(pluginCtx backend.PluginContext, regi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *cloudWatchExecutor) alertQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
func (e *cloudWatchExecutor) alertQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||||
queryContext backend.DataQuery, model *simplejson.Json) (*cloudwatchlogs.GetQueryResultsOutput, error) {
|
queryContext backend.DataQuery, model LogQueryJson) (*cloudwatchlogs.GetQueryResultsOutput, error) {
|
||||||
startQueryOutput, err := e.executeStartQuery(ctx, logsClient, model, queryContext.TimeRange)
|
startQueryOutput, err := e.executeStartQuery(ctx, logsClient, model, queryContext.TimeRange)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
requestParams := simplejson.NewFromAny(map[string]interface{}{
|
requestParams := LogQueryJson{
|
||||||
"region": model.Get("region").MustString(""),
|
Region: model.Region,
|
||||||
"queryId": *startQueryOutput.QueryId,
|
QueryId: *startQueryOutput.QueryId,
|
||||||
})
|
}
|
||||||
|
|
||||||
ticker := time.NewTicker(alertPollPeriod)
|
ticker := time.NewTicker(alertPollPeriod)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
@ -324,18 +344,19 @@ func (e *cloudWatchExecutor) QueryData(ctx context.Context, req *backend.QueryDa
|
|||||||
frontend, but because alerts are executed on the backend the logic needs to be reimplemented here.
|
frontend, but because alerts are executed on the backend the logic needs to be reimplemented here.
|
||||||
*/
|
*/
|
||||||
q := req.Queries[0]
|
q := req.Queries[0]
|
||||||
model, err := simplejson.NewJson(q.JSON)
|
var model DataQueryJson
|
||||||
|
err := json.Unmarshal(q.JSON, &model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
_, fromAlert := req.Headers["FromAlert"]
|
_, fromAlert := req.Headers["FromAlert"]
|
||||||
isLogAlertQuery := fromAlert && model.Get("queryMode").MustString("") == "Logs"
|
isLogAlertQuery := fromAlert && model.QueryMode == logsQueryMode
|
||||||
|
|
||||||
if isLogAlertQuery {
|
if isLogAlertQuery {
|
||||||
return e.executeLogAlertQuery(ctx, req)
|
return e.executeLogAlertQuery(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
queryType := model.Get("type").MustString("")
|
queryType := model.QueryType
|
||||||
|
|
||||||
var result *backend.QueryDataResponse
|
var result *backend.QueryDataResponse
|
||||||
switch queryType {
|
switch queryType {
|
||||||
@ -356,21 +377,22 @@ func (e *cloudWatchExecutor) executeLogAlertQuery(ctx context.Context, req *back
|
|||||||
resp := backend.NewQueryDataResponse()
|
resp := backend.NewQueryDataResponse()
|
||||||
|
|
||||||
for _, q := range req.Queries {
|
for _, q := range req.Queries {
|
||||||
model, err := simplejson.NewJson(q.JSON)
|
var model LogQueryJson
|
||||||
|
err := json.Unmarshal(q.JSON, &model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
model.Set("subtype", "StartQuery")
|
model.Subtype = "StartQuery"
|
||||||
model.Set("queryString", model.Get("expression").MustString(""))
|
model.QueryString = model.Expression
|
||||||
|
|
||||||
region := model.Get("region").MustString(defaultRegion)
|
region := model.Region
|
||||||
if region == defaultRegion {
|
if model.Region == "" || region == defaultRegion {
|
||||||
dsInfo, err := e.getDSInfo(req.PluginContext)
|
dsInfo, err := e.getDSInfo(req.PluginContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
model.Set("region", dsInfo.region)
|
model.Region = dsInfo.region
|
||||||
}
|
}
|
||||||
|
|
||||||
logsClient, err := e.getCWLogsClient(req.PluginContext, region)
|
logsClient, err := e.getCWLogsClient(req.PluginContext, region)
|
||||||
@ -389,10 +411,8 @@ func (e *cloudWatchExecutor) executeLogAlertQuery(ctx context.Context, req *back
|
|||||||
}
|
}
|
||||||
|
|
||||||
var frames []*data.Frame
|
var frames []*data.Frame
|
||||||
|
if len(model.StatsGroups) > 0 && len(dataframe.Fields) > 0 {
|
||||||
statsGroups := model.Get("statsGroups").MustStringArray()
|
frames, err = groupResults(dataframe, model.StatsGroups)
|
||||||
if len(statsGroups) > 0 && len(dataframe.Fields) > 0 {
|
|
||||||
frames, err = groupResults(dataframe, statsGroups)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package cloudwatch
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
@ -13,13 +14,13 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
|
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LimitExceededException = "LimitExceededException"
|
limitExceededException = "LimitExceededException"
|
||||||
defaultLimit = 10
|
defaultLimit = int64(10)
|
||||||
|
logGroupDefaultLimit = int64(50)
|
||||||
)
|
)
|
||||||
|
|
||||||
type AWSError struct {
|
type AWSError struct {
|
||||||
@ -28,6 +29,26 @@ type AWSError struct {
|
|||||||
Payload map[string]string
|
Payload map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LogQueryJson struct {
|
||||||
|
LogType string `json:"type"`
|
||||||
|
SubType string
|
||||||
|
Limit *int64
|
||||||
|
Time int64
|
||||||
|
StartTime int64
|
||||||
|
EndTime int64
|
||||||
|
LogGroupName string
|
||||||
|
LogGroupNames []string
|
||||||
|
LogGroupNamePrefix string
|
||||||
|
LogStreamName string
|
||||||
|
StartFromHead bool
|
||||||
|
Region string
|
||||||
|
QueryString string
|
||||||
|
QueryId string
|
||||||
|
StatsGroups []string
|
||||||
|
Subtype string
|
||||||
|
Expression string
|
||||||
|
}
|
||||||
|
|
||||||
func (e *AWSError) Error() string {
|
func (e *AWSError) Error() string {
|
||||||
return fmt.Sprintf("%s: %s", e.Code, e.Message)
|
return fmt.Sprintf("%s: %s", e.Code, e.Message)
|
||||||
}
|
}
|
||||||
@ -39,7 +60,8 @@ func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, req *backend
|
|||||||
eg, ectx := errgroup.WithContext(ctx)
|
eg, ectx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
for _, query := range req.Queries {
|
for _, query := range req.Queries {
|
||||||
model, err := simplejson.NewJson(query.JSON)
|
var model LogQueryJson
|
||||||
|
err := json.Unmarshal(query.JSON, &model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -58,7 +80,7 @@ func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, req *backend
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
groupedFrames, err := groupResponseFrame(dataframe, model.Get("statsGroups").MustStringArray())
|
groupedFrames, err := groupResponseFrame(dataframe, model.StatsGroups)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -86,25 +108,24 @@ func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, req *backend
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *cloudWatchExecutor) executeLogAction(ctx context.Context, model *simplejson.Json, query backend.DataQuery, pluginCtx backend.PluginContext) (*data.Frame, error) {
|
func (e *cloudWatchExecutor) executeLogAction(ctx context.Context, model LogQueryJson, query backend.DataQuery, pluginCtx backend.PluginContext) (*data.Frame, error) {
|
||||||
subType := model.Get("subtype").MustString()
|
|
||||||
|
|
||||||
dsInfo, err := e.getDSInfo(pluginCtx)
|
dsInfo, err := e.getDSInfo(pluginCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultRegion := dsInfo.region
|
region := dsInfo.region
|
||||||
|
if model.Region != "" {
|
||||||
|
region = model.Region
|
||||||
|
}
|
||||||
|
|
||||||
region := model.Get("region").MustString(defaultRegion)
|
|
||||||
logsClient, err := e.getCWLogsClient(pluginCtx, region)
|
logsClient, err := e.getCWLogsClient(pluginCtx, region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var data *data.Frame = nil
|
var data *data.Frame = nil
|
||||||
|
switch model.SubType {
|
||||||
switch subType {
|
|
||||||
case "DescribeLogGroups":
|
case "DescribeLogGroups":
|
||||||
data, err = e.handleDescribeLogGroups(ctx, logsClient, model)
|
data, err = e.handleDescribeLogGroups(ctx, logsClient, model)
|
||||||
case "GetLogGroupFields":
|
case "GetLogGroupFields":
|
||||||
@ -119,38 +140,36 @@ func (e *cloudWatchExecutor) executeLogAction(ctx context.Context, model *simple
|
|||||||
data, err = e.handleGetLogEvents(ctx, logsClient, model)
|
data, err = e.handleGetLogEvents(ctx, logsClient, model)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to execute log action with subtype: %s: %w", subType, err)
|
return nil, fmt.Errorf("failed to execute log action with subtype: %s: %w", model.SubType, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *cloudWatchExecutor) handleGetLogEvents(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
func (e *cloudWatchExecutor) handleGetLogEvents(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||||
parameters *simplejson.Json) (*data.Frame, error) {
|
parameters LogQueryJson) (*data.Frame, error) {
|
||||||
queryRequest := &cloudwatchlogs.GetLogEventsInput{
|
limit := defaultLimit
|
||||||
Limit: aws.Int64(parameters.Get("limit").MustInt64(defaultLimit)),
|
if parameters.Limit != nil && *parameters.Limit > 0 {
|
||||||
StartFromHead: aws.Bool(parameters.Get("startFromHead").MustBool(false)),
|
limit = *parameters.Limit
|
||||||
}
|
}
|
||||||
|
|
||||||
logGroupName, err := parameters.Get("logGroupName").String()
|
queryRequest := &cloudwatchlogs.GetLogEventsInput{
|
||||||
if err != nil {
|
Limit: aws.Int64(limit),
|
||||||
|
StartFromHead: aws.Bool(parameters.StartFromHead),
|
||||||
|
}
|
||||||
|
|
||||||
|
if parameters.LogGroupName == "" {
|
||||||
return nil, fmt.Errorf("Error: Parameter 'logGroupName' is required")
|
return nil, fmt.Errorf("Error: Parameter 'logGroupName' is required")
|
||||||
}
|
}
|
||||||
queryRequest.SetLogGroupName(logGroupName)
|
queryRequest.SetLogGroupName(parameters.LogGroupName)
|
||||||
|
|
||||||
logStreamName, err := parameters.Get("logStreamName").String()
|
if parameters.LogStreamName == "" {
|
||||||
if err != nil {
|
return nil, fmt.Errorf("Error: Parameter 'logStreamName' is required")
|
||||||
return nil, fmt.Errorf("Error: Parameter 'logStream' is required")
|
|
||||||
}
|
}
|
||||||
queryRequest.SetLogStreamName(logStreamName)
|
queryRequest.SetLogStreamName(parameters.LogStreamName)
|
||||||
|
|
||||||
if startTime, err := parameters.Get("startTime").Int64(); err == nil {
|
queryRequest.SetStartTime(parameters.StartTime)
|
||||||
queryRequest.SetStartTime(startTime)
|
queryRequest.SetEndTime(parameters.EndTime)
|
||||||
}
|
|
||||||
|
|
||||||
if endTime, err := parameters.Get("endTime").Int64(); err == nil {
|
|
||||||
queryRequest.SetEndTime(endTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
logEvents, err := logsClient.GetLogEventsWithContext(ctx, queryRequest)
|
logEvents, err := logsClient.GetLogEventsWithContext(ctx, queryRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -178,19 +197,22 @@ func (e *cloudWatchExecutor) handleGetLogEvents(ctx context.Context, logsClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *cloudWatchExecutor) handleDescribeLogGroups(ctx context.Context,
|
func (e *cloudWatchExecutor) handleDescribeLogGroups(ctx context.Context,
|
||||||
logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json) (*data.Frame, error) {
|
logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters LogQueryJson) (*data.Frame, error) {
|
||||||
logGroupNamePrefix := parameters.Get("logGroupNamePrefix").MustString("")
|
logGroupLimit := logGroupDefaultLimit
|
||||||
|
if parameters.Limit != nil && *parameters.Limit != 0 {
|
||||||
|
logGroupLimit = *parameters.Limit
|
||||||
|
}
|
||||||
|
|
||||||
var response *cloudwatchlogs.DescribeLogGroupsOutput = nil
|
var response *cloudwatchlogs.DescribeLogGroupsOutput = nil
|
||||||
var err error
|
var err error
|
||||||
if len(logGroupNamePrefix) == 0 {
|
if len(parameters.LogGroupNamePrefix) == 0 {
|
||||||
response, err = logsClient.DescribeLogGroupsWithContext(ctx, &cloudwatchlogs.DescribeLogGroupsInput{
|
response, err = logsClient.DescribeLogGroupsWithContext(ctx, &cloudwatchlogs.DescribeLogGroupsInput{
|
||||||
Limit: aws.Int64(parameters.Get("limit").MustInt64(50)),
|
Limit: aws.Int64(logGroupLimit),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
response, err = logsClient.DescribeLogGroupsWithContext(ctx, &cloudwatchlogs.DescribeLogGroupsInput{
|
response, err = logsClient.DescribeLogGroupsWithContext(ctx, &cloudwatchlogs.DescribeLogGroupsInput{
|
||||||
Limit: aws.Int64(parameters.Get("limit").MustInt64(50)),
|
Limit: aws.Int64(logGroupLimit),
|
||||||
LogGroupNamePrefix: aws.String(logGroupNamePrefix),
|
LogGroupNamePrefix: aws.String(parameters.LogGroupNamePrefix),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err != nil || response == nil {
|
if err != nil || response == nil {
|
||||||
@ -209,7 +231,7 @@ func (e *cloudWatchExecutor) handleDescribeLogGroups(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||||
parameters *simplejson.Json, timeRange backend.TimeRange) (*cloudwatchlogs.StartQueryOutput, error) {
|
parameters LogQueryJson, timeRange backend.TimeRange) (*cloudwatchlogs.StartQueryOutput, error) {
|
||||||
startTime := timeRange.From
|
startTime := timeRange.From
|
||||||
endTime := timeRange.To
|
endTime := timeRange.To
|
||||||
|
|
||||||
@ -222,7 +244,7 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c
|
|||||||
// The usage of ltrim around the @log/@logStream fields is a necessary workaround, as without it,
|
// The usage of ltrim around the @log/@logStream fields is a necessary workaround, as without it,
|
||||||
// CloudWatch wouldn't consider a query using a non-alised @log/@logStream valid.
|
// CloudWatch wouldn't consider a query using a non-alised @log/@logStream valid.
|
||||||
modifiedQueryString := "fields @timestamp,ltrim(@log) as " + logIdentifierInternal + ",ltrim(@logStream) as " +
|
modifiedQueryString := "fields @timestamp,ltrim(@log) as " + logIdentifierInternal + ",ltrim(@logStream) as " +
|
||||||
logStreamIdentifierInternal + "|" + parameters.Get("queryString").MustString("")
|
logStreamIdentifierInternal + "|" + parameters.QueryString
|
||||||
|
|
||||||
startQueryInput := &cloudwatchlogs.StartQueryInput{
|
startQueryInput := &cloudwatchlogs.StartQueryInput{
|
||||||
StartTime: aws.Int64(startTime.Unix()),
|
StartTime: aws.Int64(startTime.Unix()),
|
||||||
@ -232,25 +254,25 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c
|
|||||||
// and also a little bit more but as CW logs accept only seconds as integers there is not much to do about
|
// and also a little bit more but as CW logs accept only seconds as integers there is not much to do about
|
||||||
// that.
|
// that.
|
||||||
EndTime: aws.Int64(int64(math.Ceil(float64(endTime.UnixNano()) / 1e9))),
|
EndTime: aws.Int64(int64(math.Ceil(float64(endTime.UnixNano()) / 1e9))),
|
||||||
LogGroupNames: aws.StringSlice(parameters.Get("logGroupNames").MustStringArray()),
|
LogGroupNames: aws.StringSlice(parameters.LogGroupNames),
|
||||||
QueryString: aws.String(modifiedQueryString),
|
QueryString: aws.String(modifiedQueryString),
|
||||||
}
|
}
|
||||||
|
|
||||||
if resultsLimit, err := parameters.Get("limit").Int64(); err == nil {
|
if parameters.Limit != nil {
|
||||||
startQueryInput.Limit = aws.Int64(resultsLimit)
|
startQueryInput.Limit = aws.Int64(*parameters.Limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
return logsClient.StartQueryWithContext(ctx, startQueryInput)
|
return logsClient.StartQueryWithContext(ctx, startQueryInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *cloudWatchExecutor) handleStartQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
func (e *cloudWatchExecutor) handleStartQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||||
model *simplejson.Json, timeRange backend.TimeRange, refID string) (*data.Frame, error) {
|
model LogQueryJson, timeRange backend.TimeRange, refID string) (*data.Frame, error) {
|
||||||
startQueryResponse, err := e.executeStartQuery(ctx, logsClient, model, timeRange)
|
startQueryResponse, err := e.executeStartQuery(ctx, logsClient, model, timeRange)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var awsErr awserr.Error
|
var awsErr awserr.Error
|
||||||
if errors.As(err, &awsErr) && awsErr.Code() == "LimitExceededException" {
|
if errors.As(err, &awsErr) && awsErr.Code() == "LimitExceededException" {
|
||||||
plog.Debug("executeStartQuery limit exceeded", "err", awsErr)
|
plog.Debug("executeStartQuery limit exceeded", "err", awsErr)
|
||||||
return nil, &AWSError{Code: LimitExceededException, Message: err.Error()}
|
return nil, &AWSError{Code: limitExceededException, Message: err.Error()}
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -258,11 +280,14 @@ func (e *cloudWatchExecutor) handleStartQuery(ctx context.Context, logsClient cl
|
|||||||
dataFrame := data.NewFrame(refID, data.NewField("queryId", nil, []string{*startQueryResponse.QueryId}))
|
dataFrame := data.NewFrame(refID, data.NewField("queryId", nil, []string{*startQueryResponse.QueryId}))
|
||||||
dataFrame.RefID = refID
|
dataFrame.RefID = refID
|
||||||
|
|
||||||
clientRegion := model.Get("region").MustString("default")
|
region := "default"
|
||||||
|
if model.Region != "" {
|
||||||
|
region = model.Region
|
||||||
|
}
|
||||||
|
|
||||||
dataFrame.Meta = &data.FrameMeta{
|
dataFrame.Meta = &data.FrameMeta{
|
||||||
Custom: map[string]interface{}{
|
Custom: map[string]interface{}{
|
||||||
"Region": clientRegion,
|
"Region": region,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,9 +295,9 @@ func (e *cloudWatchExecutor) handleStartQuery(ctx context.Context, logsClient cl
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *cloudWatchExecutor) executeStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
func (e *cloudWatchExecutor) executeStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||||
parameters *simplejson.Json) (*cloudwatchlogs.StopQueryOutput, error) {
|
parameters LogQueryJson) (*cloudwatchlogs.StopQueryOutput, error) {
|
||||||
queryInput := &cloudwatchlogs.StopQueryInput{
|
queryInput := &cloudwatchlogs.StopQueryInput{
|
||||||
QueryId: aws.String(parameters.Get("queryId").MustString()),
|
QueryId: aws.String(parameters.QueryId),
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := logsClient.StopQueryWithContext(ctx, queryInput)
|
response, err := logsClient.StopQueryWithContext(ctx, queryInput)
|
||||||
@ -291,7 +316,7 @@ func (e *cloudWatchExecutor) executeStopQuery(ctx context.Context, logsClient cl
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *cloudWatchExecutor) handleStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
func (e *cloudWatchExecutor) handleStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||||
parameters *simplejson.Json) (*data.Frame, error) {
|
parameters LogQueryJson) (*data.Frame, error) {
|
||||||
response, err := e.executeStopQuery(ctx, logsClient, parameters)
|
response, err := e.executeStopQuery(ctx, logsClient, parameters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -302,16 +327,16 @@ func (e *cloudWatchExecutor) handleStopQuery(ctx context.Context, logsClient clo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *cloudWatchExecutor) executeGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
func (e *cloudWatchExecutor) executeGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||||
parameters *simplejson.Json) (*cloudwatchlogs.GetQueryResultsOutput, error) {
|
parameters LogQueryJson) (*cloudwatchlogs.GetQueryResultsOutput, error) {
|
||||||
queryInput := &cloudwatchlogs.GetQueryResultsInput{
|
queryInput := &cloudwatchlogs.GetQueryResultsInput{
|
||||||
QueryId: aws.String(parameters.Get("queryId").MustString()),
|
QueryId: aws.String(parameters.QueryId),
|
||||||
}
|
}
|
||||||
|
|
||||||
return logsClient.GetQueryResultsWithContext(ctx, queryInput)
|
return logsClient.GetQueryResultsWithContext(ctx, queryInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *cloudWatchExecutor) handleGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
func (e *cloudWatchExecutor) handleGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||||
parameters *simplejson.Json, refID string) (*data.Frame, error) {
|
parameters LogQueryJson, refID string) (*data.Frame, error) {
|
||||||
getQueryResultsOutput, err := e.executeGetQueryResults(ctx, logsClient, parameters)
|
getQueryResultsOutput, err := e.executeGetQueryResults(ctx, logsClient, parameters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -329,10 +354,10 @@ func (e *cloudWatchExecutor) handleGetQueryResults(ctx context.Context, logsClie
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *cloudWatchExecutor) handleGetLogGroupFields(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
func (e *cloudWatchExecutor) handleGetLogGroupFields(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||||
parameters *simplejson.Json, refID string) (*data.Frame, error) {
|
parameters LogQueryJson, refID string) (*data.Frame, error) {
|
||||||
queryInput := &cloudwatchlogs.GetLogGroupFieldsInput{
|
queryInput := &cloudwatchlogs.GetLogGroupFieldsInput{
|
||||||
LogGroupName: aws.String(parameters.Get("logGroupName").MustString()),
|
LogGroupName: aws.String(parameters.LogGroupName),
|
||||||
Time: aws.Int64(parameters.Get("time").MustInt64()),
|
Time: aws.Int64(parameters.Time),
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogGroupFieldsOutput, err := logsClient.GetLogGroupFieldsWithContext(ctx, queryInput)
|
getLogGroupFieldsOutput, err := logsClient.GetLogGroupFieldsWithContext(ctx, queryInput)
|
||||||
|
@ -406,33 +406,6 @@ func Test_executeStartQuery(t *testing.T) {
|
|||||||
}, cli.calls.startQueryWithContext)
|
}, cli.calls.startQueryWithContext)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("cannot parse limit as float", func(t *testing.T) {
|
|
||||||
cli = fakeCWLogsClient{}
|
|
||||||
im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
|
||||||
return datasourceInfo{}, nil
|
|
||||||
})
|
|
||||||
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
|
|
||||||
|
|
||||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
|
||||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
|
||||||
Queries: []backend.DataQuery{
|
|
||||||
{
|
|
||||||
RefID: "A",
|
|
||||||
TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)},
|
|
||||||
JSON: json.RawMessage(`{
|
|
||||||
"type": "logAction",
|
|
||||||
"subtype": "StartQuery",
|
|
||||||
"limit": 12.0
|
|
||||||
}`),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
|
||||||
require.Len(t, cli.calls.startQueryWithContext, 1)
|
|
||||||
assert.Nil(t, cli.calls.startQueryWithContext[0].Limit)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("does not populate StartQueryInput.limit when no limit provided", func(t *testing.T) {
|
t.Run("does not populate StartQueryInput.limit when no limit provided", func(t *testing.T) {
|
||||||
cli = fakeCWLogsClient{}
|
cli = fakeCWLogsClient{}
|
||||||
im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package cloudwatch
|
package cloudwatch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
@ -12,32 +13,60 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var validMetricDataID = regexp.MustCompile(`^[a-z][a-zA-Z0-9_]*$`)
|
var validMetricDataID = regexp.MustCompile(`^[a-z][a-zA-Z0-9_]*$`)
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
// 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) {
|
func (e *cloudWatchExecutor) parseQueries(queries []backend.DataQuery, startTime time.Time, endTime time.Time) (map[string][]*cloudWatchQuery, error) {
|
||||||
requestQueries := make(map[string][]*cloudWatchQuery)
|
requestQueries := make(map[string][]*cloudWatchQuery)
|
||||||
|
|
||||||
migratedQueries, err := migrateLegacyQuery(queries, e.features.IsEnabled(featuremgmt.FlagCloudWatchDynamicLabels))
|
migratedQueries, err := migrateLegacyQuery(queries, e.features.IsEnabled(featuremgmt.FlagCloudWatchDynamicLabels))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, query := range migratedQueries {
|
for _, query := range migratedQueries {
|
||||||
model, err := simplejson.NewJson(query.JSON)
|
var model QueryJson
|
||||||
|
err := json.Unmarshal(query.JSON, &model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &queryError{err: err, RefID: query.RefID}
|
return nil, &queryError{err: err, RefID: query.RefID}
|
||||||
}
|
}
|
||||||
|
|
||||||
queryType := model.Get("type").MustString()
|
queryType := model.QueryType
|
||||||
if queryType != "timeSeriesQuery" && queryType != "" {
|
if queryType != "timeSeriesQuery" && queryType != "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if model.MatchExact == nil {
|
||||||
|
trueBooleanValue := true
|
||||||
|
model.MatchExact = &trueBooleanValue
|
||||||
|
}
|
||||||
|
|
||||||
refID := query.RefID
|
refID := query.RefID
|
||||||
query, err := parseRequestQuery(model, refID, startTime, endTime)
|
query, err := parseRequestQuery(model, refID, startTime, endTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -58,7 +87,8 @@ func migrateLegacyQuery(queries []backend.DataQuery, dynamicLabelsEnabled bool)
|
|||||||
migratedQueries := []*backend.DataQuery{}
|
migratedQueries := []*backend.DataQuery{}
|
||||||
for _, q := range queries {
|
for _, q := range queries {
|
||||||
query := q
|
query := q
|
||||||
queryJson, err := simplejson.NewJson(query.JSON)
|
var queryJson *QueryJson
|
||||||
|
err := json.Unmarshal(query.JSON, &queryJson)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -67,12 +97,10 @@ func migrateLegacyQuery(queries []backend.DataQuery, dynamicLabelsEnabled bool)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, labelExists := queryJson.CheckGet("label")
|
if queryJson.Label == nil && dynamicLabelsEnabled {
|
||||||
if !labelExists && dynamicLabelsEnabled {
|
|
||||||
migrateAliasToDynamicLabel(queryJson)
|
migrateAliasToDynamicLabel(queryJson)
|
||||||
}
|
}
|
||||||
|
query.JSON, err = json.Marshal(queryJson)
|
||||||
query.JSON, err = queryJson.MarshalJSON()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -86,16 +114,15 @@ func migrateLegacyQuery(queries []backend.DataQuery, dynamicLabelsEnabled bool)
|
|||||||
// migrateStatisticsToStatistic migrates queries that has a `statistics` field to use the `statistic` field instead.
|
// 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
|
// 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
|
// Read more here https://github.com/grafana/grafana/issues/30629
|
||||||
func migrateStatisticsToStatistic(queryJson *simplejson.Json) error {
|
func migrateStatisticsToStatistic(queryJson *QueryJson) 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 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 {
|
if queryJson.Statistic == nil {
|
||||||
stats, err := queryJson.Get("statistics").StringArray()
|
if queryJson.Statistics == nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("query must have either statistic or statistics field")
|
return fmt.Errorf("query must have either statistic or statistics field")
|
||||||
}
|
}
|
||||||
queryJson.Del("statistics")
|
|
||||||
queryJson.Set("statistic", stats[0])
|
queryJson.Statistic = queryJson.Statistics[0]
|
||||||
|
queryJson.Statistics = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -112,10 +139,13 @@ var aliasPatterns = map[string]string{
|
|||||||
|
|
||||||
var legacyAliasRegexp = regexp.MustCompile(`{{\s*(.+?)\s*}}`)
|
var legacyAliasRegexp = regexp.MustCompile(`{{\s*(.+?)\s*}}`)
|
||||||
|
|
||||||
func migrateAliasToDynamicLabel(queryJson *simplejson.Json) {
|
func migrateAliasToDynamicLabel(queryJson *QueryJson) {
|
||||||
fullAliasField := queryJson.Get("alias").MustString()
|
fullAliasField := ""
|
||||||
if fullAliasField != "" {
|
|
||||||
matches := legacyAliasRegexp.FindAllStringSubmatch(fullAliasField, -1)
|
if queryJson.Alias != nil && *queryJson.Alias != "" {
|
||||||
|
matches := legacyAliasRegexp.FindAllStringSubmatch(*queryJson.Alias, -1)
|
||||||
|
fullAliasField = *queryJson.Alias
|
||||||
|
|
||||||
for _, groups := range matches {
|
for _, groups := range matches {
|
||||||
fullMatch := groups[0]
|
fullMatch := groups[0]
|
||||||
subgroup := groups[1]
|
subgroup := groups[1]
|
||||||
@ -126,36 +156,36 @@ func migrateAliasToDynamicLabel(queryJson *simplejson.Json) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
queryJson.Label = &fullAliasField
|
||||||
queryJson.Set("label", fullAliasField)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseRequestQuery(model *simplejson.Json, refId string, startTime time.Time, endTime time.Time) (*cloudWatchQuery, error) {
|
func parseRequestQuery(model QueryJson, refId string, startTime time.Time, endTime time.Time) (*cloudWatchQuery, error) {
|
||||||
plog.Debug("Parsing request query", "query", model)
|
plog.Debug("Parsing request query", "query", model)
|
||||||
|
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,
|
||||||
|
}
|
||||||
reNumber := regexp.MustCompile(`^\d+$`)
|
reNumber := regexp.MustCompile(`^\d+$`)
|
||||||
region, err := model.Get("region").String()
|
dimensions, err := parseDimensions(model.Dimensions)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
namespace, err := model.Get("namespace").String()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get namespace: %v", err)
|
|
||||||
}
|
|
||||||
metricName, err := model.Get("metricName").String()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get metricName: %v", err)
|
|
||||||
}
|
|
||||||
dimensions, err := parseDimensions(model)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse dimensions: %v", err)
|
return nil, fmt.Errorf("failed to parse dimensions: %v", err)
|
||||||
}
|
}
|
||||||
|
cloudWatchQuery.Dimensions = dimensions
|
||||||
|
|
||||||
statistic, err := model.Get("statistic").String()
|
p := model.Period
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse statistic: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
p := model.Get("period").MustString("")
|
|
||||||
var period int
|
var period int
|
||||||
if strings.ToLower(p) == "auto" || p == "" {
|
if strings.ToLower(p) == "auto" || p == "" {
|
||||||
deltaInSeconds := endTime.Sub(startTime).Seconds()
|
deltaInSeconds := endTime.Sub(startTime).Seconds()
|
||||||
@ -182,9 +212,9 @@ func parseRequestQuery(model *simplejson.Json, refId string, startTime time.Time
|
|||||||
period = int(d.Seconds())
|
period = int(d.Seconds())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cloudWatchQuery.Period = period
|
||||||
|
|
||||||
id := model.Get("id").MustString("")
|
if model.Id == "" {
|
||||||
if id == "" {
|
|
||||||
// Why not just use refId if id is not specified in the frontend? When specifying an id in the editor,
|
// 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,
|
// 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.
|
// it would likely collide with some ref id. That's why the `query` prefix is used.
|
||||||
@ -193,55 +223,48 @@ func parseRequestQuery(model *simplejson.Json, refId string, startTime time.Time
|
|||||||
uuid := uuid.NewString()
|
uuid := uuid.NewString()
|
||||||
suffix = strings.Replace(uuid, "-", "", -1)
|
suffix = strings.Replace(uuid, "-", "", -1)
|
||||||
}
|
}
|
||||||
id = fmt.Sprintf("query%s", suffix)
|
cloudWatchQuery.Id = fmt.Sprintf("query%s", suffix)
|
||||||
}
|
}
|
||||||
expression := model.Get("expression").MustString("")
|
|
||||||
sqlExpression := model.Get("sqlExpression").MustString("")
|
|
||||||
alias := model.Get("alias").MustString()
|
|
||||||
label := model.Get("label").MustString()
|
|
||||||
returnData := !model.Get("hide").MustBool(false)
|
|
||||||
queryType := model.Get("type").MustString()
|
|
||||||
timezoneUTCOffset := model.Get("timezoneUTCOffset").MustString("")
|
|
||||||
|
|
||||||
if queryType == "" {
|
if model.Hide != nil {
|
||||||
|
cloudWatchQuery.ReturnData = !*model.Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.QueryType == "" {
|
||||||
// If no type is provided we assume we are called by alerting service, which requires to return data!
|
// 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
|
// 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.
|
// who (which service) called the TsdbQueryEndpoint.Query(...) function.
|
||||||
returnData = true
|
cloudWatchQuery.ReturnData = true
|
||||||
}
|
}
|
||||||
|
|
||||||
matchExact := model.Get("matchExact").MustBool(true)
|
if model.MetricEditorMode == nil && len(model.Expression) > 0 {
|
||||||
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
|
// this should only ever happen if this is an alerting query that has not yet been migrated in the frontend
|
||||||
metricEditorModeValue = MetricEditorModeRaw
|
cloudWatchQuery.MetricEditorMode = MetricEditorModeRaw
|
||||||
} else {
|
} else {
|
||||||
metricEditorModeValue = metricEditorMode(memv)
|
if model.MetricEditorMode != nil {
|
||||||
|
cloudWatchQuery.MetricEditorMode = metricEditorMode(*model.MetricEditorMode)
|
||||||
|
} else {
|
||||||
|
cloudWatchQuery.MetricEditorMode = metricEditorMode(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cloudWatchQuery{
|
if model.Statistic != nil {
|
||||||
RefId: refId,
|
cloudWatchQuery.Statistic = *model.Statistic
|
||||||
Region: region,
|
}
|
||||||
Id: id,
|
|
||||||
Namespace: namespace,
|
if model.MatchExact != nil {
|
||||||
MetricName: metricName,
|
cloudWatchQuery.MatchExact = *model.MatchExact
|
||||||
Statistic: statistic,
|
}
|
||||||
Expression: expression,
|
|
||||||
ReturnData: returnData,
|
if model.Alias != nil {
|
||||||
Dimensions: dimensions,
|
cloudWatchQuery.Alias = *model.Alias
|
||||||
Period: period,
|
}
|
||||||
Alias: alias,
|
|
||||||
Label: label,
|
if model.Label != nil {
|
||||||
MatchExact: matchExact,
|
cloudWatchQuery.Label = *model.Label
|
||||||
UsedExpression: "",
|
}
|
||||||
MetricQueryType: metricQueryType,
|
|
||||||
MetricEditorMode: metricEditorModeValue,
|
return &cloudWatchQuery, nil
|
||||||
SqlExpression: sqlExpression,
|
|
||||||
TimezoneUTCOffset: timezoneUTCOffset,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRetainedPeriods(timeSince time.Duration) []int {
|
func getRetainedPeriods(timeSince time.Duration) []int {
|
||||||
@ -257,9 +280,9 @@ func getRetainedPeriods(timeSince time.Duration) []int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDimensions(model *simplejson.Json) (map[string][]string, error) {
|
func parseDimensions(dimensions map[string]interface{}) (map[string][]string, error) {
|
||||||
parsedDimensions := make(map[string][]string)
|
parsedDimensions := make(map[string][]string)
|
||||||
for k, v := range model.Get("dimensions").MustMap() {
|
for k, v := range dimensions {
|
||||||
// This is for backwards compatibility. Before 6.5 dimensions values were stored as strings and not arrays
|
// This is for backwards compatibility. Before 6.5 dimensions values were stored as strings and not arrays
|
||||||
if value, ok := v.(string); ok {
|
if value, ok := v.(string); ok {
|
||||||
parsedDimensions[k] = []string{value}
|
parsedDimensions[k] = []string{value}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
package cloudwatch
|
package cloudwatch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -37,32 +37,33 @@ func TestRequestParser(t *testing.T) {
|
|||||||
|
|
||||||
migratedQuery := migratedQueries[0]
|
migratedQuery := migratedQueries[0]
|
||||||
assert.Equal(t, "A", migratedQuery.RefID)
|
assert.Equal(t, "A", migratedQuery.RefID)
|
||||||
model, err := simplejson.NewJson(migratedQuery.JSON)
|
var model QueryJson
|
||||||
|
err = json.Unmarshal(migratedQuery.JSON, &model)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "Average", model.Get("statistic").MustString())
|
assert.Equal(t, "Average", *model.Statistic)
|
||||||
res, err := model.Get("statistic").Array()
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Nil(t, res)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("New dimensions structure", func(t *testing.T) {
|
t.Run("New dimensions structure", func(t *testing.T) {
|
||||||
query := simplejson.NewFromAny(map[string]interface{}{
|
fixtureJSON := []byte(`{
|
||||||
"refId": "ref1",
|
"refId": "ref1",
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"namespace": "ec2",
|
"namespace": "ec2",
|
||||||
"metricName": "CPUUtilization",
|
"metricName": "CPUUtilization",
|
||||||
"id": "",
|
"id": "",
|
||||||
"expression": "",
|
"expression": "",
|
||||||
"dimensions": map[string]interface{}{
|
"dimensions": {
|
||||||
"InstanceId": []interface{}{"test"},
|
"InstanceId": ["test"],
|
||||||
"InstanceType": []interface{}{"test2", "test3"},
|
"InstanceType": ["test2", "test3"]
|
||||||
},
|
},
|
||||||
"statistic": "Average",
|
"statistic": "Average",
|
||||||
"period": "600",
|
"period": "600",
|
||||||
"hide": false,
|
"hide": false
|
||||||
})
|
}`)
|
||||||
|
|
||||||
|
var query QueryJson
|
||||||
|
err := json.Unmarshal(fixtureJSON, &query)
|
||||||
|
require.NoError(t, err)
|
||||||
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
|
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "us-east-1", res.Region)
|
assert.Equal(t, "us-east-1", res.Region)
|
||||||
@ -81,21 +82,25 @@ func TestRequestParser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Old dimensions structure (backwards compatibility)", func(t *testing.T) {
|
t.Run("Old dimensions structure (backwards compatibility)", func(t *testing.T) {
|
||||||
query := simplejson.NewFromAny(map[string]interface{}{
|
fixtureJSON := []byte(`{
|
||||||
"refId": "ref1",
|
"refId": "ref1",
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"namespace": "ec2",
|
"namespace": "ec2",
|
||||||
"metricName": "CPUUtilization",
|
"metricName": "CPUUtilization",
|
||||||
"id": "",
|
"id": "",
|
||||||
"expression": "",
|
"expression": "",
|
||||||
"dimensions": map[string]interface{}{
|
"dimensions": {
|
||||||
"InstanceId": "test",
|
"InstanceId": ["test"],
|
||||||
"InstanceType": "test2",
|
"InstanceType": ["test2"]
|
||||||
},
|
},
|
||||||
"statistic": "Average",
|
"statistic": "Average",
|
||||||
"period": "600",
|
"period": "600",
|
||||||
"hide": false,
|
"hide": false
|
||||||
})
|
}`)
|
||||||
|
|
||||||
|
var query QueryJson
|
||||||
|
err := json.Unmarshal(fixtureJSON, &query)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
|
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -115,21 +120,25 @@ func TestRequestParser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Period defined in the editor by the user is being used when time range is short", func(t *testing.T) {
|
t.Run("Period defined in the editor by the user is being used when time range is short", func(t *testing.T) {
|
||||||
query := simplejson.NewFromAny(map[string]interface{}{
|
fixtureJSON := []byte(`{
|
||||||
"refId": "ref1",
|
"refId": "ref1",
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"namespace": "ec2",
|
"namespace": "ec2",
|
||||||
"metricName": "CPUUtilization",
|
"metricName": "CPUUtilization",
|
||||||
"id": "",
|
"id": "",
|
||||||
"expression": "",
|
"expression": "",
|
||||||
"dimensions": map[string]interface{}{
|
"dimensions": {
|
||||||
"InstanceId": "test",
|
"InstanceId": ["test"],
|
||||||
"InstanceType": "test2",
|
"InstanceType": ["test2"]
|
||||||
},
|
},
|
||||||
"statistic": "Average",
|
"statistic": "Average",
|
||||||
"hide": false,
|
"hide": false
|
||||||
})
|
}`)
|
||||||
query.Set("period", "900")
|
|
||||||
|
var query QueryJson
|
||||||
|
err := json.Unmarshal(fixtureJSON, &query)
|
||||||
|
require.NoError(t, err)
|
||||||
|
query.Period = "900"
|
||||||
|
|
||||||
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
|
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -137,24 +146,28 @@ func TestRequestParser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Period is parsed correctly if not defined by user", func(t *testing.T) {
|
t.Run("Period is parsed correctly if not defined by user", func(t *testing.T) {
|
||||||
query := simplejson.NewFromAny(map[string]interface{}{
|
fixtureJSON := []byte(`{
|
||||||
"refId": "ref1",
|
"refId": "ref1",
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"namespace": "ec2",
|
"namespace": "ec2",
|
||||||
"metricName": "CPUUtilization",
|
"metricName": "CPUUtilization",
|
||||||
"id": "",
|
"id": "",
|
||||||
"expression": "",
|
"expression": "",
|
||||||
"dimensions": map[string]interface{}{
|
"dimensions": {
|
||||||
"InstanceId": "test",
|
"InstanceId": ["test"],
|
||||||
"InstanceType": "test2",
|
"InstanceType": ["test2"]
|
||||||
},
|
},
|
||||||
"statistic": "Average",
|
"statistic": "Average",
|
||||||
"hide": false,
|
"hide": false,
|
||||||
"period": "auto",
|
"period": "auto"
|
||||||
})
|
}`)
|
||||||
|
|
||||||
|
var query QueryJson
|
||||||
|
err := json.Unmarshal(fixtureJSON, &query)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("Time range is 5 minutes", func(t *testing.T) {
|
t.Run("Time range is 5 minutes", func(t *testing.T) {
|
||||||
query.Set("period", "auto")
|
query.Period = "auto"
|
||||||
to := time.Now()
|
to := time.Now()
|
||||||
from := to.Local().Add(time.Minute * time.Duration(5))
|
from := to.Local().Add(time.Minute * time.Duration(5))
|
||||||
|
|
||||||
@ -164,7 +177,7 @@ func TestRequestParser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Time range is 1 day", func(t *testing.T) {
|
t.Run("Time range is 1 day", func(t *testing.T) {
|
||||||
query.Set("period", "auto")
|
query.Period = "auto"
|
||||||
to := time.Now()
|
to := time.Now()
|
||||||
from := to.AddDate(0, 0, -1)
|
from := to.AddDate(0, 0, -1)
|
||||||
|
|
||||||
@ -174,7 +187,7 @@ func TestRequestParser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Time range is 2 days", func(t *testing.T) {
|
t.Run("Time range is 2 days", func(t *testing.T) {
|
||||||
query.Set("period", "auto")
|
query.Period = "auto"
|
||||||
to := time.Now()
|
to := time.Now()
|
||||||
from := to.AddDate(0, 0, -2)
|
from := to.AddDate(0, 0, -2)
|
||||||
res, err := parseRequestQuery(query, "ref1", from, to)
|
res, err := parseRequestQuery(query, "ref1", from, to)
|
||||||
@ -183,7 +196,7 @@ func TestRequestParser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Time range is 7 days", func(t *testing.T) {
|
t.Run("Time range is 7 days", func(t *testing.T) {
|
||||||
query.Set("period", "auto")
|
query.Period = "auto"
|
||||||
to := time.Now()
|
to := time.Now()
|
||||||
from := to.AddDate(0, 0, -7)
|
from := to.AddDate(0, 0, -7)
|
||||||
|
|
||||||
@ -193,7 +206,7 @@ func TestRequestParser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Time range is 30 days", func(t *testing.T) {
|
t.Run("Time range is 30 days", func(t *testing.T) {
|
||||||
query.Set("period", "auto")
|
query.Period = "auto"
|
||||||
to := time.Now()
|
to := time.Now()
|
||||||
from := to.AddDate(0, 0, -30)
|
from := to.AddDate(0, 0, -30)
|
||||||
|
|
||||||
@ -203,7 +216,7 @@ func TestRequestParser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Time range is 90 days", func(t *testing.T) {
|
t.Run("Time range is 90 days", func(t *testing.T) {
|
||||||
query.Set("period", "auto")
|
query.Period = "auto"
|
||||||
to := time.Now()
|
to := time.Now()
|
||||||
from := to.AddDate(0, 0, -90)
|
from := to.AddDate(0, 0, -90)
|
||||||
|
|
||||||
@ -213,7 +226,7 @@ func TestRequestParser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Time range is 1 year", func(t *testing.T) {
|
t.Run("Time range is 1 year", func(t *testing.T) {
|
||||||
query.Set("period", "auto")
|
query.Period = "auto"
|
||||||
to := time.Now()
|
to := time.Now()
|
||||||
from := to.AddDate(-1, 0, 0)
|
from := to.AddDate(-1, 0, 0)
|
||||||
|
|
||||||
@ -223,7 +236,7 @@ func TestRequestParser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Time range is 2 years", func(t *testing.T) {
|
t.Run("Time range is 2 years", func(t *testing.T) {
|
||||||
query.Set("period", "auto")
|
query.Period = "auto"
|
||||||
to := time.Now()
|
to := time.Now()
|
||||||
from := to.AddDate(-2, 0, 0)
|
from := to.AddDate(-2, 0, 0)
|
||||||
|
|
||||||
@ -233,7 +246,7 @@ func TestRequestParser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Time range is 2 days, but 16 days ago", func(t *testing.T) {
|
t.Run("Time range is 2 days, but 16 days ago", func(t *testing.T) {
|
||||||
query.Set("period", "auto")
|
query.Period = "auto"
|
||||||
to := time.Now().AddDate(0, 0, -14)
|
to := time.Now().AddDate(0, 0, -14)
|
||||||
from := to.AddDate(0, 0, -2)
|
from := to.AddDate(0, 0, -2)
|
||||||
res, err := parseRequestQuery(query, "ref1", from, to)
|
res, err := parseRequestQuery(query, "ref1", from, to)
|
||||||
@ -242,7 +255,7 @@ func TestRequestParser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Time range is 2 days, but 90 days ago", func(t *testing.T) {
|
t.Run("Time range is 2 days, but 90 days ago", func(t *testing.T) {
|
||||||
query.Set("period", "auto")
|
query.Period = "auto"
|
||||||
to := time.Now().AddDate(0, 0, -88)
|
to := time.Now().AddDate(0, 0, -88)
|
||||||
from := to.AddDate(0, 0, -2)
|
from := to.AddDate(0, 0, -2)
|
||||||
res, err := parseRequestQuery(query, "ref1", from, to)
|
res, err := parseRequestQuery(query, "ref1", from, to)
|
||||||
@ -251,7 +264,7 @@ func TestRequestParser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Time range is 2 days, but 456 days ago", func(t *testing.T) {
|
t.Run("Time range is 2 days, but 456 days ago", func(t *testing.T) {
|
||||||
query.Set("period", "auto")
|
query.Period = "auto"
|
||||||
to := time.Now().AddDate(0, 0, -454)
|
to := time.Now().AddDate(0, 0, -454)
|
||||||
from := to.AddDate(0, 0, -2)
|
from := to.AddDate(0, 0, -2)
|
||||||
res, err := parseRequestQuery(query, "ref1", from, to)
|
res, err := parseRequestQuery(query, "ref1", from, to)
|
||||||
@ -273,7 +286,7 @@ func TestRequestParser(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("and an expression is specified it should be metric search builder", func(t *testing.T) {
|
t.Run("and an expression is specified it should be metric search builder", func(t *testing.T) {
|
||||||
query := getBaseJsonQuery()
|
query := getBaseJsonQuery()
|
||||||
query.Set("expression", "SUM(a)")
|
query.Expression = "SUM(a)"
|
||||||
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
|
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, MetricQueryTypeSearch, res.MetricQueryType)
|
assert.Equal(t, MetricQueryTypeSearch, res.MetricQueryType)
|
||||||
@ -284,7 +297,7 @@ func TestRequestParser(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("and an expression is specified it should be metric search builder", func(t *testing.T) {
|
t.Run("and an expression is specified it should be metric search builder", func(t *testing.T) {
|
||||||
query := getBaseJsonQuery()
|
query := getBaseJsonQuery()
|
||||||
query.Set("expression", "SUM(a)")
|
query.Expression = "SUM(a)"
|
||||||
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
|
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, MetricQueryTypeSearch, res.MetricQueryType)
|
assert.Equal(t, MetricQueryTypeSearch, res.MetricQueryType)
|
||||||
@ -303,7 +316,7 @@ func TestRequestParser(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Valid id is generated if ID is not provided and refId is not a valid MetricData ID", func(t *testing.T) {
|
t.Run("Valid id is generated if ID is not provided and refId is not a valid MetricData ID", func(t *testing.T) {
|
||||||
query := getBaseJsonQuery()
|
query := getBaseJsonQuery()
|
||||||
query.Set("refId", "$$")
|
query.RefId = "$$"
|
||||||
res, err := parseRequestQuery(query, "$$", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
|
res, err := parseRequestQuery(query, "$$", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "$$", res.RefId)
|
assert.Equal(t, "$$", res.RefId)
|
||||||
@ -312,8 +325,11 @@ func TestRequestParser(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("parseRequestQuery sets label when label is present in json query", func(t *testing.T) {
|
t.Run("parseRequestQuery sets label when label is present in json query", func(t *testing.T) {
|
||||||
query := getBaseJsonQuery()
|
query := getBaseJsonQuery()
|
||||||
query.Set("alias", "some alias")
|
alias := "some alias"
|
||||||
query.Set("label", "some label")
|
query.Alias = &alias
|
||||||
|
|
||||||
|
label := "some label"
|
||||||
|
query.Label = &label
|
||||||
|
|
||||||
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
|
res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
|
||||||
|
|
||||||
@ -323,15 +339,22 @@ func TestRequestParser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBaseJsonQuery() *simplejson.Json {
|
func getBaseJsonQuery() QueryJson {
|
||||||
return simplejson.NewFromAny(map[string]interface{}{
|
fixtureJSON := []byte(`{
|
||||||
"refId": "ref1",
|
"refId": "ref1",
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"namespace": "ec2",
|
"namespace": "ec2",
|
||||||
"metricName": "CPUUtilization",
|
"metricName": "CPUUtilization",
|
||||||
"statistic": "Average",
|
"statistic": "Average",
|
||||||
"period": "900",
|
"period": "900"
|
||||||
})
|
}`)
|
||||||
|
|
||||||
|
var query QueryJson
|
||||||
|
err := json.Unmarshal(fixtureJSON, &query)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_migrateAliasToDynamicLabel_single_query_preserves_old_alias_and_creates_new_label(t *testing.T) {
|
func Test_migrateAliasToDynamicLabel_single_query_preserves_old_alias_and_creates_new_label(t *testing.T) {
|
||||||
@ -352,7 +375,7 @@ func Test_migrateAliasToDynamicLabel_single_query_preserves_old_alias_and_create
|
|||||||
}
|
}
|
||||||
for name, tc := range testCases {
|
for name, tc := range testCases {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
queryJson, err := simplejson.NewJson([]byte(fmt.Sprintf(`{
|
queryJson := []byte(fmt.Sprintf(`{
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"namespace": "ec2",
|
"namespace": "ec2",
|
||||||
"metricName": "CPUUtilization",
|
"metricName": "CPUUtilization",
|
||||||
@ -363,26 +386,35 @@ func Test_migrateAliasToDynamicLabel_single_query_preserves_old_alias_and_create
|
|||||||
"statistic": "Average",
|
"statistic": "Average",
|
||||||
"period": "600",
|
"period": "600",
|
||||||
"hide": false
|
"hide": false
|
||||||
}`, tc.inputAlias)))
|
}`, tc.inputAlias))
|
||||||
|
|
||||||
|
var query QueryJson
|
||||||
|
err := json.Unmarshal(queryJson, &query)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
migrateAliasToDynamicLabel(queryJson)
|
migrateAliasToDynamicLabel(&query)
|
||||||
|
|
||||||
assert.Equal(t, simplejson.NewFromAny(
|
matchedJson := []byte(fmt.Sprintf(`{
|
||||||
map[string]interface{}{
|
"alias": "%s",
|
||||||
"alias": tc.inputAlias,
|
"dimensions": {
|
||||||
"dimensions": map[string]interface{}{"InstanceId": []interface{}{"test"}},
|
"InstanceId": ["test"]
|
||||||
|
},
|
||||||
"hide": false,
|
"hide": false,
|
||||||
"label": tc.expectedLabel,
|
"label": "%s",
|
||||||
"metricName": "CPUUtilization",
|
"metricName": "CPUUtilization",
|
||||||
"namespace": "ec2",
|
"namespace": "ec2",
|
||||||
"period": "600",
|
"period": "600",
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"statistic": "Average"}), queryJson)
|
"statistic": "Average"
|
||||||
|
}`, tc.inputAlias, tc.expectedLabel))
|
||||||
|
|
||||||
|
result, err := json.Marshal(query)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.JSONEq(t, string(matchedJson), string(result))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_Test_migrateLegacyQuery(t *testing.T) {
|
func Test_Test_migrateLegacyQuery(t *testing.T) {
|
||||||
t.Run("migrates alias to label when label does not already exist and feature toggle enabled", func(t *testing.T) {
|
t.Run("migrates alias to label when label does not already exist and feature toggle enabled", func(t *testing.T) {
|
||||||
migratedQueries, err := migrateLegacyQuery(
|
migratedQueries, err := migrateLegacyQuery(
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -297,9 +296,9 @@ func createDataLinks(link string) []data.DataLink {
|
|||||||
func createMeta(query *cloudWatchQuery) *data.FrameMeta {
|
func createMeta(query *cloudWatchQuery) *data.FrameMeta {
|
||||||
return &data.FrameMeta{
|
return &data.FrameMeta{
|
||||||
ExecutedQueryString: query.UsedExpression,
|
ExecutedQueryString: query.UsedExpression,
|
||||||
Custom: simplejson.NewFromAny(map[string]interface{}{
|
Custom: fmt.Sprintf(`{
|
||||||
"period": query.Period,
|
"period": %d,
|
||||||
"id": query.Id,
|
"id": %s,
|
||||||
}),
|
}`, query.Period, query.Id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user