Datasource/Cloudwatch: Adds support for Cloudwatch Logs (#23566)
* Datasource/Cloudwatch: Adds support for Cloudwatch Logs * Fix rebase leftover * Use jsurl for AWS url serialization * WIP: Temporary workaround for CLIQ metrics * Only allow up to 20 log groups to be selected * WIP additional changes * More changes based on feedback * More changes based on PR feedback * Fix strict null errors
This commit is contained in:
@@ -52,7 +52,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
|
||||
return nil, err
|
||||
}
|
||||
|
||||
emptySerieCount := 0
|
||||
emptySeriesCount := 0
|
||||
evalMatchCount := 0
|
||||
var matches []*alerting.EvalMatch
|
||||
|
||||
@@ -61,7 +61,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
|
||||
evalMatch := c.Evaluator.Eval(reducedValue)
|
||||
|
||||
if !reducedValue.Valid {
|
||||
emptySerieCount++
|
||||
emptySeriesCount++
|
||||
}
|
||||
|
||||
if context.IsTestRun {
|
||||
@@ -100,7 +100,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
|
||||
|
||||
return &alerting.ConditionResult{
|
||||
Firing: evalMatchCount > 0,
|
||||
NoDataFound: emptySerieCount == len(seriesList),
|
||||
NoDataFound: emptySeriesCount == len(seriesList),
|
||||
Operator: c.Operator,
|
||||
EvalMatches: matches,
|
||||
}, nil
|
||||
@@ -224,6 +224,9 @@ func (c *QueryCondition) getRequestForAlertRule(datasource *models.DataSource, t
|
||||
DataSource: datasource,
|
||||
},
|
||||
},
|
||||
Headers: map[string]string{
|
||||
"FromAlert": "true",
|
||||
},
|
||||
Debug: debug,
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,17 @@ package cloudwatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
|
||||
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
@@ -15,6 +22,9 @@ type CloudWatchExecutor struct {
|
||||
*models.DataSource
|
||||
ec2Svc ec2iface.EC2API
|
||||
rgtaSvc resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI
|
||||
|
||||
logsClientsByRegion map[string](*cloudwatchlogs.CloudWatchLogs)
|
||||
mux sync.Mutex
|
||||
}
|
||||
|
||||
type DatasourceInfo struct {
|
||||
@@ -28,8 +38,43 @@ type DatasourceInfo struct {
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
func NewCloudWatchExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
||||
return &CloudWatchExecutor{}, nil
|
||||
const CLOUDWATCH_TS_FORMAT = "2006-01-02 15:04:05.000"
|
||||
|
||||
func (e *CloudWatchExecutor) getLogsClient(region string) (*cloudwatchlogs.CloudWatchLogs, error) {
|
||||
e.mux.Lock()
|
||||
defer e.mux.Unlock()
|
||||
|
||||
if logsClient, ok := e.logsClientsByRegion[region]; ok {
|
||||
return logsClient, nil
|
||||
}
|
||||
|
||||
dsInfo := retrieveDsInfo(e.DataSource, region)
|
||||
newLogsClient, err := retrieveLogsClient(dsInfo)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e.logsClientsByRegion[region] = newLogsClient
|
||||
|
||||
return newLogsClient, nil
|
||||
}
|
||||
|
||||
func NewCloudWatchExecutor(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
||||
dsInfo := retrieveDsInfo(datasource, "default")
|
||||
defaultLogsClient, err := retrieveLogsClient(dsInfo)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logsClientsByRegion := make(map[string](*cloudwatchlogs.CloudWatchLogs))
|
||||
logsClientsByRegion[dsInfo.Region] = defaultLogsClient
|
||||
logsClientsByRegion["default"] = defaultLogsClient
|
||||
|
||||
return &CloudWatchExecutor{
|
||||
logsClientsByRegion: logsClientsByRegion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -43,10 +88,60 @@ func init() {
|
||||
aliasFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) alertQuery(ctx context.Context, logsClient *cloudwatchlogs.CloudWatchLogs, queryContext *tsdb.TsdbQuery) (*cloudwatchlogs.GetQueryResultsOutput, error) {
|
||||
const maxAttempts = 8
|
||||
const pollPeriod = 1000 * time.Millisecond
|
||||
|
||||
queryParams := queryContext.Queries[0].Model
|
||||
startQueryOutput, err := e.executeStartQuery(ctx, logsClient, queryParams, queryContext.TimeRange)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
requestParams := simplejson.NewFromAny(map[string]interface{}{
|
||||
"region": queryParams.Get("region").MustString(""),
|
||||
"queryId": *startQueryOutput.QueryId,
|
||||
})
|
||||
|
||||
ticker := time.NewTicker(pollPeriod)
|
||||
defer ticker.Stop()
|
||||
|
||||
attemptCount := 1
|
||||
for range ticker.C {
|
||||
if res, err := e.executeGetQueryResults(ctx, logsClient, requestParams); err != nil {
|
||||
return nil, err
|
||||
} else if isTerminated(*res.Status) {
|
||||
return res, err
|
||||
} else if attemptCount >= maxAttempts {
|
||||
return res, fmt.Errorf("fetching of query results exceeded max number of attempts")
|
||||
}
|
||||
|
||||
attemptCount++
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSource, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||
var result *tsdb.Response
|
||||
e.DataSource = dsInfo
|
||||
queryType := queryContext.Queries[0].Model.Get("type").MustString("")
|
||||
|
||||
/*
|
||||
Unlike many other data sources, with Cloudwatch Logs query requests don't receive the results as the response to the query, but rather
|
||||
an ID is first returned. Following this, a client is expected to send requests along with the ID until the status of the query is complete,
|
||||
receiving (possibly partial) results each time. For queries made via dashboards and Explore, the logic of making these repeated queries is handled on
|
||||
the frontend, but because alerts are executed on the backend the logic needs to be reimplemented here.
|
||||
*/
|
||||
queryParams := queryContext.Queries[0].Model
|
||||
_, fromAlert := queryContext.Headers["FromAlert"]
|
||||
isLogAlertQuery := fromAlert && queryParams.Get("mode").MustString("") == "Logs"
|
||||
|
||||
if isLogAlertQuery {
|
||||
return e.executeLogAlertQuery(ctx, queryContext)
|
||||
}
|
||||
|
||||
queryType := queryParams.Get("type").MustString("")
|
||||
var err error
|
||||
|
||||
switch queryType {
|
||||
@@ -54,6 +149,8 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
|
||||
result, err = e.executeMetricFindQuery(ctx, queryContext)
|
||||
case "annotationQuery":
|
||||
result, err = e.executeAnnotationQuery(ctx, queryContext)
|
||||
case "logAction":
|
||||
result, err = e.executeLogActions(ctx, queryContext)
|
||||
case "timeSeriesQuery":
|
||||
fallthrough
|
||||
default:
|
||||
@@ -62,3 +159,108 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) executeLogAlertQuery(ctx context.Context, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||
queryParams := queryContext.Queries[0].Model
|
||||
queryParams.Set("subtype", "StartQuery")
|
||||
queryParams.Set("queryString", queryParams.Get("expression").MustString(""))
|
||||
|
||||
region := queryParams.Get("region").MustString("default")
|
||||
if region == "default" {
|
||||
region = e.DataSource.JsonData.Get("defaultRegion").MustString()
|
||||
queryParams.Set("region", region)
|
||||
}
|
||||
|
||||
logsClient, err := e.getLogsClient(region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := e.executeStartQuery(ctx, logsClient, queryParams, queryContext.TimeRange)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
queryParams.Set("queryId", *result.QueryId)
|
||||
|
||||
// Get Query Results
|
||||
getQueryResultsOutput, err := e.alertQuery(ctx, logsClient, queryContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataframe, err := queryResultsToDataframe(getQueryResultsOutput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataframeEnc, err := dataframe.MarshalArrow()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &tsdb.Response{
|
||||
Results: make(map[string]*tsdb.QueryResult),
|
||||
}
|
||||
|
||||
response.Results["A"] = &tsdb.QueryResult{
|
||||
RefId: "A",
|
||||
Dataframes: [][]byte{dataframeEnc},
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func queryResultsToDataframe(results *cloudwatchlogs.GetQueryResultsOutput) (*data.Frame, error) {
|
||||
rowCount := len(results.Results)
|
||||
fieldValues := make(map[string]interface{})
|
||||
for i, row := range results.Results {
|
||||
for _, resultField := range row {
|
||||
// Strip @ptr field from results as it's not needed
|
||||
if *resultField.Field == "@ptr" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, exists := fieldValues[*resultField.Field]; !exists {
|
||||
if _, err := time.Parse(CLOUDWATCH_TS_FORMAT, *resultField.Value); err == nil {
|
||||
fieldValues[*resultField.Field] = make([]*time.Time, rowCount)
|
||||
} else if _, err := strconv.ParseFloat(*resultField.Value, 64); err == nil {
|
||||
fieldValues[*resultField.Field] = make([]*float64, rowCount)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if timeField, ok := fieldValues[*resultField.Field].([]*time.Time); ok {
|
||||
parsedTime, err := time.Parse(CLOUDWATCH_TS_FORMAT, *resultField.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeField[i] = &parsedTime
|
||||
} else if numericField, ok := fieldValues[*resultField.Field].([]*float64); ok {
|
||||
parsedFloat, err := strconv.ParseFloat(*resultField.Value, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
numericField[i] = &parsedFloat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newFields := make([]*data.Field, 0)
|
||||
for fieldName, vals := range fieldValues {
|
||||
newFields = append(newFields, data.NewField(fieldName, nil, vals))
|
||||
|
||||
if fieldName == "@timestamp" {
|
||||
newFields[len(newFields)-1].SetConfig(&data.FieldConfig{Title: "Time"})
|
||||
}
|
||||
}
|
||||
|
||||
frame := data.NewFrame("CloudWatchLogsResponse", newFields...)
|
||||
return frame, nil
|
||||
}
|
||||
|
||||
func isTerminated(queryStatus string) bool {
|
||||
return queryStatus == "Complete" || queryStatus == "Cancelled" || queryStatus == "Failed" || queryStatus == "Timeout"
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go/service/sts"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -41,7 +43,7 @@ func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) {
|
||||
}
|
||||
credentialCacheLock.RUnlock()
|
||||
|
||||
accessKeyId := ""
|
||||
accessKeyID := ""
|
||||
secretAccessKey := ""
|
||||
sessionToken := ""
|
||||
var expiration *time.Time = nil
|
||||
@@ -78,7 +80,7 @@ func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Credentials != nil {
|
||||
accessKeyId = *resp.Credentials.AccessKeyId
|
||||
accessKeyID = *resp.Credentials.AccessKeyId
|
||||
secretAccessKey = *resp.Credentials.SecretAccessKey
|
||||
sessionToken = *resp.Credentials.SessionToken
|
||||
expiration = resp.Credentials.Expiration
|
||||
@@ -96,7 +98,7 @@ func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) {
|
||||
creds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.StaticProvider{Value: credentials.Value{
|
||||
AccessKeyID: accessKeyId,
|
||||
AccessKeyID: accessKeyID,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
SessionToken: sessionToken,
|
||||
}},
|
||||
@@ -154,20 +156,24 @@ func ec2RoleProvider(sess *session.Session) credentials.Provider {
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) getDsInfo(region string) *DatasourceInfo {
|
||||
defaultRegion := e.DataSource.JsonData.Get("defaultRegion").MustString()
|
||||
return retrieveDsInfo(e.DataSource, region)
|
||||
}
|
||||
|
||||
func retrieveDsInfo(datasource *models.DataSource, region string) *DatasourceInfo {
|
||||
defaultRegion := datasource.JsonData.Get("defaultRegion").MustString()
|
||||
if region == "default" {
|
||||
region = defaultRegion
|
||||
}
|
||||
|
||||
authType := e.DataSource.JsonData.Get("authType").MustString()
|
||||
assumeRoleArn := e.DataSource.JsonData.Get("assumeRoleArn").MustString()
|
||||
decrypted := e.DataSource.DecryptedValues()
|
||||
authType := datasource.JsonData.Get("authType").MustString()
|
||||
assumeRoleArn := datasource.JsonData.Get("assumeRoleArn").MustString()
|
||||
decrypted := datasource.DecryptedValues()
|
||||
accessKey := decrypted["accessKey"]
|
||||
secretKey := decrypted["secretKey"]
|
||||
|
||||
datasourceInfo := &DatasourceInfo{
|
||||
Region: region,
|
||||
Profile: e.DataSource.Database,
|
||||
Profile: datasource.Database,
|
||||
AuthType: authType,
|
||||
AssumeRoleArn: assumeRoleArn,
|
||||
AccessKey: accessKey,
|
||||
@@ -177,7 +183,7 @@ func (e *CloudWatchExecutor) getDsInfo(region string) *DatasourceInfo {
|
||||
return datasourceInfo
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) getAwsConfig(dsInfo *DatasourceInfo) (*aws.Config, error) {
|
||||
func getAwsConfig(dsInfo *DatasourceInfo) (*aws.Config, error) {
|
||||
creds, err := GetCredentials(dsInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -193,7 +199,7 @@ func (e *CloudWatchExecutor) getAwsConfig(dsInfo *DatasourceInfo) (*aws.Config,
|
||||
|
||||
func (e *CloudWatchExecutor) getClient(region string) (*cloudwatch.CloudWatch, error) {
|
||||
datasourceInfo := e.getDsInfo(region)
|
||||
cfg, err := e.getAwsConfig(datasourceInfo)
|
||||
cfg, err := getAwsConfig(datasourceInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -211,3 +217,23 @@ func (e *CloudWatchExecutor) getClient(region string) (*cloudwatch.CloudWatch, e
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func retrieveLogsClient(datasourceInfo *DatasourceInfo) (*cloudwatchlogs.CloudWatchLogs, error) {
|
||||
cfg, err := getAwsConfig(datasourceInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := cloudwatchlogs.New(sess, cfg)
|
||||
|
||||
client.Handlers.Send.PushFront(func(r *request.Request) {
|
||||
r.HTTPRequest.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
|
||||
})
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func (e *CloudWatchExecutor) executeLogActions(ctx context.Context, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||
resultChan := make(chan *tsdb.QueryResult, len(queryContext.Queries))
|
||||
eg, ectx := errgroup.WithContext(ctx)
|
||||
|
||||
for _, query := range queryContext.Queries {
|
||||
query := query
|
||||
|
||||
eg.Go(func() error {
|
||||
dataframe, err := e.executeLogAction(ectx, queryContext, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dataframeEnc, err := dataframe.MarshalArrow()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resultChan <- &tsdb.QueryResult{RefId: query.RefId, Dataframes: [][]byte{dataframeEnc}}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
close(resultChan)
|
||||
|
||||
response := &tsdb.Response{
|
||||
Results: make(map[string]*tsdb.QueryResult),
|
||||
}
|
||||
|
||||
for result := range resultChan {
|
||||
response.Results[result.RefId] = result
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) executeLogAction(ctx context.Context, queryContext *tsdb.TsdbQuery, query *tsdb.Query) (*data.Frame, error) {
|
||||
parameters := query.Model
|
||||
subType := query.Model.Get("subtype").MustString()
|
||||
|
||||
defaultRegion := e.DataSource.JsonData.Get("defaultRegion").MustString()
|
||||
region := parameters.Get("region").MustString(defaultRegion)
|
||||
logsClient, err := e.getLogsClient(region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data *data.Frame = nil
|
||||
|
||||
switch subType {
|
||||
case "DescribeLogGroups":
|
||||
data, err = e.handleDescribeLogGroups(ctx, logsClient, parameters)
|
||||
case "GetLogGroupFields":
|
||||
data, err = e.handleGetLogGroupFields(ctx, logsClient, parameters, query.RefId)
|
||||
case "StartQuery":
|
||||
data, err = e.handleStartQuery(ctx, logsClient, parameters, queryContext.TimeRange, query.RefId)
|
||||
case "StopQuery":
|
||||
data, err = e.handleStopQuery(ctx, logsClient, parameters)
|
||||
case "GetQueryResults":
|
||||
data, err = e.handleGetQueryResults(ctx, logsClient, parameters, query.RefId)
|
||||
case "GetLogEvents":
|
||||
data, err = e.handleGetLogEvents(ctx, logsClient, parameters)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) handleGetLogEvents(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json) (*data.Frame, error) {
|
||||
queryRequest := &cloudwatchlogs.GetLogEventsInput{
|
||||
Limit: aws.Int64(parameters.Get("limit").MustInt64(10)),
|
||||
StartFromHead: aws.Bool(parameters.Get("startFromHead").MustBool(false)),
|
||||
}
|
||||
|
||||
logGroupName, err := parameters.Get("logGroupName").String()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error: Parameter 'logGroupName' is required")
|
||||
}
|
||||
queryRequest.SetLogGroupName(logGroupName)
|
||||
|
||||
logStreamName, err := parameters.Get("logStreamName").String()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error: Parameter 'logStream' is required")
|
||||
}
|
||||
queryRequest.SetLogStreamName(logStreamName)
|
||||
|
||||
if startTime, err := parameters.Get("startTime").Int64(); err == nil {
|
||||
queryRequest.SetStartTime(startTime)
|
||||
}
|
||||
|
||||
if endTime, err := parameters.Get("endTime").Int64(); err == nil {
|
||||
queryRequest.SetEndTime(endTime)
|
||||
}
|
||||
|
||||
logEvents, err := logsClient.GetLogEventsWithContext(ctx, queryRequest)
|
||||
if err != nil {
|
||||
return nil, errutil.Wrap(err.(awserr.Error).Message(), err)
|
||||
}
|
||||
|
||||
messages := make([]*string, 0)
|
||||
timestamps := make([]*int64, 0)
|
||||
|
||||
sort.Slice(logEvents.Events, func(i, j int) bool {
|
||||
return *(logEvents.Events[i].Timestamp) > *(logEvents.Events[j].Timestamp)
|
||||
})
|
||||
|
||||
for _, event := range logEvents.Events {
|
||||
messages = append(messages, event.Message)
|
||||
timestamps = append(timestamps, event.Timestamp)
|
||||
}
|
||||
|
||||
timestampField := data.NewField("ts", nil, timestamps)
|
||||
timestampField.SetConfig(&data.FieldConfig{Title: "Time"})
|
||||
|
||||
messageField := data.NewField("line", nil, messages)
|
||||
|
||||
return data.NewFrame("logEvents", timestampField, messageField), nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) handleDescribeLogGroups(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json) (*data.Frame, error) {
|
||||
logGroupNamePrefix := parameters.Get("logGroupNamePrefix").MustString("")
|
||||
var response *cloudwatchlogs.DescribeLogGroupsOutput = nil
|
||||
var err error
|
||||
|
||||
if len(logGroupNamePrefix) < 1 {
|
||||
response, err = logsClient.DescribeLogGroupsWithContext(ctx, &cloudwatchlogs.DescribeLogGroupsInput{
|
||||
Limit: aws.Int64(parameters.Get("limit").MustInt64(50)),
|
||||
})
|
||||
} else {
|
||||
response, err = logsClient.DescribeLogGroupsWithContext(ctx, &cloudwatchlogs.DescribeLogGroupsInput{
|
||||
Limit: aws.Int64(parameters.Get("limit").MustInt64(50)),
|
||||
LogGroupNamePrefix: aws.String(logGroupNamePrefix),
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil || response == nil {
|
||||
return nil, errutil.Wrap(err.(awserr.Error).Message(), err)
|
||||
}
|
||||
|
||||
logGroupNames := make([]*string, 0)
|
||||
for _, logGroup := range response.LogGroups {
|
||||
logGroupNames = append(logGroupNames, logGroup.LogGroupName)
|
||||
}
|
||||
|
||||
groupNamesField := data.NewField("logGroupName", nil, logGroupNames)
|
||||
frame := data.NewFrame("logGroups", groupNamesField)
|
||||
|
||||
return frame, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json, timeRange *tsdb.TimeRange) (*cloudwatchlogs.StartQueryOutput, error) {
|
||||
startTime, err := timeRange.ParseFrom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endTime, err := timeRange.ParseTo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !startTime.Before(endTime) {
|
||||
return nil, fmt.Errorf("invalid time range: Start time must be before end time")
|
||||
}
|
||||
|
||||
startQueryInput := &cloudwatchlogs.StartQueryInput{
|
||||
StartTime: aws.Int64(startTime.Unix()),
|
||||
EndTime: aws.Int64(endTime.Unix()),
|
||||
Limit: aws.Int64(parameters.Get("limit").MustInt64(1000)),
|
||||
LogGroupNames: aws.StringSlice(parameters.Get("logGroupNames").MustStringArray()),
|
||||
QueryString: aws.String("fields @timestamp,@log,@logStream|" + parameters.Get("queryString").MustString("")),
|
||||
}
|
||||
return logsClient.StartQueryWithContext(ctx, startQueryInput)
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) handleStartQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json, timeRange *tsdb.TimeRange, refID string) (*data.Frame, error) {
|
||||
startQueryResponse, err := e.executeStartQuery(ctx, logsClient, parameters, timeRange)
|
||||
if err != nil {
|
||||
return nil, errutil.Wrap(err.(awserr.Error).Message(), err)
|
||||
}
|
||||
|
||||
dataFrame := data.NewFrame(refID, data.NewField("queryId", nil, []string{*startQueryResponse.QueryId}))
|
||||
dataFrame.RefID = refID
|
||||
|
||||
clientRegion := parameters.Get("region").MustString("default")
|
||||
|
||||
dataFrame.Meta = &data.FrameMeta{
|
||||
Custom: map[string]interface{}{
|
||||
"Region": clientRegion,
|
||||
},
|
||||
}
|
||||
|
||||
return dataFrame, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) executeStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json) (*cloudwatchlogs.StopQueryOutput, error) {
|
||||
queryInput := &cloudwatchlogs.StopQueryInput{
|
||||
QueryId: aws.String(parameters.Get("queryId").MustString()),
|
||||
}
|
||||
|
||||
response, err := logsClient.StopQueryWithContext(ctx, queryInput)
|
||||
if err != nil {
|
||||
awsErr := err.(awserr.Error)
|
||||
// If the query has already stopped by the time CloudWatch receives the stop query request,
|
||||
// an "InvalidParameterException" error is returned. For our purposes though the query has been
|
||||
// stopped, so we ignore the error.
|
||||
if awsErr.Code() == "InvalidParameterException" {
|
||||
response = &cloudwatchlogs.StopQueryOutput{Success: aws.Bool(false)}
|
||||
err = nil
|
||||
} else {
|
||||
err = errutil.Wrap(awsErr.Message(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) handleStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json) (*data.Frame, error) {
|
||||
response, err := e.executeStopQuery(ctx, logsClient, parameters)
|
||||
if err != nil {
|
||||
return nil, errutil.Wrap(err.(awserr.Error).Message(), err)
|
||||
}
|
||||
|
||||
dataFrame := data.NewFrame("StopQueryResponse", data.NewField("success", nil, []bool{*response.Success}))
|
||||
return dataFrame, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) executeGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json) (*cloudwatchlogs.GetQueryResultsOutput, error) {
|
||||
queryInput := &cloudwatchlogs.GetQueryResultsInput{
|
||||
QueryId: aws.String(parameters.Get("queryId").MustString()),
|
||||
}
|
||||
|
||||
return logsClient.GetQueryResultsWithContext(ctx, queryInput)
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) handleGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json, refID string) (*data.Frame, error) {
|
||||
getQueryResultsOutput, err := e.executeGetQueryResults(ctx, logsClient, parameters)
|
||||
if err != nil {
|
||||
return nil, errutil.Wrap(err.(awserr.Error).Message(), err)
|
||||
}
|
||||
|
||||
dataFrame, err := logsResultsToDataframes(getQueryResultsOutput)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataFrame.Name = refID
|
||||
dataFrame.RefID = refID
|
||||
|
||||
return dataFrame, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) handleGetLogGroupFields(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json, refID string) (*data.Frame, error) {
|
||||
queryInput := &cloudwatchlogs.GetLogGroupFieldsInput{
|
||||
LogGroupName: aws.String(parameters.Get("logGroupName").MustString()),
|
||||
Time: aws.Int64(parameters.Get("time").MustInt64()),
|
||||
}
|
||||
|
||||
getLogGroupFieldsOutput, err := logsClient.GetLogGroupFieldsWithContext(ctx, queryInput)
|
||||
if err != nil {
|
||||
return nil, errutil.Wrap(err.(awserr.Error).Message(), err)
|
||||
}
|
||||
|
||||
fieldNames := make([]*string, 0)
|
||||
fieldPercentages := make([]*int64, 0)
|
||||
|
||||
for _, logGroupField := range getLogGroupFieldsOutput.LogGroupFields {
|
||||
fieldNames = append(fieldNames, logGroupField.Name)
|
||||
fieldPercentages = append(fieldPercentages, logGroupField.Percent)
|
||||
}
|
||||
|
||||
dataFrame := data.NewFrame(
|
||||
refID,
|
||||
data.NewField("name", nil, fieldNames),
|
||||
data.NewField("percent", nil, fieldPercentages),
|
||||
)
|
||||
|
||||
dataFrame.RefID = refID
|
||||
|
||||
return dataFrame, nil
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
//***
|
||||
// LogActions Tests
|
||||
//***
|
||||
|
||||
func TestHandleDescribeLogGroups_WhenLogGroupNamePrefixIsEmpty(t *testing.T) {
|
||||
executor := &CloudWatchExecutor{}
|
||||
|
||||
logsClient := &FakeLogsClient{
|
||||
Config: aws.Config{
|
||||
Region: aws.String("default"),
|
||||
},
|
||||
}
|
||||
|
||||
params := simplejson.NewFromAny(map[string]interface{}{
|
||||
"limit": 50,
|
||||
})
|
||||
|
||||
frame, err := executor.handleDescribeLogGroups(context.Background(), logsClient, params)
|
||||
|
||||
expectedField := data.NewField("logGroupName", nil, []*string{aws.String("group_a"), aws.String("group_b"), aws.String("group_c")})
|
||||
expectedFrame := data.NewFrame("logGroups", expectedField)
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, expectedFrame, frame)
|
||||
}
|
||||
|
||||
func TestHandleDescribeLogGroups_WhenLogGroupNamePrefixIsNotEmpty(t *testing.T) {
|
||||
executor := &CloudWatchExecutor{}
|
||||
|
||||
logsClient := &FakeLogsClient{
|
||||
Config: aws.Config{
|
||||
Region: aws.String("default"),
|
||||
},
|
||||
}
|
||||
|
||||
params := simplejson.NewFromAny(map[string]interface{}{
|
||||
"logGroupNamePrefix": "g",
|
||||
})
|
||||
|
||||
frame, err := executor.handleDescribeLogGroups(context.Background(), logsClient, params)
|
||||
|
||||
expectedField := data.NewField("logGroupName", nil, []*string{aws.String("group_a"), aws.String("group_b"), aws.String("group_c")})
|
||||
expectedFrame := data.NewFrame("logGroups", expectedField)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, expectedFrame, frame)
|
||||
}
|
||||
|
||||
func TestHandleGetLogGroupFields_WhenLogGroupNamePrefixIsNotEmpty(t *testing.T) {
|
||||
executor := &CloudWatchExecutor{}
|
||||
|
||||
logsClient := &FakeLogsClient{
|
||||
Config: aws.Config{
|
||||
Region: aws.String("default"),
|
||||
},
|
||||
}
|
||||
|
||||
params := simplejson.NewFromAny(map[string]interface{}{
|
||||
"logGroupName": "group_a",
|
||||
"limit": 50,
|
||||
})
|
||||
|
||||
frame, err := executor.handleGetLogGroupFields(context.Background(), logsClient, params, "A")
|
||||
|
||||
expectedNameField := data.NewField("name", nil, []*string{aws.String("field_a"), aws.String("field_b"), aws.String("field_c")})
|
||||
expectedPercentField := data.NewField("percent", nil, []*int64{aws.Int64(100), aws.Int64(30), aws.Int64(55)})
|
||||
expectedFrame := data.NewFrame("A", expectedNameField, expectedPercentField)
|
||||
expectedFrame.RefID = "A"
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, expectedFrame, frame)
|
||||
}
|
||||
|
||||
func TestExecuteStartQuery(t *testing.T) {
|
||||
executor := &CloudWatchExecutor{}
|
||||
|
||||
logsClient := &FakeLogsClient{
|
||||
Config: aws.Config{
|
||||
Region: aws.String("default"),
|
||||
},
|
||||
}
|
||||
|
||||
timeRange := &tsdb.TimeRange{
|
||||
From: "1584873443000",
|
||||
To: "1584700643000",
|
||||
}
|
||||
|
||||
params := simplejson.NewFromAny(map[string]interface{}{
|
||||
"region": "default",
|
||||
"limit": 50,
|
||||
"queryString": "fields @message",
|
||||
})
|
||||
|
||||
response, err := executor.executeStartQuery(context.Background(), logsClient, params, timeRange)
|
||||
|
||||
var expectedResponse *cloudwatchlogs.StartQueryOutput = nil
|
||||
|
||||
assert.Equal(t, expectedResponse, response)
|
||||
assert.Equal(t, fmt.Errorf("invalid time range: Start time must be before end time"), err)
|
||||
|
||||
}
|
||||
|
||||
func TestHandleStartQuery(t *testing.T) {
|
||||
executor := &CloudWatchExecutor{}
|
||||
|
||||
logsClient := &FakeLogsClient{
|
||||
Config: aws.Config{
|
||||
Region: aws.String("default"),
|
||||
},
|
||||
}
|
||||
|
||||
timeRange := &tsdb.TimeRange{
|
||||
From: "1584700643000",
|
||||
To: "1584873443000",
|
||||
}
|
||||
|
||||
params := simplejson.NewFromAny(map[string]interface{}{
|
||||
"region": "default",
|
||||
"limit": 50,
|
||||
"queryString": "fields @message",
|
||||
})
|
||||
|
||||
frame, err := executor.handleStartQuery(context.Background(), logsClient, params, timeRange, "A")
|
||||
|
||||
expectedField := data.NewField("queryId", nil, []string{"abcd-efgh-ijkl-mnop"})
|
||||
expectedFrame := data.NewFrame("A", expectedField)
|
||||
expectedFrame.RefID = "A"
|
||||
expectedFrame.Meta = &data.FrameMeta{
|
||||
Custom: map[string]interface{}{
|
||||
"Region": "default",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, expectedFrame, frame)
|
||||
}
|
||||
|
||||
func TestHandleStopQuery(t *testing.T) {
|
||||
executor := &CloudWatchExecutor{}
|
||||
|
||||
logsClient := &FakeLogsClient{
|
||||
Config: aws.Config{
|
||||
Region: aws.String("default"),
|
||||
},
|
||||
}
|
||||
|
||||
params := simplejson.NewFromAny(map[string]interface{}{
|
||||
"queryId": "abcd-efgh-ijkl-mnop",
|
||||
})
|
||||
|
||||
frame, err := executor.handleStopQuery(context.Background(), logsClient, params)
|
||||
|
||||
expectedField := data.NewField("success", nil, []bool{true})
|
||||
expectedFrame := data.NewFrame("StopQueryResponse", expectedField)
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, expectedFrame, frame)
|
||||
}
|
||||
|
||||
func TestHandleGetQueryResults(t *testing.T) {
|
||||
executor := &CloudWatchExecutor{}
|
||||
|
||||
logsClient := &FakeLogsClient{
|
||||
Config: aws.Config{
|
||||
Region: aws.String("default"),
|
||||
},
|
||||
}
|
||||
|
||||
params := simplejson.NewFromAny(map[string]interface{}{
|
||||
"queryId": "abcd-efgh-ijkl-mnop",
|
||||
})
|
||||
|
||||
frame, err := executor.handleGetQueryResults(context.Background(), logsClient, params, "A")
|
||||
timeA, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-20 10:37:23.000")
|
||||
timeB, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-20 10:40:43.000")
|
||||
expectedTimeField := data.NewField("@timestamp", nil, []*time.Time{
|
||||
aws.Time(timeA), aws.Time(timeB),
|
||||
})
|
||||
expectedTimeField.SetConfig(&data.FieldConfig{Title: "Time"})
|
||||
|
||||
expectedFieldB := data.NewField("field_b", nil, []*string{
|
||||
aws.String("b_1"), aws.String("b_2"),
|
||||
})
|
||||
|
||||
expectedFrame := data.NewFrame("A", expectedTimeField, expectedFieldB)
|
||||
expectedFrame.RefID = "A"
|
||||
|
||||
expectedFrame.Meta = &data.FrameMeta{
|
||||
Custom: map[string]interface{}{
|
||||
"Status": "Complete",
|
||||
"Statistics": cloudwatchlogs.QueryStatistics{
|
||||
BytesScanned: aws.Float64(512),
|
||||
RecordsMatched: aws.Float64(256),
|
||||
RecordsScanned: aws.Float64(1024),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, nil, err)
|
||||
assert.ElementsMatch(t, expectedFrame.Fields, frame.Fields)
|
||||
assert.Equal(t, expectedFrame.Meta, frame.Meta)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
)
|
||||
|
||||
func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput) (*data.Frame, error) {
|
||||
rowCount := len(response.Results)
|
||||
fieldValues := make(map[string]interface{})
|
||||
for i, row := range response.Results {
|
||||
for _, resultField := range row {
|
||||
// Strip @ptr field from results as it's not needed
|
||||
if *resultField.Field == "@ptr" {
|
||||
continue
|
||||
}
|
||||
|
||||
if *resultField.Field == "@timestamp" {
|
||||
if _, exists := fieldValues[*resultField.Field]; !exists {
|
||||
fieldValues[*resultField.Field] = make([]*time.Time, rowCount)
|
||||
}
|
||||
|
||||
parsedTime, err := time.Parse(CLOUDWATCH_TS_FORMAT, *resultField.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fieldValues[*resultField.Field].([]*time.Time)[i] = &parsedTime
|
||||
} else {
|
||||
if _, exists := fieldValues[*resultField.Field]; !exists {
|
||||
// Check if field is time field
|
||||
if _, err := time.Parse(CLOUDWATCH_TS_FORMAT, *resultField.Value); err == nil {
|
||||
fieldValues[*resultField.Field] = make([]*time.Time, rowCount)
|
||||
} else {
|
||||
fieldValues[*resultField.Field] = make([]*string, rowCount)
|
||||
}
|
||||
}
|
||||
|
||||
if timeField, ok := fieldValues[*resultField.Field].([]*time.Time); ok {
|
||||
parsedTime, err := time.Parse(CLOUDWATCH_TS_FORMAT, *resultField.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeField[i] = &parsedTime
|
||||
} else {
|
||||
fieldValues[*resultField.Field].([]*string)[i] = resultField.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newFields := make([]*data.Field, 0)
|
||||
for fieldName, vals := range fieldValues {
|
||||
newFields = append(newFields, data.NewField(fieldName, nil, vals))
|
||||
|
||||
if fieldName == "@timestamp" {
|
||||
newFields[len(newFields)-1].SetConfig(&data.FieldConfig{Title: "Time"})
|
||||
} else if fieldName == "@logStream" || fieldName == "@log" {
|
||||
newFields[len(newFields)-1].SetConfig(
|
||||
&data.FieldConfig{
|
||||
Custom: map[string]interface{}{
|
||||
"Hidden": true,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
frame := data.NewFrame("CloudWatchLogsResponse", newFields...)
|
||||
frame.Meta = &data.FrameMeta{
|
||||
Custom: map[string]interface{}{
|
||||
"Status": *response.Status,
|
||||
"Statistics": *response.Statistics,
|
||||
},
|
||||
}
|
||||
|
||||
return frame, nil
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
//***
|
||||
// LogQuery tests
|
||||
//***
|
||||
|
||||
func TestLogsResultsToDataframes(t *testing.T) {
|
||||
fakeCloudwatchResponse := &cloudwatchlogs.GetQueryResultsOutput{
|
||||
Results: [][]*cloudwatchlogs.ResultField{
|
||||
{
|
||||
&cloudwatchlogs.ResultField{
|
||||
Field: aws.String("@ptr"),
|
||||
Value: aws.String("fake ptr"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
Field: aws.String("@timestamp"),
|
||||
Value: aws.String("2020-03-02 15:04:05.000"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
Field: aws.String("line"),
|
||||
Value: aws.String("test message 1"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
Field: aws.String("@logStream"),
|
||||
Value: aws.String("fakelogstream"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
Field: aws.String("@log"),
|
||||
Value: aws.String("fakelog"),
|
||||
},
|
||||
},
|
||||
{
|
||||
&cloudwatchlogs.ResultField{
|
||||
Field: aws.String("@ptr"),
|
||||
Value: aws.String("fake ptr"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
Field: aws.String("@timestamp"),
|
||||
Value: aws.String("2020-03-02 16:04:05.000"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
Field: aws.String("line"),
|
||||
Value: aws.String("test message 2"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
Field: aws.String("@logStream"),
|
||||
Value: aws.String("fakelogstream"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
Field: aws.String("@log"),
|
||||
Value: aws.String("fakelog"),
|
||||
},
|
||||
},
|
||||
{
|
||||
&cloudwatchlogs.ResultField{
|
||||
Field: aws.String("@ptr"),
|
||||
Value: aws.String("fake ptr"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
Field: aws.String("@timestamp"),
|
||||
Value: aws.String("2020-03-02 17:04:05.000"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
Field: aws.String("line"),
|
||||
Value: aws.String("test message 3"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
Field: aws.String("@logStream"),
|
||||
Value: aws.String("fakelogstream"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
Field: aws.String("@log"),
|
||||
Value: aws.String("fakelog"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: aws.String("ok"),
|
||||
Statistics: &cloudwatchlogs.QueryStatistics{
|
||||
BytesScanned: aws.Float64(2000),
|
||||
RecordsMatched: aws.Float64(3),
|
||||
RecordsScanned: aws.Float64(5000),
|
||||
},
|
||||
}
|
||||
|
||||
dataframes, _ := logsResultsToDataframes(fakeCloudwatchResponse)
|
||||
timeA, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 15:04:05.000")
|
||||
timeB, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 16:04:05.000")
|
||||
timeC, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 17:04:05.000")
|
||||
timeVals := []*time.Time{
|
||||
&timeA, &timeB, &timeC,
|
||||
}
|
||||
timeField := data.NewField("@timestamp", nil, timeVals)
|
||||
timeField.SetConfig(&data.FieldConfig{Title: "Time"})
|
||||
|
||||
lineField := data.NewField("line", nil, []*string{
|
||||
aws.String("test message 1"),
|
||||
aws.String("test message 2"),
|
||||
aws.String("test message 3"),
|
||||
})
|
||||
|
||||
logStreamField := data.NewField("@logStream", nil, []*string{
|
||||
aws.String("fakelogstream"),
|
||||
aws.String("fakelogstream"),
|
||||
aws.String("fakelogstream"),
|
||||
})
|
||||
logStreamField.SetConfig(&data.FieldConfig{
|
||||
Custom: map[string]interface{}{
|
||||
"Hidden": true,
|
||||
},
|
||||
})
|
||||
|
||||
logField := data.NewField("@log", nil, []*string{
|
||||
aws.String("fakelog"),
|
||||
aws.String("fakelog"),
|
||||
aws.String("fakelog"),
|
||||
})
|
||||
logField.SetConfig(&data.FieldConfig{
|
||||
Custom: map[string]interface{}{
|
||||
"Hidden": true,
|
||||
},
|
||||
})
|
||||
|
||||
expectedDataframe := &data.Frame{
|
||||
Name: "CloudWatchLogsResponse",
|
||||
Fields: []*data.Field{
|
||||
timeField,
|
||||
lineField,
|
||||
logStreamField,
|
||||
logField,
|
||||
},
|
||||
RefID: "",
|
||||
Meta: &data.FrameMeta{
|
||||
Custom: map[string]interface{}{
|
||||
"Status": "ok",
|
||||
"Statistics": cloudwatchlogs.QueryStatistics{
|
||||
BytesScanned: aws.Float64(2000),
|
||||
RecordsMatched: aws.Float64(3),
|
||||
RecordsScanned: aws.Float64(5000),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Splitting these assertions up so it's clearer what's wrong should the test
|
||||
// fail in the future
|
||||
assert.Equal(t, expectedDataframe.Name, dataframes.Name)
|
||||
assert.Equal(t, expectedDataframe.RefID, dataframes.RefID)
|
||||
assert.Equal(t, expectedDataframe.Meta, dataframes.Meta)
|
||||
assert.ElementsMatch(t, expectedDataframe.Fields, dataframes.Fields)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
|
||||
)
|
||||
|
||||
type FakeLogsClient struct {
|
||||
cloudwatchlogsiface.CloudWatchLogsAPI
|
||||
Config aws.Config
|
||||
}
|
||||
|
||||
func (f FakeLogsClient) DescribeLogGroupsWithContext(ctx context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, option ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
|
||||
return &cloudwatchlogs.DescribeLogGroupsOutput{
|
||||
LogGroups: []*cloudwatchlogs.LogGroup{
|
||||
{
|
||||
LogGroupName: aws.String("group_a"),
|
||||
},
|
||||
{
|
||||
LogGroupName: aws.String("group_b"),
|
||||
},
|
||||
{
|
||||
LogGroupName: aws.String("group_c"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f FakeLogsClient) GetLogGroupFieldsWithContext(ctx context.Context, input *cloudwatchlogs.GetLogGroupFieldsInput, option ...request.Option) (*cloudwatchlogs.GetLogGroupFieldsOutput, error) {
|
||||
return &cloudwatchlogs.GetLogGroupFieldsOutput{
|
||||
LogGroupFields: []*cloudwatchlogs.LogGroupField{
|
||||
{
|
||||
Name: aws.String("field_a"),
|
||||
Percent: aws.Int64(100),
|
||||
},
|
||||
{
|
||||
Name: aws.String("field_b"),
|
||||
Percent: aws.Int64(30),
|
||||
},
|
||||
{
|
||||
Name: aws.String("field_c"),
|
||||
Percent: aws.Int64(55),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f FakeLogsClient) StartQueryWithContext(ctx context.Context, input *cloudwatchlogs.StartQueryInput, option ...request.Option) (*cloudwatchlogs.StartQueryOutput, error) {
|
||||
return &cloudwatchlogs.StartQueryOutput{
|
||||
QueryId: aws.String("abcd-efgh-ijkl-mnop"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f FakeLogsClient) StopQueryWithContext(ctx context.Context, input *cloudwatchlogs.StopQueryInput, option ...request.Option) (*cloudwatchlogs.StopQueryOutput, error) {
|
||||
return &cloudwatchlogs.StopQueryOutput{
|
||||
Success: aws.Bool(true),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f FakeLogsClient) GetQueryResultsWithContext(ctx context.Context, input *cloudwatchlogs.GetQueryResultsInput, option ...request.Option) (*cloudwatchlogs.GetQueryResultsOutput, error) {
|
||||
return &cloudwatchlogs.GetQueryResultsOutput{
|
||||
Results: [][]*cloudwatchlogs.ResultField{
|
||||
{
|
||||
{
|
||||
Field: aws.String("@timestamp"),
|
||||
Value: aws.String("2020-03-20 10:37:23.000"),
|
||||
},
|
||||
{
|
||||
Field: aws.String("field_b"),
|
||||
Value: aws.String("b_1"),
|
||||
},
|
||||
{
|
||||
Field: aws.String("@ptr"),
|
||||
Value: aws.String("abcdefg"),
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
{
|
||||
Field: aws.String("@timestamp"),
|
||||
Value: aws.String("2020-03-20 10:40:43.000"),
|
||||
},
|
||||
{
|
||||
Field: aws.String("field_b"),
|
||||
Value: aws.String("b_2"),
|
||||
},
|
||||
{
|
||||
Field: aws.String("@ptr"),
|
||||
Value: aws.String("hijklmnop"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Statistics: &cloudwatchlogs.QueryStatistics{
|
||||
BytesScanned: aws.Float64(512),
|
||||
RecordsMatched: aws.Float64(256),
|
||||
RecordsScanned: aws.Float64(1024),
|
||||
},
|
||||
|
||||
Status: aws.String("Complete"),
|
||||
}, nil
|
||||
}
|
||||
@@ -469,7 +469,7 @@ func (e *CloudWatchExecutor) handleGetDimensionValues(ctx context.Context, param
|
||||
func (e *CloudWatchExecutor) ensureClientSession(region string) error {
|
||||
if e.ec2Svc == nil {
|
||||
dsInfo := e.getDsInfo(region)
|
||||
cfg, err := e.getAwsConfig(dsInfo)
|
||||
cfg, err := getAwsConfig(dsInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to call ec2:getAwsConfig, %v", err)
|
||||
}
|
||||
@@ -595,7 +595,7 @@ func (e *CloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context,
|
||||
func (e *CloudWatchExecutor) ensureRGTAClientSession(region string) error {
|
||||
if e.rgtaSvc == nil {
|
||||
dsInfo := e.getDsInfo(region)
|
||||
cfg, err := e.getAwsConfig(dsInfo)
|
||||
cfg, err := getAwsConfig(dsInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to call ec2:getAwsConfig, %v", err)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
|
||||
// Parses the json queries and returns a requestQuery. The requstQuery has a 1 to 1 mapping to a query editor row
|
||||
// Parses the json queries and returns a requestQuery. The requestQuery has a 1 to 1 mapping to a query editor row
|
||||
func (e *CloudWatchExecutor) parseQueries(queryContext *tsdb.TsdbQuery, startTime time.Time, endTime time.Time) (map[string][]*requestQuery, error) {
|
||||
requestQueries := make(map[string][]*requestQuery)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
type TsdbQuery struct {
|
||||
TimeRange *TimeRange
|
||||
Queries []*Query
|
||||
Headers map[string]string
|
||||
Debug bool
|
||||
User *models.SignedInUser
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user