Path: blob/main/pkg/integrations/cloudwatch_exporter/config.go
5376 views
package cloudwatch_exporter12import (3"crypto/md5"4"encoding/hex"5"fmt"6"time"78"github.com/go-kit/log"9yaceConf "github.com/nerdswords/yet-another-cloudwatch-exporter/pkg/config"10yaceModel "github.com/nerdswords/yet-another-cloudwatch-exporter/pkg/model"11"gopkg.in/yaml.v2"1213"github.com/grafana/agent/pkg/integrations"14integrations_v2 "github.com/grafana/agent/pkg/integrations/v2"15"github.com/grafana/agent/pkg/integrations/v2/metricsutils"16)1718const (19metricsPerQuery = 50020cloudWatchConcurrency = 521tagConcurrency = 522labelsSnakeCase = false23)2425// Since we are gathering metrics from CloudWatch and writing them in prometheus during each scrape, the timestamp26// used should be the scrape one27var addCloudwatchTimestamp = false2829// Avoid producing absence of values in metrics30var nilToZero = true3132func init() {33integrations.RegisterIntegration(&Config{})34integrations_v2.RegisterLegacy(&Config{}, integrations_v2.TypeMultiplex, metricsutils.NewNamedShim("cloudwatch"))35}3637// Config is the configuration for the CloudWatch metrics integration38type Config struct {39STSRegion string `yaml:"sts_region"`40FIPSDisabled bool `yaml:"fips_disabled"`41Discovery DiscoveryConfig `yaml:"discovery"`42Static []StaticJob `yaml:"static"`43}4445// DiscoveryConfig configures scraping jobs that will auto-discover metrics dimensions for a given service.46type DiscoveryConfig struct {47ExportedTags TagsPerNamespace `yaml:"exported_tags"`48Jobs []*DiscoveryJob `yaml:"jobs"`49}5051// TagsPerNamespace represents for each namespace, a list of tags that will be exported as labels in each metric.52type TagsPerNamespace map[string][]string5354// DiscoveryJob configures a discovery job for a given service.55type DiscoveryJob struct {56InlineRegionAndRoles `yaml:",inline"`57InlineCustomTags `yaml:",inline"`58SearchTags []Tag `yaml:"search_tags"`59Type string `yaml:"type"`60Metrics []Metric `yaml:"metrics"`61}6263// StaticJob will scrape metrics that match all defined dimensions.64type StaticJob struct {65InlineRegionAndRoles `yaml:",inline"`66InlineCustomTags `yaml:",inline"`67Name string `yaml:"name"`68Namespace string `yaml:"namespace"`69Dimensions []Dimension `yaml:"dimensions"`70Metrics []Metric `yaml:"metrics"`71}7273// InlineRegionAndRoles exposes for each supported job, the AWS regions and IAM roles in which the agent should perform the74// scrape.75type InlineRegionAndRoles struct {76Regions []string `yaml:"regions"`77Roles []Role `yaml:"roles"`78}7980type InlineCustomTags struct {81CustomTags []Tag `yaml:"custom_tags"`82}8384type Role struct {85RoleArn string `yaml:"role_arn"`86ExternalID string `yaml:"external_id"`87}8889type Dimension struct {90Name string `yaml:"name"`91Value string `yaml:"value"`92}9394type Tag struct {95Key string `yaml:"key"`96Value string `yaml:"value"`97}9899type Metric struct {100Name string `yaml:"name"`101Statistics []string `yaml:"statistics"`102Period time.Duration `yaml:"period"`103}104105// Name returns the name of the integration this config is for.106func (c *Config) Name() string {107return "cloudwatch_exporter"108}109110func (c *Config) InstanceKey(agentKey string) (string, error) {111return getHash(c)112}113114// NewIntegration creates a new integration from the config.115func (c *Config) NewIntegration(l log.Logger) (integrations.Integration, error) {116exporterConfig, fipsEnabled, err := ToYACEConfig(c)117if err != nil {118return nil, fmt.Errorf("invalid cloudwatch exporter configuration: %w", err)119}120return newCloudwatchExporter(c.Name(), l, exporterConfig, fipsEnabled), nil121}122123// getHash calculates the MD5 hash of the yaml representation of the config124func getHash(c *Config) (string, error) {125bytes, err := yaml.Marshal(c)126if err != nil {127return "", err128}129hash := md5.Sum(bytes)130return hex.EncodeToString(hash[:]), nil131}132133// ToYACEConfig converts a Config into YACE's config model. Note that the conversion is not direct, some values134// have been opinionated to simplify the config model the agent exposes for this integration.135// The returned boolean is whether or not AWS FIPS endpoints will be enabled.136func ToYACEConfig(c *Config) (yaceConf.ScrapeConf, bool, error) {137discoveryJobs := []*yaceConf.Job{}138for _, job := range c.Discovery.Jobs {139discoveryJobs = append(discoveryJobs, toYACEDiscoveryJob(job))140}141staticJobs := []*yaceConf.Static{}142for _, stat := range c.Static {143staticJobs = append(staticJobs, toYACEStaticJob(stat))144}145conf := yaceConf.ScrapeConf{146APIVersion: "v1alpha1",147StsRegion: c.STSRegion,148Discovery: yaceConf.Discovery{149ExportedTagsOnMetrics: yaceConf.ExportedTagsOnMetrics(c.Discovery.ExportedTags),150Jobs: discoveryJobs,151},152Static: staticJobs,153}154155// yaceSess expects a default value of True156fipsEnabled := !c.FIPSDisabled157158// Run the exporter's config validation. Between other things, it will check that the service for which a discovery159// job is instantiated, it's supported.160if err := conf.Validate(); err != nil {161return conf, fipsEnabled, err162}163patchYACEDefaults(&conf)164165return conf, fipsEnabled, nil166}167168// patchYACEDefaults overrides some default values YACE applies after validation.169func patchYACEDefaults(yc *yaceConf.ScrapeConf) {170// YACE doesn't allow during validation a zero-delay in each metrics scrape. Override this behaviour since it's taken171// into account by the rounding period.172// https://github.com/nerdswords/yet-another-cloudwatch-exporter/blob/7e5949124bb5f26353eeff298724a5897de2a2a4/pkg/config/config.go#L320173for _, job := range yc.Discovery.Jobs {174for _, metric := range job.Metrics {175metric.Delay = 0176}177}178}179180func toYACEStaticJob(job StaticJob) *yaceConf.Static {181return &yaceConf.Static{182Name: job.Name,183Regions: job.Regions,184Roles: toYACERoles(job.Roles),185Namespace: job.Namespace,186CustomTags: toYACETags(job.CustomTags),187Dimensions: toYACEDimensions(job.Dimensions),188Metrics: toYACEMetrics(job.Metrics),189}190}191192func toYACEDimensions(dim []Dimension) []yaceConf.Dimension {193yaceDims := []yaceConf.Dimension{}194for _, d := range dim {195yaceDims = append(yaceDims, yaceConf.Dimension{196Name: d.Name,197Value: d.Value,198})199}200return yaceDims201}202203func toYACEDiscoveryJob(job *DiscoveryJob) *yaceConf.Job {204roles := toYACERoles(job.Roles)205yaceJob := yaceConf.Job{206Regions: job.Regions,207Roles: roles,208CustomTags: toYACETags(job.CustomTags),209Type: job.Type,210Metrics: toYACEMetrics(job.Metrics),211SearchTags: toYACETags(job.SearchTags),212213// By setting RoundingPeriod to nil, the exporter will align the start and end times for retrieving CloudWatch214// metrics, with the smallest period in the retrieved batch.215RoundingPeriod: nil,216217JobLevelMetricFields: yaceConf.JobLevelMetricFields{218// Set to zero job-wide scraping time settings. This should be configured at the metric level to make the data219// being fetched more explicit.220Period: 0,221Length: 0,222Delay: 0,223NilToZero: &nilToZero,224AddCloudwatchTimestamp: &addCloudwatchTimestamp,225},226}227return &yaceJob228}229230func toYACEMetrics(metrics []Metric) []*yaceConf.Metric {231yaceMetrics := []*yaceConf.Metric{}232for _, metric := range metrics {233periodSeconds := int64(metric.Period.Seconds())234lengthSeconds := periodSeconds235yaceMetrics = append(yaceMetrics, &yaceConf.Metric{236Name: metric.Name,237Statistics: metric.Statistics,238239// Length dictates the size of the window for whom we request metrics, that is, endTime - startTime. Period240// dictates the size of the buckets in which we aggregate data, inside that window. Since data will be scraped241// by the agent every so often, dictated by the scrapedInterval, CloudWatch should return a single datapoint242// for each requested metric. That is if Period >= Length, but is Period > Length, we will be getting not enough243// data to fill the whole aggregation bucket. Therefore, Period == Length.244Period: periodSeconds,245Length: lengthSeconds,246247// Delay moves back the time window for whom CloudWatch is requested data. Since we are already adjusting248// this with RoundingPeriod (see toYACEDiscoveryJob), we should omit this setting.249Delay: 0,250251NilToZero: &nilToZero,252AddCloudwatchTimestamp: &addCloudwatchTimestamp,253})254}255return yaceMetrics256}257258func toYACERoles(roles []Role) []yaceConf.Role {259yaceRoles := []yaceConf.Role{}260// YACE defaults to an empty role, which means the environment configured role is used261// https://github.com/nerdswords/yet-another-cloudwatch-exporter/blob/30aeceb2324763cdd024a1311045f83a09c1df36/pkg/config/config.go#L111262if len(roles) == 0 {263yaceRoles = append(yaceRoles, yaceConf.Role{})264}265for _, role := range roles {266yaceRoles = append(yaceRoles, yaceConf.Role{267RoleArn: role.RoleArn,268ExternalID: role.ExternalID,269})270}271return yaceRoles272}273274func toYACETags(tags []Tag) []yaceModel.Tag {275outTags := []yaceModel.Tag{}276for _, t := range tags {277outTags = append(outTags, yaceModel.Tag{278Key: t.Key,279Value: t.Value,280})281}282return outTags283}284285286