2023-04-13 16:07:06 +08:00
package cloudwatch
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
2025-04-01 16:03:06 +08:00
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
2023-04-13 16:07:06 +08:00
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-plugin-sdk-go/backend"
2024-02-07 20:53:05 +08:00
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
2023-04-13 16:07:06 +08:00
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func Test_executeSyncLogQuery ( t * testing . T ) {
origNewCWClient := NewCWClient
t . Cleanup ( func ( ) {
NewCWClient = origNewCWClient
} )
var cli fakeCWLogsClient
2025-04-01 16:03:06 +08:00
NewCWLogsClient = func ( aws . Config ) models . CWLogsClient {
2023-04-13 16:07:06 +08:00
return & cli
}
t . Run ( "getCWLogsClient is called with region from input JSON" , func ( t * testing . T ) {
2025-04-01 16:03:06 +08:00
cli = fakeCWLogsClient { queryResults : cloudwatchlogs . GetQueryResultsOutput { Status : "Complete" } }
im := defaultTestInstanceManager ( )
2024-02-27 04:59:54 +08:00
executor := newExecutor ( im , log . NewNullLogger ( ) )
2023-04-13 16:07:06 +08:00
_ , err := executor . QueryData ( context . Background ( ) , & backend . QueryDataRequest {
2024-01-25 22:40:55 +08:00
Headers : map [ string ] string { headerFromAlert : "some value" } ,
2023-04-13 16:07:06 +08:00
PluginContext : backend . PluginContext { DataSourceInstanceSettings : & backend . DataSourceInstanceSettings { } } ,
Queries : [ ] backend . DataQuery {
{
TimeRange : backend . TimeRange { From : time . Unix ( 0 , 0 ) , To : time . Unix ( 1 , 0 ) } ,
JSON : json . RawMessage ( ` {
"queryMode" : "Logs" ,
"region" : "some region"
} ` ) ,
} ,
} ,
} )
assert . NoError ( t , err )
2025-04-01 16:03:06 +08:00
//assert.Equal(t, []string{"some region"}, sess.calledRegions)
2023-04-13 16:07:06 +08:00
} )
t . Run ( "getCWLogsClient is called with region from instance manager when region is default" , func ( t * testing . T ) {
2025-04-01 16:03:06 +08:00
cli = fakeCWLogsClient { queryResults : cloudwatchlogs . GetQueryResultsOutput { Status : "Complete" } }
im := testInstanceManagerWithSettings ( models . CloudWatchSettings { AWSDatasourceSettings : awsds . AWSDatasourceSettings { Region : "instance manager's region" } } , false )
2023-04-13 16:07:06 +08:00
2024-02-27 04:59:54 +08:00
executor := newExecutor ( im , log . NewNullLogger ( ) )
2023-04-13 16:07:06 +08:00
_ , err := executor . QueryData ( context . Background ( ) , & backend . QueryDataRequest {
2024-01-25 22:40:55 +08:00
Headers : map [ string ] string { headerFromAlert : "some value" } ,
2023-04-13 16:07:06 +08:00
PluginContext : backend . PluginContext { DataSourceInstanceSettings : & backend . DataSourceInstanceSettings { } } ,
Queries : [ ] backend . DataQuery {
{
TimeRange : backend . TimeRange { From : time . Unix ( 0 , 0 ) , To : time . Unix ( 1 , 0 ) } ,
JSON : json . RawMessage ( ` {
"queryMode" : "Logs" ,
"region" : "default"
} ` ) ,
} ,
} ,
} )
assert . NoError ( t , err )
2025-04-01 16:03:06 +08:00
//assert.Equal(t, []string{"instance manager's region"}, sess.calledRegions)
2023-04-13 16:07:06 +08:00
} )
t . Run ( "with header" , func ( t * testing . T ) {
testcases := [ ] struct {
name string
headers map [ string ] string
called bool
} {
{
"alert header" ,
2024-01-25 22:40:55 +08:00
map [ string ] string { headerFromAlert : "some value" } ,
2023-04-13 16:07:06 +08:00
true ,
} ,
{
"expression header" ,
2024-01-25 22:40:55 +08:00
map [ string ] string { fmt . Sprintf ( "http_%s" , headerFromExpression ) : "some value" } ,
2023-04-13 16:07:06 +08:00
true ,
} ,
{
"no header" ,
map [ string ] string { } ,
false ,
} ,
}
origExecuteSyncLogQuery := executeSyncLogQuery
var syncCalled bool
executeSyncLogQuery = func ( ctx context . Context , e * cloudWatchExecutor , req * backend . QueryDataRequest ) ( * backend . QueryDataResponse , error ) {
syncCalled = true
return nil , nil
}
for _ , tc := range testcases {
t . Run ( tc . name , func ( t * testing . T ) {
syncCalled = false
2025-04-01 16:03:06 +08:00
cli = fakeCWLogsClient { queryResults : cloudwatchlogs . GetQueryResultsOutput { Status : "Complete" } }
im := testInstanceManagerWithSettings ( models . CloudWatchSettings { AWSDatasourceSettings : awsds . AWSDatasourceSettings { Region : "instance manager's region" } } , false )
2023-04-13 16:07:06 +08:00
2024-02-27 04:59:54 +08:00
executor := newExecutor ( im , log . NewNullLogger ( ) )
2023-04-13 16:07:06 +08:00
_ , err := executor . QueryData ( context . Background ( ) , & backend . QueryDataRequest {
Headers : tc . headers ,
PluginContext : backend . PluginContext { DataSourceInstanceSettings : & backend . DataSourceInstanceSettings { } } ,
Queries : [ ] backend . DataQuery {
{
TimeRange : backend . TimeRange { From : time . Unix ( 0 , 0 ) , To : time . Unix ( 1 , 0 ) } ,
JSON : json . RawMessage ( ` {
"queryMode" : "Logs" ,
"type" : "logAction" ,
"subtype" : "StartQuery" ,
"region" : "default" ,
"queryString" : "fields @message"
} ` ) ,
} ,
} ,
} )
assert . NoError ( t , err )
assert . Equal ( t , tc . called , syncCalled )
} )
}
executeSyncLogQuery = origExecuteSyncLogQuery
} )
2023-11-21 06:44:22 +08:00
t . Run ( "when query mode is 'Logs' and does not include type or subtype" , func ( t * testing . T ) {
origExecuteSyncLogQuery := executeSyncLogQuery
syncCalled := false
executeSyncLogQuery = func ( ctx context . Context , e * cloudWatchExecutor , req * backend . QueryDataRequest ) ( * backend . QueryDataResponse , error ) {
syncCalled = true
return nil , nil
}
t . Cleanup ( func ( ) {
executeSyncLogQuery = origExecuteSyncLogQuery
} )
2025-04-01 16:03:06 +08:00
cli = fakeCWLogsClient { queryResults : cloudwatchlogs . GetQueryResultsOutput { Status : "Complete" } }
im := testInstanceManagerWithSettings ( models . CloudWatchSettings { AWSDatasourceSettings : awsds . AWSDatasourceSettings { Region : "instance manager's region" } } , false )
2023-11-21 06:44:22 +08:00
2024-02-27 04:59:54 +08:00
executor := newExecutor ( im , log . NewNullLogger ( ) )
2023-11-21 06:44:22 +08:00
_ , err := executor . QueryData ( context . Background ( ) , & backend . QueryDataRequest {
PluginContext : backend . PluginContext { DataSourceInstanceSettings : & backend . DataSourceInstanceSettings { } } ,
Queries : [ ] backend . DataQuery {
{
TimeRange : backend . TimeRange { From : time . Unix ( 0 , 0 ) , To : time . Unix ( 1 , 0 ) } ,
JSON : json . RawMessage ( ` {
"queryMode" : "Logs" ,
"region" : "default" ,
"queryString" : "fields @message"
} ` ) ,
} ,
} ,
} )
assert . NoError ( t , err )
assert . Equal ( t , true , syncCalled )
} )
2023-04-13 16:07:06 +08:00
}
func Test_executeSyncLogQuery_handles_RefId_from_input_queries ( t * testing . T ) {
origNewCWClient := NewCWClient
t . Cleanup ( func ( ) {
NewCWClient = origNewCWClient
} )
var cli * mockLogsSyncClient
2025-04-01 16:03:06 +08:00
NewCWLogsClient = func ( aws . Config ) models . CWLogsClient {
2023-04-13 16:07:06 +08:00
return cli
}
t . Run ( "when a query refId is not provided, 'A' is assigned by default" , func ( t * testing . T ) {
cli = & mockLogsSyncClient { }
2025-04-01 16:03:06 +08:00
cli . On ( "StartQuery" , mock . Anything , mock . Anything , mock . Anything ) . Return ( & cloudwatchlogs . StartQueryOutput {
2023-04-13 16:07:06 +08:00
QueryId : aws . String ( "abcd-efgh-ijkl-mnop" ) ,
} , nil )
2025-04-01 16:03:06 +08:00
cli . On ( "GetQueryResults" , mock . Anything , mock . Anything , mock . Anything ) . Return ( & cloudwatchlogs . GetQueryResultsOutput { Status : "Complete" } , nil )
im := defaultTestInstanceManager ( )
2024-02-27 04:59:54 +08:00
executor := newExecutor ( im , log . NewNullLogger ( ) )
2023-04-13 16:07:06 +08:00
res , err := executor . QueryData ( context . Background ( ) , & backend . QueryDataRequest {
2024-01-25 22:40:55 +08:00
Headers : map [ string ] string { headerFromAlert : "some value" } ,
2023-04-13 16:07:06 +08:00
PluginContext : backend . PluginContext { DataSourceInstanceSettings : & backend . DataSourceInstanceSettings { } } ,
Queries : [ ] backend . DataQuery {
{
TimeRange : backend . TimeRange { From : time . Unix ( 0 , 0 ) , To : time . Unix ( 1 , 0 ) } ,
JSON : json . RawMessage ( ` {
"queryMode" : "Logs"
} ` ) ,
} ,
} ,
} )
assert . NoError ( t , err )
_ , ok := res . Responses [ "A" ]
assert . True ( t , ok )
} )
t . Run ( "when a query refId is provided, it is returned in the response" , func ( t * testing . T ) {
cli = & mockLogsSyncClient { }
2025-04-01 16:03:06 +08:00
cli . On ( "StartQuery" , mock . Anything , mock . Anything , mock . Anything ) . Return ( & cloudwatchlogs . StartQueryOutput {
2023-04-13 16:07:06 +08:00
QueryId : aws . String ( "abcd-efgh-ijkl-mnop" ) ,
} , nil )
2025-04-01 16:03:06 +08:00
cli . On ( "GetQueryResults" , mock . Anything , mock . Anything , mock . Anything ) . Return ( & cloudwatchlogs . GetQueryResultsOutput { Status : "Complete" } , nil )
im := defaultTestInstanceManager ( )
2024-02-27 04:59:54 +08:00
executor := newExecutor ( im , log . NewNullLogger ( ) )
2023-04-13 16:07:06 +08:00
res , err := executor . QueryData ( context . Background ( ) , & backend . QueryDataRequest {
2024-01-25 22:40:55 +08:00
Headers : map [ string ] string { headerFromAlert : "some value" } ,
2023-04-13 16:07:06 +08:00
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 ( ` {
"queryMode" : "Logs"
} ` ) ,
} ,
{
RefID : "B" ,
TimeRange : backend . TimeRange { From : time . Unix ( 0 , 0 ) , To : time . Unix ( 1 , 0 ) } ,
JSON : json . RawMessage ( ` {
"queryMode" : "Logs"
} ` ) ,
} ,
} ,
} )
assert . NoError ( t , err )
_ , ok := res . Responses [ "A" ]
assert . True ( t , ok )
_ , ok = res . Responses [ "B" ]
assert . True ( t , ok )
} )
t . Run ( "when RefIDs are provided, correctly pass them on with the results" , func ( t * testing . T ) {
// This test demonstrates that given two queries with different RefIds,
// when each query has a different response from AWS API calls, the RefIds are correctly reassigned to the associated response.
cli = & mockLogsSyncClient { }
// mock.MatchedBy makes sure that the QueryId below will only be returned when the input expression = "query string for A"
2025-04-01 16:03:06 +08:00
cli . On ( "StartQuery" , mock . Anything , mock . MatchedBy ( func ( input * cloudwatchlogs . StartQueryInput ) bool {
2023-04-13 16:07:06 +08:00
return * input . QueryString == "fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|query string for A"
} ) , mock . Anything ) . Return ( & cloudwatchlogs . StartQueryOutput {
QueryId : aws . String ( "queryId for A" ) ,
} , nil )
// mock.MatchedBy makes sure that the QueryId below will only be returned when the input expression = "query string for B"
2025-04-01 16:03:06 +08:00
cli . On ( "StartQuery" , mock . Anything , mock . MatchedBy ( func ( input * cloudwatchlogs . StartQueryInput ) bool {
2023-04-13 16:07:06 +08:00
return * input . QueryString == "fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|query string for B"
} ) , mock . Anything ) . Return ( & cloudwatchlogs . StartQueryOutput {
QueryId : aws . String ( "queryId for B" ) ,
} , nil )
2025-04-01 16:03:06 +08:00
cli . On ( "GetQueryResults" , mock . Anything , mock . MatchedBy ( func ( input * cloudwatchlogs . GetQueryResultsInput ) bool {
2023-04-13 16:07:06 +08:00
return * input . QueryId == "queryId for A"
} ) , mock . Anything ) . Return ( & cloudwatchlogs . GetQueryResultsOutput {
// this result will only be returned when the argument is QueryId = "queryId for A"
2025-04-01 16:03:06 +08:00
Results : [ ] [ ] cloudwatchlogstypes . ResultField { { {
2023-04-13 16:07:06 +08:00
Field : utils . Pointer ( "@log" ) ,
Value : utils . Pointer ( "A result" ) ,
} } } ,
2025-04-01 16:03:06 +08:00
Status : "Complete" } , nil )
cli . On ( "GetQueryResults" , mock . Anything , mock . MatchedBy ( func ( input * cloudwatchlogs . GetQueryResultsInput ) bool {
2023-04-13 16:07:06 +08:00
return * input . QueryId == "queryId for B"
} ) , mock . Anything ) . Return ( & cloudwatchlogs . GetQueryResultsOutput {
// this result will only be returned when the argument is QueryId = "queryId for B"
2025-04-01 16:03:06 +08:00
Results : [ ] [ ] cloudwatchlogstypes . ResultField { { {
2023-04-13 16:07:06 +08:00
Field : utils . Pointer ( "@log" ) ,
Value : utils . Pointer ( "B result" ) ,
} } } ,
2025-04-01 16:03:06 +08:00
Status : "Complete" } , nil )
2023-04-13 16:07:06 +08:00
2025-04-01 16:03:06 +08:00
im := defaultTestInstanceManager ( )
2024-02-27 04:59:54 +08:00
executor := newExecutor ( im , log . NewNullLogger ( ) )
2023-04-13 16:07:06 +08:00
res , err := executor . QueryData ( context . Background ( ) , & backend . QueryDataRequest {
2024-01-25 22:40:55 +08:00
Headers : map [ string ] string { headerFromAlert : "some value" } ,
2023-04-13 16:07:06 +08:00
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 ( ` {
"queryMode" : "Logs" ,
"expression" : "query string for A"
} ` ) ,
} ,
{
RefID : "B" ,
TimeRange : backend . TimeRange { From : time . Unix ( 0 , 0 ) , To : time . Unix ( 1 , 0 ) } ,
JSON : json . RawMessage ( ` {
"queryMode" : "Logs" ,
"expression" : "query string for B"
} ` ) ,
} ,
} ,
} )
2025-04-01 16:03:06 +08:00
expectedLogFieldFromFirstCall := data . NewField ( "@log" , nil , [ ] * string { utils . Pointer ( "A result" ) } ) // verifies the response from GetQueryResults matches the input RefId A
2023-04-13 16:07:06 +08:00
assert . NoError ( t , err )
respA , ok := res . Responses [ "A" ]
require . True ( t , ok )
assert . Equal ( t , [ ] * data . Field { expectedLogFieldFromFirstCall } , respA . Frames [ 0 ] . Fields )
2025-04-01 16:03:06 +08:00
expectedLogFieldFromSecondCall := data . NewField ( "@log" , nil , [ ] * string { utils . Pointer ( "B result" ) } ) // verifies the response from GetQueryResults matches the input RefId B
2023-04-13 16:07:06 +08:00
respB , ok := res . Responses [ "B" ]
require . True ( t , ok )
assert . Equal ( t , [ ] * data . Field { expectedLogFieldFromSecondCall } , respB . Frames [ 0 ] . Fields )
} )
2023-08-04 01:35:30 +08:00
t . Run ( "when logsTimeout setting is defined, the polling period will be set to that variable" , func ( t * testing . T ) {
cli = & mockLogsSyncClient { }
2025-04-01 16:03:06 +08:00
cli . On ( "StartQuery" , mock . Anything , mock . Anything , mock . Anything ) . Return ( & cloudwatchlogs . StartQueryOutput {
2023-08-04 01:35:30 +08:00
QueryId : aws . String ( "abcd-efgh-ijkl-mnop" ) ,
} , nil )
2025-04-01 16:03:06 +08:00
cli . On ( "GetQueryResults" , mock . Anything , mock . Anything , mock . Anything ) . Return ( & cloudwatchlogs . GetQueryResultsOutput { Status : "Running" } , nil )
im := testInstanceManagerWithSettings ( models . CloudWatchSettings { LogsTimeout : models . Duration { Duration : time . Millisecond } } , false )
2024-02-07 20:53:05 +08:00
2024-02-27 04:59:54 +08:00
executor := newExecutor ( im , log . NewNullLogger ( ) )
2023-08-04 01:35:30 +08:00
_ , err := executor . QueryData ( context . Background ( ) , & backend . QueryDataRequest {
2024-01-25 22:40:55 +08:00
Headers : map [ string ] string { headerFromAlert : "some value" } ,
2023-08-04 01:35:30 +08:00
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 ( ` {
"queryMode" : "Logs" ,
"expression" : "query string for A"
} ` ) ,
} ,
} ,
} )
assert . Error ( t , err )
2025-04-01 16:03:06 +08:00
cli . AssertNumberOfCalls ( t , "GetQueryResults" , 1 )
2023-08-04 01:35:30 +08:00
} )
2023-08-16 05:31:55 +08:00
t . Run ( "when getQueryResults returns aws error is returned, it keeps the context" , func ( t * testing . T ) {
cli = & mockLogsSyncClient { }
2025-04-01 16:03:06 +08:00
cli . On ( "StartQuery" , mock . Anything , mock . Anything , mock . Anything ) . Return ( & cloudwatchlogs . StartQueryOutput {
2023-08-16 05:31:55 +08:00
QueryId : aws . String ( "abcd-efgh-ijkl-mnop" ) ,
} , nil )
2025-04-01 16:03:06 +08:00
cli . On ( "GetQueryResults" , mock . Anything , mock . Anything , mock . Anything ) . Return (
& cloudwatchlogs . GetQueryResultsOutput { Status : "Complete" } ,
& fakeSmithyError { code : "foo" , message : "bar" } ,
2023-08-16 05:31:55 +08:00
)
2025-04-01 16:03:06 +08:00
im := defaultTestInstanceManager ( )
2024-02-27 04:59:54 +08:00
executor := newExecutor ( im , log . NewNullLogger ( ) )
2023-08-16 05:31:55 +08:00
res , err := executor . QueryData ( context . Background ( ) , & backend . QueryDataRequest {
2024-01-25 22:40:55 +08:00
Headers : map [ string ] string { headerFromAlert : "some value" } ,
2023-08-16 05:31:55 +08:00
PluginContext : backend . PluginContext { DataSourceInstanceSettings : & backend . DataSourceInstanceSettings { } } ,
Queries : [ ] backend . DataQuery {
{
TimeRange : backend . TimeRange { From : time . Unix ( 0 , 0 ) , To : time . Unix ( 1 , 0 ) } ,
JSON : json . RawMessage ( ` {
2024-07-30 21:55:01 +08:00
"refId" : "A" ,
2023-08-16 05:31:55 +08:00
"queryMode" : "Logs"
} ` ) ,
} ,
} ,
} )
2024-07-30 21:55:01 +08:00
require . NotNil ( t , res )
require . NotNil ( t , res . Responses [ "A" ] )
require . Equal ( t , "CloudWatch error: foo: bar" , res . Responses [ "A" ] . Error . Error ( ) )
require . Equal ( t , backend . ErrorSourceDownstream , res . Responses [ "A" ] . ErrorSource )
require . Nil ( t , err )
2023-08-16 05:31:55 +08:00
} )
2023-04-13 16:07:06 +08:00
}