Files
grafana/pkg/tsdb/cloudwatch/log_anomalies_query.go
T

147 lines
5.2 KiB
Go

package cloudwatch
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
cloudwatchLogsTypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery"
)
var executeLogAnomaliesQuery = func(ctx context.Context, ds *DataSource, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
resp := backend.NewQueryDataResponse()
for _, q := range req.Queries {
var anomaliesQuery dataquery.CloudWatchLogsAnomaliesQuery
err := json.Unmarshal(q.JSON, &anomaliesQuery)
if err != nil {
continue
}
region := anomaliesQuery.Region
if region == "" || region == defaultRegion {
anomaliesQuery.Region = ds.Settings.Region
}
logsClient, err := ds.getCWLogsClient(ctx, region)
if err != nil {
return nil, err
}
listAnomaliesInput := &cloudwatchlogs.ListAnomaliesInput{}
if anomaliesQuery.SuppressionState != nil {
listAnomaliesInput.SuppressionState = getSuppressionState(*anomaliesQuery.SuppressionState)
}
if anomaliesQuery.AnomalyDetectionARN == nil || *anomaliesQuery.AnomalyDetectionARN != "" {
listAnomaliesInput.AnomalyDetectorArn = anomaliesQuery.AnomalyDetectionARN
}
response, err := logsClient.ListAnomalies(ctx, listAnomaliesInput)
if err != nil {
result := backend.NewQueryDataResponse()
result.Responses[q.RefID] = backend.ErrorResponseWithErrorSource(backend.DownstreamError(fmt.Errorf("%v: %w", "failed to call cloudwatch:ListAnomalies", err)))
return result, nil
}
dataframe, err := logsAnomaliesResultsToDataframes(response)
if err != nil {
return nil, err
}
respD := resp.Responses[q.RefID]
respD.Frames = data.Frames{dataframe}
resp.Responses[q.RefID] = respD
}
return resp, nil
}
func logsAnomaliesResultsToDataframes(response *cloudwatchlogs.ListAnomaliesOutput) (*data.Frame, error) {
frame := data.NewFrame("Log anomalies")
if len(response.Anomalies) == 0 {
return frame, nil
}
n := len(response.Anomalies)
anomalyArns := make([]string, n)
descriptions := make([]string, n)
suppressedStatus := make([]bool, n)
priorities := make([]string, n)
patterns := make([]string, n)
statuses := make([]string, n)
logGroupArnLists := make([]string, n)
firstSeens := make([]time.Time, n)
lastSeens := make([]time.Time, n)
logTrends := make([]*json.RawMessage, n)
for i, anomaly := range response.Anomalies {
anomalyArns[i] = *anomaly.AnomalyDetectorArn
descriptions[i] = *anomaly.Description
suppressedStatus[i] = *anomaly.Suppressed
priorities[i] = *anomaly.Priority
if anomaly.PatternString != nil {
patterns[i] = *anomaly.PatternString
}
statuses[i] = string(anomaly.State)
logGroupArnLists[i] = strings.Join(anomaly.LogGroupArnList, ",")
firstSeens[i] = time.UnixMilli(anomaly.FirstSeen)
lastSeens[i] = time.UnixMilli(anomaly.LastSeen)
// data.Frame returned from the backend cannot contain fields of type data.Frames
// so histogram is kept as json.RawMessageto be built as sparkline table cell on the FE
histogramField := anomaly.Histogram
histogramJSON, err := json.Marshal(histogramField)
if err != nil {
logTrends[i] = nil
} else {
rawMsg := json.RawMessage(histogramJSON)
logTrends[i] = &rawMsg
}
}
newFields := make([]*data.Field, 0, len(response.Anomalies))
newFields = append(newFields, data.NewField("state", nil, statuses).SetConfig(&data.FieldConfig{DisplayName: "State"}))
newFields = append(newFields, data.NewField("description", nil, descriptions).SetConfig(&data.FieldConfig{DisplayName: "Anomaly"}))
newFields = append(newFields, data.NewField("priority", nil, priorities).SetConfig(&data.FieldConfig{DisplayName: "Priority"}))
newFields = append(newFields, data.NewField("patternString", nil, patterns).SetConfig(&data.FieldConfig{DisplayName: "Log Pattern"}))
// FE expects the field name to be logTrend in order to identify histogram field for sparkline rendering
newFields = append(newFields, data.NewField("logTrend", nil, logTrends).SetConfig(&data.FieldConfig{DisplayName: "Log Trend"}))
newFields = append(newFields, data.NewField("firstSeen", nil, firstSeens).SetConfig(&data.FieldConfig{DisplayName: "First seen"}))
newFields = append(newFields, data.NewField("lastSeen", nil, lastSeens).SetConfig(&data.FieldConfig{DisplayName: "Last seen"}))
newFields = append(newFields, data.NewField("suppressed", nil, suppressedStatus).SetConfig(&data.FieldConfig{DisplayName: "Suppressed?"}))
newFields = append(newFields, data.NewField("logGroupArnList", nil, logGroupArnLists).SetConfig(&data.FieldConfig{DisplayName: "Log Groups"}))
newFields = append(newFields, data.NewField("anomalyArn", nil, anomalyArns).SetConfig(&data.FieldConfig{DisplayName: "Anomaly Arn"}))
frame.Fields = newFields
setPreferredVisType(frame, data.VisTypeTable)
return frame, nil
}
func getSuppressionState(suppressionState string) cloudwatchLogsTypes.SuppressionState {
switch suppressionState {
case "suppressed":
return cloudwatchLogsTypes.SuppressionStateSuppressed
case "unsuppressed":
return cloudwatchLogsTypes.SuppressionStateUnsuppressed
case "all":
return ""
default:
return ""
}
}