2017-04-03 07:50:40 -05:00
package cloudwatch
import (
"context"
2021-03-23 10:32:12 -05:00
"encoding/json"
2024-02-02 12:45:57 -06:00
"errors"
2020-04-25 15:48:20 -05:00
"fmt"
2022-01-18 02:34:12 -06:00
"net/http"
2023-09-27 09:41:48 -05:00
"time"
2017-04-03 07:50:40 -05:00
2022-01-18 02:34:12 -06:00
"github.com/aws/aws-sdk-go/aws"
2020-07-23 11:52:22 -05:00
"github.com/aws/aws-sdk-go/aws/session"
2020-07-14 01:23:23 -05:00
"github.com/aws/aws-sdk-go/service/cloudwatch"
2020-07-23 11:52:22 -05:00
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
2020-04-25 15:48:20 -05:00
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
2020-07-23 01:17:20 -05:00
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
2019-04-15 10:55:07 -05:00
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
2021-03-23 10:32:12 -05:00
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
2024-01-25 06:33:31 -06:00
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
2021-03-23 10:32:12 -05:00
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
2024-02-07 06:53:05 -06:00
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
2024-02-02 12:45:57 -06:00
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
2022-02-16 13:28:26 -06:00
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
2022-10-20 05:53:28 -05:00
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/clients"
2023-04-18 13:56:00 -05:00
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery"
2022-10-20 05:53:28 -05:00
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
2023-09-27 09:41:48 -05:00
"github.com/patrickmn/go-cache"
2017-04-03 07:50:40 -05:00
)
2024-01-25 08:40:55 -06:00
const (
tagValueCacheExpiration = time . Hour * 24
// headerFromExpression is used by datasources to identify expression queries
headerFromExpression = "X-Grafana-From-Expr"
// headerFromAlert is used by datasources to identify alert queries
headerFromAlert = "FromAlert"
)
2023-09-27 09:41:48 -05:00
2022-07-08 14:39:53 -05:00
type DataQueryJson struct {
2023-04-18 13:56:00 -05:00
dataquery . CloudWatchAnnotationQuery
Type string ` json:"type,omitempty" `
2022-07-08 14:39:53 -05:00
}
2022-10-25 02:52:12 -05:00
type DataSource struct {
2023-09-27 09:41:48 -05:00
Settings models . CloudWatchSettings
HTTPClient * http . Client
2024-02-26 14:59:54 -06:00
sessions SessionCache
2023-09-27 09:41:48 -05:00
tagValueCache * cache . Cache
2024-02-02 12:45:57 -06:00
ProxyOpts * proxy . Options
2022-10-25 02:52:12 -05:00
}
2022-01-31 16:30:16 -06:00
const (
2022-12-02 03:21:46 -06:00
defaultRegion = "default"
logsQueryMode = "Logs"
2022-07-27 07:45:59 -05:00
// QueryTypes
annotationQuery = "annotationQuery"
logAction = "logAction"
timeSeriesQuery = "timeSeriesQuery"
2022-01-31 16:30:16 -06:00
)
2020-05-13 08:34:23 -05:00
2024-02-05 12:59:32 -06:00
func ProvideService ( httpClientProvider * httpclient . Provider ) * CloudWatchService {
2024-02-07 06:53:05 -06:00
logger := backend . NewLoggerWith ( "logger" , "tsdb.cloudwatch" )
2022-11-02 09:14:02 -05:00
logger . Debug ( "Initializing" )
2021-03-23 10:32:12 -05:00
2024-02-07 06:53:05 -06:00
executor := newExecutor (
datasource . NewInstanceManager ( NewInstanceSettings ( httpClientProvider ) ) ,
logger ,
)
2021-08-25 08:11:22 -05:00
return & CloudWatchService {
2022-01-20 07:58:39 -06:00
Executor : executor ,
2022-01-20 11:16:22 -06:00
}
2021-08-25 08:11:22 -05:00
}
type CloudWatchService struct {
2022-01-20 07:58:39 -06:00
Executor * cloudWatchExecutor
2021-03-12 07:30:21 -06:00
}
type SessionCache interface {
2024-06-26 10:56:39 -05:00
GetSessionWithAuthSettings ( c awsds . GetSessionConfig , as awsds . AuthSettings ) ( * session . Session , error )
2021-03-08 00:02:49 -06:00
}
2024-02-26 14:59:54 -06:00
func newExecutor ( im instancemgmt . InstanceManager , logger log . Logger ) * cloudWatchExecutor {
2022-10-20 05:53:28 -05:00
e := & cloudWatchExecutor {
2024-02-26 14:59:54 -06:00
im : im ,
logger : logger ,
2020-07-14 01:23:23 -05:00
}
2022-10-20 05:53:28 -05:00
e . resourceHandler = httpadapter . New ( e . newResourceMux ( ) )
return e
}
2024-01-25 06:33:31 -06:00
func NewInstanceSettings ( httpClientProvider * httpclient . Provider ) datasource . InstanceFactoryFunc {
2023-10-16 09:40:04 -05:00
return func ( ctx context . Context , settings backend . DataSourceInstanceSettings ) ( instancemgmt . Instance , error ) {
2024-02-05 12:59:32 -06:00
instanceSettings , err := models . LoadCloudWatchSettings ( ctx , settings )
2021-03-23 10:32:12 -05:00
if err != nil {
return nil , fmt . Errorf ( "error reading settings: %w" , err )
}
2023-10-16 09:40:04 -05:00
opts , err := settings . HTTPClientOptions ( ctx )
2023-03-27 11:00:37 -05:00
if err != nil {
return nil , err
}
httpClient , err := httpClientProvider . New ( opts )
2022-01-18 02:34:12 -06:00
if err != nil {
return nil , fmt . Errorf ( "error creating http client: %w" , err )
}
2022-10-25 02:52:12 -05:00
return DataSource {
2023-09-27 09:41:48 -05:00
Settings : instanceSettings ,
HTTPClient : httpClient ,
tagValueCache : cache . New ( tagValueCacheExpiration , tagValueCacheExpiration * 5 ) ,
2024-02-26 14:59:54 -06:00
sessions : awsds . NewSessionCache ( ) ,
2024-02-02 12:45:57 -06:00
// this is used to build a custom dialer when secure socks proxy is enabled
ProxyOpts : opts . ProxyOptions ,
2022-10-25 02:52:12 -05:00
} , nil
2021-03-23 10:32:12 -05:00
}
}
2024-02-20 07:52:11 -06:00
// cloudWatchExecutor executes CloudWatch requests
2020-07-14 01:23:23 -05:00
type cloudWatchExecutor struct {
2024-02-26 14:59:54 -06:00
im instancemgmt . InstanceManager
logger log . Logger
2022-02-16 13:28:26 -06:00
resourceHandler backend . CallResourceHandler
}
2024-02-07 06:53:05 -06:00
// instrumentContext adds plugin key-values to the context; later, logger.FromContext(ctx) will provide a logger
// that adds these values to its output.
// TODO: move this into the sdk (see https://github.com/grafana/grafana/issues/82033)
func instrumentContext ( ctx context . Context , endpoint string , pCtx backend . PluginContext ) context . Context {
p := [ ] any { "endpoint" , endpoint , "pluginId" , pCtx . PluginID }
if pCtx . DataSourceInstanceSettings != nil {
p = append ( p , "dsName" , pCtx . DataSourceInstanceSettings . Name )
p = append ( p , "dsUID" , pCtx . DataSourceInstanceSettings . UID )
}
if pCtx . User != nil {
p = append ( p , "uname" , pCtx . User . Login )
}
return log . WithContextualAttributes ( ctx , p )
}
2023-05-24 03:19:34 -05:00
func ( e * cloudWatchExecutor ) getRequestContext ( ctx context . Context , pluginCtx backend . PluginContext , region string ) ( models . RequestContext , error ) {
2022-12-02 03:21:46 -06:00
r := region
2023-05-24 03:19:34 -05:00
instance , err := e . getInstance ( ctx , pluginCtx )
2022-12-02 03:21:46 -06:00
if region == defaultRegion {
if err != nil {
return models . RequestContext { } , err
}
r = instance . Settings . Region
}
2023-09-25 13:19:12 -05:00
ec2Client , err := e . getEC2Client ( ctx , pluginCtx , defaultRegion )
if err != nil {
return models . RequestContext { } , err
}
2024-02-26 14:59:54 -06:00
sess , err := instance . newSession ( r )
2022-12-02 03:21:46 -06:00
if err != nil {
return models . RequestContext { } , err
}
2023-09-25 13:19:12 -05:00
2022-12-02 03:21:46 -06:00
return models . RequestContext {
OAMAPIProvider : NewOAMAPI ( sess ) ,
2024-02-05 12:59:32 -06:00
MetricsClientProvider : clients . NewMetricsClient ( NewMetricsAPI ( sess ) , instance . Settings . GrafanaSettings . ListMetricsPageLimit ) ,
2022-12-02 03:21:46 -06:00
LogsAPIProvider : NewLogsAPI ( sess ) ,
2023-09-25 13:19:12 -05:00
EC2APIProvider : ec2Client ,
2022-12-02 03:21:46 -06:00
Settings : instance . Settings ,
2024-02-07 06:53:05 -06:00
Logger : e . logger . FromContext ( ctx ) ,
2022-12-02 03:21:46 -06:00
} , nil
}
2024-03-12 15:30:54 -05:00
// getRequestContextOnlySettings is useful for resource endpoints that are called before auth has been configured such as external-id that need access to settings but nothing else
func ( e * cloudWatchExecutor ) getRequestContextOnlySettings ( ctx context . Context , pluginCtx backend . PluginContext , _ string ) ( models . RequestContext , error ) {
instance , err := e . getInstance ( ctx , pluginCtx )
if err != nil {
return models . RequestContext { } , err
}
return models . RequestContext {
OAMAPIProvider : nil ,
MetricsClientProvider : nil ,
LogsAPIProvider : nil ,
EC2APIProvider : nil ,
Settings : instance . Settings ,
Logger : e . logger . FromContext ( ctx ) ,
} , nil
}
2022-02-16 13:28:26 -06:00
func ( e * cloudWatchExecutor ) CallResource ( ctx context . Context , req * backend . CallResourceRequest , sender backend . CallResourceResponseSender ) error {
2024-07-09 08:03:46 -05:00
ctx = instrumentContext ( ctx , string ( backend . EndpointCallResource ) , req . PluginContext )
2022-02-16 13:28:26 -06:00
return e . resourceHandler . CallResource ( ctx , req , sender )
2020-04-25 15:48:20 -05:00
}
2022-12-02 03:21:46 -06:00
func ( e * cloudWatchExecutor ) QueryData ( ctx context . Context , req * backend . QueryDataRequest ) ( * backend . QueryDataResponse , error ) {
2024-07-09 08:03:46 -05:00
ctx = instrumentContext ( ctx , string ( backend . EndpointQueryData ) , req . PluginContext )
2022-12-02 03:21:46 -06:00
q := req . Queries [ 0 ]
var model DataQueryJson
err := json . Unmarshal ( q . JSON , & model )
if err != nil {
return nil , err
2022-03-02 07:48:51 -06:00
}
2023-03-20 09:54:30 -05:00
2024-01-25 08:40:55 -06:00
_ , fromAlert := req . Headers [ headerFromAlert ]
fromExpression := req . GetHTTPHeader ( headerFromExpression ) != ""
2023-11-20 16:44:22 -06:00
// Public dashboard queries execute like alert queries, i.e. they execute on the backend, therefore, we need to handle them synchronously.
// Since `model.Type` is set during execution on the frontend by the query runner and isn't saved with the query, we are checking here is
// missing the `model.Type` property and if it is a log query in order to determine if it is a public dashboard query.
2024-03-21 05:11:29 -05:00
queryMode := ""
if model . QueryMode != nil {
queryMode = string ( * model . QueryMode )
}
fromPublicDashboard := model . Type == "" && queryMode == logsQueryMode
isSyncLogQuery := ( ( fromAlert || fromExpression ) && queryMode == logsQueryMode ) || fromPublicDashboard
2023-03-30 16:40:01 -05:00
if isSyncLogQuery {
return executeSyncLogQuery ( ctx , e , req )
2022-10-25 08:03:51 -05:00
}
2022-03-02 07:48:51 -06:00
2022-12-02 03:21:46 -06:00
var result * backend . QueryDataResponse
2023-04-18 13:56:00 -05:00
switch model . Type {
2022-12-02 03:21:46 -06:00
case annotationQuery :
2023-05-24 03:19:34 -05:00
result , err = e . executeAnnotationQuery ( ctx , req . PluginContext , model , q )
2022-12-02 03:21:46 -06:00
case logAction :
2024-02-07 06:53:05 -06:00
result , err = e . executeLogActions ( ctx , req )
2022-12-02 03:21:46 -06:00
case timeSeriesQuery :
fallthrough
default :
2024-02-07 06:53:05 -06:00
result , err = e . executeTimeSeriesQuery ( ctx , req )
2022-07-08 14:39:53 -05:00
}
2022-12-02 03:21:46 -06:00
return result , err
2022-03-02 07:48:51 -06:00
}
func ( e * cloudWatchExecutor ) CheckHealth ( ctx context . Context , req * backend . CheckHealthRequest ) ( * backend . CheckHealthResult , error ) {
2024-07-09 08:03:46 -05:00
ctx = instrumentContext ( ctx , string ( backend . EndpointCheckHealth ) , req . PluginContext )
2022-03-02 07:48:51 -06:00
status := backend . HealthStatusOk
metricsTest := "Successfully queried the CloudWatch metrics API."
logsTest := "Successfully queried the CloudWatch logs API."
2023-05-24 03:19:34 -05:00
err := e . checkHealthMetrics ( ctx , req . PluginContext )
2022-03-02 07:48:51 -06:00
if err != nil {
status = backend . HealthStatusError
metricsTest = fmt . Sprintf ( "CloudWatch metrics query failed: %s" , err . Error ( ) )
}
2023-05-24 03:19:34 -05:00
err = e . checkHealthLogs ( ctx , req . PluginContext )
2022-03-02 07:48:51 -06:00
if err != nil {
status = backend . HealthStatusError
logsTest = fmt . Sprintf ( "CloudWatch logs query failed: %s" , err . Error ( ) )
}
return & backend . CheckHealthResult {
Status : status ,
Message : fmt . Sprintf ( "1. %s\n2. %s" , metricsTest , logsTest ) ,
} , nil
}
2023-05-24 03:19:34 -05:00
func ( e * cloudWatchExecutor ) checkHealthMetrics ( ctx context . Context , pluginCtx backend . PluginContext ) error {
2022-12-02 03:21:46 -06:00
namespace := "AWS/Billing"
metric := "EstimatedCharges"
params := & cloudwatch . ListMetricsInput {
Namespace : & namespace ,
MetricName : & metric ,
}
2024-02-26 14:59:54 -06:00
instance , err := e . getInstance ( ctx , pluginCtx )
2022-12-02 03:21:46 -06:00
if err != nil {
return err
}
2024-02-05 12:59:32 -06:00
2024-02-26 14:59:54 -06:00
session , err := instance . newSession ( defaultRegion )
2024-02-05 12:59:32 -06:00
if err != nil {
return err
}
metricClient := clients . NewMetricsClient ( NewMetricsAPI ( session ) , instance . Settings . GrafanaSettings . ListMetricsPageLimit )
2023-10-23 09:17:06 -05:00
_ , err = metricClient . ListMetricsWithPageLimit ( ctx , params )
2022-12-02 03:21:46 -06:00
return err
}
2023-05-24 03:19:34 -05:00
func ( e * cloudWatchExecutor ) checkHealthLogs ( ctx context . Context , pluginCtx backend . PluginContext ) error {
2024-02-26 14:59:54 -06:00
session , err := e . newSessionFromContext ( ctx , pluginCtx , defaultRegion )
2022-12-02 03:21:46 -06:00
if err != nil {
return err
}
logsClient := NewLogsAPI ( session )
2023-11-01 14:06:06 -05:00
_ , err = logsClient . DescribeLogGroupsWithContext ( ctx , & cloudwatchlogs . DescribeLogGroupsInput { Limit : aws . Int64 ( 1 ) } )
2022-12-02 03:21:46 -06:00
return err
}
2024-02-26 14:59:54 -06:00
func ( ds * DataSource ) newSession ( region string ) ( * session . Session , error ) {
2021-03-23 10:32:12 -05:00
if region == defaultRegion {
2024-02-26 14:59:54 -06:00
if len ( ds . Settings . Region ) == 0 {
2023-09-25 13:19:12 -05:00
return nil , models . ErrMissingRegion
}
2024-02-26 14:59:54 -06:00
region = ds . Settings . Region
2021-03-23 10:32:12 -05:00
}
2024-06-26 10:56:39 -05:00
sess , err := ds . sessions . GetSessionWithAuthSettings ( awsds . GetSessionConfig {
2022-03-09 09:29:10 -06:00
// https://github.com/grafana/grafana/issues/46365
2023-03-27 11:00:37 -05:00
// HTTPClient: instance.HTTPClient,
2022-01-18 02:34:12 -06:00
Settings : awsds . AWSDatasourceSettings {
2024-02-26 14:59:54 -06:00
Profile : ds . Settings . Profile ,
2022-01-18 02:34:12 -06:00
Region : region ,
2024-02-26 14:59:54 -06:00
AuthType : ds . Settings . AuthType ,
AssumeRoleARN : ds . Settings . AssumeRoleARN ,
ExternalID : ds . Settings . ExternalID ,
Endpoint : ds . Settings . Endpoint ,
DefaultRegion : ds . Settings . Region ,
AccessKey : ds . Settings . AccessKey ,
SecretKey : ds . Settings . SecretKey ,
2022-01-18 02:34:12 -06:00
} ,
2024-06-26 10:56:39 -05:00
UserAgentName : aws . String ( "Cloudwatch" ) } ,
ds . Settings . GrafanaSettings )
2023-03-27 11:00:37 -05:00
if err != nil {
return nil , err
}
// work around until https://github.com/grafana/grafana/issues/39089 is implemented
2024-02-26 14:59:54 -06:00
if ds . Settings . GrafanaSettings . SecureSocksDSProxyEnabled && ds . Settings . SecureSocksProxyEnabled {
2023-03-27 11:00:37 -05:00
// only update the transport to try to avoid the issue mentioned here https://github.com/grafana/grafana/issues/46365
2024-02-02 12:45:57 -06:00
// also, 'sess' is cached and reused, so the first time it might have the transport not set, the following uses it will
if sess . Config . HTTPClient . Transport == nil {
// following go standard library logic (https://pkg.go.dev/net/http#Client), if no Transport is provided,
// then we use http.DefaultTransport
defTransport , ok := http . DefaultTransport . ( * http . Transport )
if ! ok {
// this should not happen but validating just in case
return nil , errors . New ( "default http client transport is not of type http.Transport" )
}
sess . Config . HTTPClient . Transport = defTransport . Clone ( )
}
2024-02-26 14:59:54 -06:00
err = proxy . New ( ds . ProxyOpts ) . ConfigureSecureSocksHTTPProxy ( sess . Config . HTTPClient . Transport . ( * http . Transport ) )
2024-02-02 12:45:57 -06:00
if err != nil {
return nil , fmt . Errorf ( "error configuring Secure Socks proxy for Transport: %w" , err )
}
2024-08-07 09:11:56 -05:00
} else if sess . Config . HTTPClient != nil {
// Workaround for https://github.com/grafana/grafana/issues/91356 - PDC transport set above
// stays on the cached session after PDC is disabled
sess . Config . HTTPClient . Transport = nil
2023-03-27 11:00:37 -05:00
}
return sess , nil
2020-07-23 11:52:22 -05:00
}
2024-02-26 14:59:54 -06:00
func ( e * cloudWatchExecutor ) newSessionFromContext ( ctx context . Context , pluginCtx backend . PluginContext , region string ) ( * session . Session , error ) {
instance , err := e . getInstance ( ctx , pluginCtx )
if err != nil {
return nil , err
}
return instance . newSession ( region )
}
2023-05-24 03:19:34 -05:00
func ( e * cloudWatchExecutor ) getInstance ( ctx context . Context , pluginCtx backend . PluginContext ) ( * DataSource , error ) {
i , err := e . im . Get ( ctx , pluginCtx )
2022-12-02 03:21:46 -06:00
if err != nil {
return nil , err
}
instance := i . ( DataSource )
return & instance , nil
}
2023-05-24 03:19:34 -05:00
func ( e * cloudWatchExecutor ) getCWClient ( ctx context . Context , pluginCtx backend . PluginContext , region string ) ( cloudwatchiface . CloudWatchAPI , error ) {
2024-02-26 14:59:54 -06:00
sess , err := e . newSessionFromContext ( ctx , pluginCtx , region )
2020-04-25 15:48:20 -05:00
if err != nil {
return nil , err
}
2021-01-07 04:36:13 -06:00
return NewCWClient ( sess ) , nil
2017-04-03 07:50:40 -05:00
}
2023-05-24 03:19:34 -05:00
func ( e * cloudWatchExecutor ) getCWLogsClient ( ctx context . Context , pluginCtx backend . PluginContext , region string ) ( cloudwatchlogsiface . CloudWatchLogsAPI , error ) {
2024-02-26 14:59:54 -06:00
sess , err := e . newSessionFromContext ( ctx , pluginCtx , region )
2020-07-14 01:23:23 -05:00
if err != nil {
return nil , err
}
2021-01-07 04:36:13 -06:00
logsClient := NewCWLogsClient ( sess )
2020-07-23 11:52:22 -05:00
return logsClient , nil
}
2023-05-24 03:19:34 -05:00
func ( e * cloudWatchExecutor ) getEC2Client ( ctx context . Context , pluginCtx backend . PluginContext , region string ) ( models . EC2APIProvider , error ) {
2024-02-26 14:59:54 -06:00
sess , err := e . newSessionFromContext ( ctx , pluginCtx , region )
2020-07-23 11:52:22 -05:00
if err != nil {
return nil , err
}
2020-07-14 01:23:23 -05:00
2023-09-25 13:19:12 -05:00
return NewEC2Client ( sess ) , nil
2020-07-23 11:52:22 -05:00
}
2023-05-24 03:19:34 -05:00
func ( e * cloudWatchExecutor ) getRGTAClient ( ctx context . Context , pluginCtx backend . PluginContext , region string ) ( resourcegroupstaggingapiiface . ResourceGroupsTaggingAPIAPI ,
2020-07-23 11:52:22 -05:00
error ) {
2024-02-26 14:59:54 -06:00
sess , err := e . newSessionFromContext ( ctx , pluginCtx , region )
2020-07-23 11:52:22 -05:00
if err != nil {
return nil , err
}
2021-07-02 03:13:23 -05:00
return newRGTAClient ( sess ) , nil
2017-04-03 07:50:40 -05:00
}
2020-07-23 11:52:22 -05:00
func isTerminated ( queryStatus string ) bool {
return queryStatus == "Complete" || queryStatus == "Cancelled" || queryStatus == "Failed" || queryStatus == "Timeout"
}