diff --git a/README.md b/README.md index 61c8dbe..f85ce2e 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ This was made as a complement to [CloudWatch Exporter](https://github.com/promet | VPC | routetablespervpc | Quota and usage of routetables per VPC | | VPC | routesperroutetable | Quota and usage of the routes per routetable | | VPC | ipv4blockspervpc | Quota and usage of ipv4 blocks per VPC | +| EC2 | transitgatewaysperregion | Quota and usage of transitgateways per region | | Route53 | recordsperhostedzone | Quota and usage of resource records per Hosted Zone | @@ -71,7 +72,13 @@ vpc: regions: - "us-east-1" - "eu-central-1" - - "eu-central-2" + timeout: 30s +ec2: + enabled: true + regions: + - "us-east-1" + - "eu-central-1" + - "us-west-1" timeout: 30s route53: enabled: true diff --git a/aws-resource-exporter-config.yaml b/aws-resource-exporter-config.yaml index f46d87f..744e4db 100644 --- a/aws-resource-exporter-config.yaml +++ b/aws-resource-exporter-config.yaml @@ -7,9 +7,15 @@ vpc: regions: - "us-east-1" - "eu-central-1" - - "eu-central-2" timeout: 30s route53: enabled: true region: "us-east-1" timeout: 60s +ec2: + enabled: true + regions: + - "us-east-1" + - "eu-central-1" + - "us-west-1" + timeout: 30s diff --git a/ec2.go b/ec2.go new file mode 100644 index 0000000..b977277 --- /dev/null +++ b/ec2.go @@ -0,0 +1,121 @@ +package main + +import ( + "context" + "sync" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/servicequotas" + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/prometheus/client_golang/prometheus" +) + +const ( + transitGatewayPerAccountQuotaCode string = "L-A2478D36" + ec2ServiceCode string = "ec2" +) + +var TransitGatewaysQuota *prometheus.Desc = prometheus.NewDesc(prometheus.BuildFQName(namespace, "", "ec2_transitgatewaysperregion_quota"), "Quota for maximum number of Transitgateways in this account", []string{"aws_region"}, nil) +var TransitGatewaysUsage *prometheus.Desc = prometheus.NewDesc(prometheus.BuildFQName(namespace, "", "ec2_transitgatewaysperregion_usage"), "Number of Tranitgatewyas in the AWS Account", []string{"aws_region"}, nil) + +type EC2Exporter struct { + sessions []*session.Session + + logger log.Logger + timeout time.Duration +} + +func NewEC2Exporter(sessions []*session.Session, logger log.Logger, timeout time.Duration) *EC2Exporter { + + level.Info(logger).Log("msg", "Initializing EC2 exporter") + return &EC2Exporter{ + sessions: sessions, + + logger: logger, + timeout: timeout, + } +} + +func (e *EC2Exporter) Collect(ch chan<- prometheus.Metric) { + ctx, ctxCancel := context.WithTimeout(context.Background(), e.timeout) + defer ctxCancel() + wg := &sync.WaitGroup{} + wg.Add(len(e.sessions)) + + for _, sess := range e.sessions { + go collectInRegion(sess, e.logger, wg, ch, ctx) + } + wg.Wait() +} + +func collectInRegion(sess *session.Session, logger log.Logger, wg *sync.WaitGroup, ch chan<- prometheus.Metric, ctx context.Context) { + defer wg.Done() + ec2Svc := ec2.New(sess) + serviceQuotaSvc := servicequotas.New(sess) + + quota, err := getQuotaValueWithContext(serviceQuotaSvc, ec2ServiceCode, transitGatewayPerAccountQuotaCode, ctx) + if err != nil { + level.Error(logger).Log("msg", "Could not retrieve Transit Gateway quota", "error", err.Error()) + exporterMetrics.IncrementErrors() + return + } + + gateways, err := getAllTransitGatewaysWithContext(ec2Svc, ctx) + if err != nil { + level.Error(logger).Log("msg", "Could not retrieve Transit Gateway quota", "error", err.Error()) + exporterMetrics.IncrementErrors() + return + } + + ch <- prometheus.MustNewConstMetric(TransitGatewaysUsage, prometheus.GaugeValue, float64(len(gateways)), *sess.Config.Region) + ch <- prometheus.MustNewConstMetric(TransitGatewaysQuota, prometheus.GaugeValue, quota, *sess.Config.Region) + +} + +func (e *EC2Exporter) Describe(ch chan<- *prometheus.Desc) { + ch <- TransitGatewaysQuota + ch <- TransitGatewaysUsage +} + +func getAllTransitGatewaysWithContext(client *ec2.EC2, ctx context.Context) ([]*ec2.TransitGateway, error) { + results := []*ec2.TransitGateway{} + describeGatewaysInput := &ec2.DescribeTransitGatewaysInput{ + DryRun: aws.Bool(false), + MaxResults: aws.Int64(1000), + } + + describeGatewaysOutput, err := client.DescribeTransitGatewaysWithContext(ctx, describeGatewaysInput) + + if err != nil { + return nil, err + } + results = append(results, describeGatewaysOutput.TransitGateways...) + + for describeGatewaysOutput.NextToken != nil { + describeGatewaysInput.SetNextToken(*describeGatewaysOutput.NextToken) + describeGatewaysOutput, err := client.DescribeTransitGatewaysWithContext(ctx, describeGatewaysInput) + if err != nil { + return nil, err + } + results = append(results, describeGatewaysOutput.TransitGateways...) + } + + return results, nil +} + +func getQuotaValueWithContext(client *servicequotas.ServiceQuotas, serviceCode string, quotaCode string, ctx context.Context) (float64, error) { + sqOutput, err := client.GetServiceQuotaWithContext(ctx, &servicequotas.GetServiceQuotaInput{ + QuotaCode: aws.String(quotaCode), + ServiceCode: aws.String(serviceCode), + }) + + if err != nil { + return 0, err + } + + return *sqOutput.Quota.Value, nil +} diff --git a/main.go b/main.go index 5f36a0d..78822af 100644 --- a/main.go +++ b/main.go @@ -63,10 +63,17 @@ type Route53Config struct { Region string `yaml:"region"` // Use only a single Region for now, as the current metric is global } +type EC2Config struct { + BaseConfig `yaml:"base,inline"` + Timeout time.Duration `yaml:"timeout"` + Regions []string `yaml:"regions"` +} + type Config struct { RdsConfig RDSConfig `yaml:"rds"` VpcConfig VPCConfig `yaml:"vpc"` Route53Config Route53Config `yaml:"route53"` + EC2Config EC2Config `yaml:"ec2"` } func loadExporterConfiguration(logger log.Logger, configFile string) (*Config, error) { @@ -88,6 +95,7 @@ func setupCollectors(logger log.Logger, configFile string, creds *credentials.Cr } level.Info(logger).Log("msg", "Configuring vpc with regions", "regions", strings.Join(config.VpcConfig.Regions, ",")) level.Info(logger).Log("msg", "Configuring rds with regions", "regions", strings.Join(config.RdsConfig.Regions, ",")) + level.Info(logger).Log("msg", "Configuring ec2 with regions", "regions", strings.Join(config.EC2Config.Regions, ",")) level.Info(logger).Log("msg", "Configuring route53 with region", "region", config.Route53Config.Region) var vpcSessions []*session.Session level.Info(logger).Log("msg", "Will VPC metrics be gathered?", "vpc-enabled", config.VpcConfig.Enabled) @@ -109,6 +117,16 @@ func setupCollectors(logger log.Logger, configFile string, creds *credentials.Cr } collectors = append(collectors, NewRDSExporter(rdsSessions, logger)) } + level.Info(logger).Log("msg", "Will EC2 metrics be gathered?", "ec2-enabled", config.EC2Config.Enabled) + var ec2Sessions []*session.Session + if config.EC2Config.Enabled { + for _, region := range config.EC2Config.Regions { + config := aws.NewConfig().WithCredentials(creds).WithRegion(region) + sess := session.Must(session.NewSession(config)) + ec2Sessions = append(ec2Sessions, sess) + } + collectors = append(collectors, NewEC2Exporter(ec2Sessions, logger, config.EC2Config.Timeout)) + } level.Info(logger).Log("msg", "Will Route53 metrics be gathered?", "route53-enabled", config.Route53Config.Enabled) if config.Route53Config.Enabled { awsConfig := aws.NewConfig().WithCredentials(creds).WithRegion(config.Route53Config.Region)