grafana/pkg/tsdb/cloudwatch/cloudwatch.go

317 lines
9.6 KiB
Go
Raw Normal View History

2017-04-03 07:50:40 -05:00
package cloudwatch
import (
"context"
"encoding/json"
"fmt"
"net/http"
2017-04-03 07:50:40 -05:00
"regexp"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
"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"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
ngalertmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/clients"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
2017-04-03 07:50:40 -05:00
)
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
}
type DataSource struct {
Settings models.CloudWatchSettings
HTTPClient *http.Client
}
const (
defaultRegion = "default"
logsQueryMode = "Logs"
// QueryTypes
annotationQuery = "annotationQuery"
logAction = "logAction"
timeSeriesQuery = "timeSeriesQuery"
)
var logger = log.New("tsdb.cloudwatch")
var aliasFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
func ProvideService(cfg *setting.Cfg, httpClientProvider httpclient.Provider, features featuremgmt.FeatureToggles) *CloudWatchService {
logger.Debug("Initializing")
executor := newExecutor(datasource.NewInstanceManager(NewInstanceSettings(httpClientProvider)), cfg, awsds.NewSessionCache(), features)
return &CloudWatchService{
Cfg: cfg,
Executor: executor,
}
}
type CloudWatchService struct {
Cfg *setting.Cfg
Executor *cloudWatchExecutor
}
type SessionCache interface {
GetSession(c awsds.SessionConfig) (*session.Session, error)
}
func newExecutor(im instancemgmt.InstanceManager, cfg *setting.Cfg, sessions SessionCache, features featuremgmt.FeatureToggles) *cloudWatchExecutor {
e := &cloudWatchExecutor{
im: im,
cfg: cfg,
sessions: sessions,
features: features,
}
e.resourceHandler = httpadapter.New(e.newResourceMux())
return e
}
func NewInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc {
return func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
instanceSettings, err := models.LoadCloudWatchSettings(settings)
if err != nil {
return nil, fmt.Errorf("error reading settings: %w", err)
}
httpClient, err := httpClientProvider.New()
if err != nil {
return nil, fmt.Errorf("error creating http client: %w", err)
}
return DataSource{
Settings: instanceSettings,
HTTPClient: httpClient,
}, nil
}
}
// cloudWatchExecutor executes CloudWatch requests.
type cloudWatchExecutor struct {
im instancemgmt.InstanceManager
cfg *setting.Cfg
sessions SessionCache
features featuremgmt.FeatureToggles
resourceHandler backend.CallResourceHandler
}
func (e *cloudWatchExecutor) getRequestContext(pluginCtx backend.PluginContext, region string) (models.RequestContext, error) {
r := region
instance, err := e.getInstance(pluginCtx)
if region == defaultRegion {
if err != nil {
return models.RequestContext{}, err
}
r = instance.Settings.Region
}
sess, err := e.newSession(pluginCtx, r)
if err != nil {
return models.RequestContext{}, err
}
return models.RequestContext{
OAMAPIProvider: NewOAMAPI(sess),
MetricsClientProvider: clients.NewMetricsClient(NewMetricsAPI(sess), e.cfg),
LogsAPIProvider: NewLogsAPI(sess),
Settings: instance.Settings,
Features: e.features,
}, nil
}
func (e *cloudWatchExecutor) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
return e.resourceHandler.CallResource(ctx, req, sender)
}
func (e *cloudWatchExecutor) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
logger := logger.FromContext(ctx)
/*
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.
*/
q := req.Queries[0]
var model DataQueryJson
err := json.Unmarshal(q.JSON, &model)
if err != nil {
return nil, err
}
_, fromAlert := req.Headers[ngalertmodels.FromAlertHeaderName]
isLogAlertQuery := fromAlert && model.QueryMode == logsQueryMode
if isLogAlertQuery {
return e.executeLogAlertQuery(ctx, req)
}
var result *backend.QueryDataResponse
switch model.QueryType {
case annotationQuery:
result, err = e.executeAnnotationQuery(req.PluginContext, model, q)
case logAction:
result, err = e.executeLogActions(ctx, logger, req)
case timeSeriesQuery:
fallthrough
default:
result, err = e.executeTimeSeriesQuery(ctx, logger, req)
}
return result, err
}
func (e *cloudWatchExecutor) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
status := backend.HealthStatusOk
metricsTest := "Successfully queried the CloudWatch metrics API."
logsTest := "Successfully queried the CloudWatch logs API."
err := e.checkHealthMetrics(req.PluginContext)
if err != nil {
status = backend.HealthStatusError
metricsTest = fmt.Sprintf("CloudWatch metrics query failed: %s", err.Error())
}
err = e.checkHealthLogs(req.PluginContext)
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
}
func (e *cloudWatchExecutor) checkHealthMetrics(pluginCtx backend.PluginContext) error {
namespace := "AWS/Billing"
metric := "EstimatedCharges"
params := &cloudwatch.ListMetricsInput{
Namespace: &namespace,
MetricName: &metric,
}
session, err := e.newSession(pluginCtx, defaultRegion)
if err != nil {
return err
}
metricClient := clients.NewMetricsClient(NewMetricsAPI(session), e.cfg)
_, err = metricClient.ListMetricsWithPageLimit(params)
return err
}
func (e *cloudWatchExecutor) checkHealthLogs(pluginCtx backend.PluginContext) error {
session, err := e.newSession(pluginCtx, defaultRegion)
if err != nil {
return err
}
logsClient := NewLogsAPI(session)
_, err = logsClient.DescribeLogGroups(&cloudwatchlogs.DescribeLogGroupsInput{Limit: aws.Int64(1)})
return err
}
func (e *cloudWatchExecutor) newSession(pluginCtx backend.PluginContext, region string) (*session.Session, error) {
instance, err := e.getInstance(pluginCtx)
if err != nil {
return nil, err
}
if region == defaultRegion {
region = instance.Settings.Region
}
CloudWatch: Re-implement authentication (#25548) * CloudWatch: Revisit authentication Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * CloudWatch: Simplify auth code Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Use ARN Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Add Drone configuration Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Remove unused code Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Remove .drone.yml Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Fix external ID usage Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * CloudWatch: Fix issues after merge Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Remove stale code Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Remove stale code Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Use auth type enum Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Fix test snapshot * Coordinate frontend and backend option names Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Remove old comments Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Fix front-end tests Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Introduce session cache Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Use constants Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Fix field alignment * CloudWatch: Fix log message Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Tidy go.mod Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * CloudWatch: Handle arn auth type Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * CloudWatch: Fix role assumption duration Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Fix test Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * CloudWatch: Inline unnecessary constants Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * CloudWatch: Use serial comma in UI Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * CloudWatch: Inline unnecessary constants Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * CloudWatch: Fail if missing region Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * CloudWatch: Handle unconfigured region Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * CloudWatch: Log when using cached session Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * CloudWatch: Include region in cache key Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Add UI warnings for lecagy support * Do not clear ARN fields whenging change authentication provider * Graph NG: annotations display (#27972) * Annotations support POC * Fix markers memoization * dev dashboard update * Update public/app/plugins/panel/graph3/plugins/AnnotationsPlugin.tsx * CloudWatch: Remove errors.BadRequest Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * CloudWatch: Undo unintentional change Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Remove log line Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Fix cache key computation Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Add region to cache key Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Improve log messages Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * CloudWatch: Add documentation Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Improve tooltip Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Improve docs Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Improve docs Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Improve docs Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Improve tooltip Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Add role assumption provisioning example Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Add upgrade notes Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Improve docs Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Apply suggestions from code review Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com> * backend: use latest sdk (#28147) fixes #27713 via https://github.com/grafana/grafana-plugin-sdk-go/pull/227 * Docs: Update Permissions documentation (#28144) * removed overview.md * content updates * Update datasource_permissions.md * update content * content updates * Update organization_roles.md * Update docs/sources/enterprise/saml.md Co-authored-by: Kyle Brandt <kyle@grafana.com> * Update dashboard_folder_permissions.md Co-authored-by: Kyle Brandt <kyle@grafana.com> * area/grafana/toolkit: ci-package needs to use synchronous writes (#28148) * ci needs to use synchronous writes or the file ends up with zero length * <Enterprise Docs> Add instructions to upload license via UI (#28067) * Add UI license upload option, reformat Enterprise license activation section Added the option to upload a license file through the Server Admin UI, and did a little reformatting to make license activation look more like a process. * Headers not bold, hyphens not asterisks * Github: run metrics collector workflow every 10min (#28153) * GithubActions: Updated cron schedule * Updated * Docs: Update explore docs: remove dot at the end of line (#28151) HI - Removed Dot(.) at the end of line to make it consistent with other 2 points. Thanks, Ashish * Fix frontend tests Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Fix frontend tests Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Docs: Update upgrade notes Co-authored-by: Sofia Papagiannaki <sofia@grafana.com> Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com> Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com> Co-authored-by: Kyle Brandt <kyle@grafana.com> Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> Co-authored-by: Brian Gann <briangann@users.noreply.github.com> Co-authored-by: Mitch Seaman <mjseaman@users.noreply.github.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.org> Co-authored-by: Torkel Ödegaard <torkel@grafana.com> Co-authored-by: ashishagarwal06 <34888589+ashishagarwal06@users.noreply.github.com>
2020-10-12 10:58:58 -05:00
return e.sessions.GetSession(awsds.SessionConfig{
// https://github.com/grafana/grafana/issues/46365
// HTTPClient: dsInfo.HTTPClient,
Settings: awsds.AWSDatasourceSettings{
Profile: instance.Settings.Profile,
Region: region,
AuthType: instance.Settings.AuthType,
AssumeRoleARN: instance.Settings.AssumeRoleARN,
ExternalID: instance.Settings.ExternalID,
Endpoint: instance.Settings.Endpoint,
DefaultRegion: instance.Settings.Region,
AccessKey: instance.Settings.AccessKey,
SecretKey: instance.Settings.SecretKey,
},
UserAgentName: aws.String("Cloudwatch"),
})
}
func (e *cloudWatchExecutor) getInstance(pluginCtx backend.PluginContext) (*DataSource, error) {
i, err := e.im.Get(pluginCtx)
if err != nil {
return nil, err
}
instance := i.(DataSource)
return &instance, nil
}
func (e *cloudWatchExecutor) getCWClient(pluginCtx backend.PluginContext, region string) (cloudwatchiface.CloudWatchAPI, error) {
sess, err := e.newSession(pluginCtx, region)
if err != nil {
return nil, err
}
return NewCWClient(sess), nil
2017-04-03 07:50:40 -05:00
}
func (e *cloudWatchExecutor) getCWLogsClient(pluginCtx backend.PluginContext, region string) (cloudwatchlogsiface.CloudWatchLogsAPI, error) {
sess, err := e.newSession(pluginCtx, region)
if err != nil {
return nil, err
}
logsClient := NewCWLogsClient(sess)
return logsClient, nil
}
func (e *cloudWatchExecutor) getEC2Client(pluginCtx backend.PluginContext, region string) (ec2iface.EC2API, error) {
sess, err := e.newSession(pluginCtx, region)
if err != nil {
return nil, err
}
2021-07-02 03:13:23 -05:00
return newEC2Client(sess), nil
}
func (e *cloudWatchExecutor) getRGTAClient(pluginCtx backend.PluginContext, region string) (resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI,
error) {
sess, err := e.newSession(pluginCtx, region)
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
}
func isTerminated(queryStatus string) bool {
return queryStatus == "Complete" || queryStatus == "Cancelled" || queryStatus == "Failed" || queryStatus == "Timeout"
}