diff --git a/x-pack/metricbeat/module/aws/aws.go b/x-pack/metricbeat/module/aws/aws.go index 9b2f23cc43c..d120d8d0654 100644 --- a/x-pack/metricbeat/module/aws/aws.go +++ b/x-pack/metricbeat/module/aws/aws.go @@ -81,6 +81,11 @@ func NewMetricSet(base mb.BaseMetricSet) (*MetricSet, error) { } // Calculate duration based on period + if config.Period == "" { + err = errors.New("period is not set in AWS module config") + return nil, err + } + durationString, periodSec, err := convertPeriodToDuration(config.Period) if err != nil { return nil, err diff --git a/x-pack/metricbeat/module/aws/ec2/_meta/data.json b/x-pack/metricbeat/module/aws/ec2/_meta/data.json index d753f58ef13..eba6c8d66cd 100644 --- a/x-pack/metricbeat/module/aws/ec2/_meta/data.json +++ b/x-pack/metricbeat/module/aws/ec2/_meta/data.json @@ -7,12 +7,12 @@ "aws": { "ec2": { "cpu": { - "credit_balance": 169.089216, - "credit_usage": 0.002926, + "credit_balance": 576, + "credit_usage": 0.144723, "surplus_credit_balance": 0, "surplus_credits_charged": 0, "total": { - "pct": 0.033333333333322 + "pct": 1.366194313233248 } }, "diskio": { @@ -27,21 +27,21 @@ }, "instance": { "core": { - "count": 1 + "count": 2 }, "image": { - "id": "ami-01e24be29428c15b2" + "id": "ami-f920cd94" }, "monitoring": { "state": "disabled" }, "private": { - "dns_name": "ip-172-31-26-12.us-west-2.compute.internal", - "ip": "172.31.26.12" + "dns_name": "ip-10-0-0-148.ec2.internal", + "ip": "10.0.0.148" }, "public": { - "dns_name": "ec2-34-217-213-210.us-west-2.compute.amazonaws.com", - "ip": "34.217.213.210" + "dns_name": "ec2-54-226-109-162.compute-1.amazonaws.com", + "ip": "54.226.109.162" }, "state": { "code": 16, @@ -51,12 +51,12 @@ }, "network": { "in": { - "bytes": 296.6, - "packets": 3 + "bytes": 737000.4, + "packets": 1361.2 }, "out": { - "bytes": 269, - "packets": 3.6 + "bytes": 227871.2, + "packets": 1411.2 } }, "status": { @@ -67,15 +67,15 @@ } }, "cloud": { - "availability_zone": "us-west-2a", + "availability_zone": "us-east-1b", "instance": { - "id": "i-077bdaf7e5d81bba3" + "id": "i-77f84332" }, "machine": { - "type": "t2.micro" + "type": "t2.medium" }, "provider": "ec2", - "region": "us-west-2" + "region": "us-east-1" }, "event": { "dataset": "aws.ec2", diff --git a/x-pack/metricbeat/module/aws/ec2/data.go b/x-pack/metricbeat/module/aws/ec2/data.go index e73e8309c1f..12599b32d87 100644 --- a/x-pack/metricbeat/module/aws/ec2/data.go +++ b/x-pack/metricbeat/module/aws/ec2/data.go @@ -5,7 +5,6 @@ package ec2 import ( - "github.com/elastic/beats/libbeat/common" s "github.com/elastic/beats/libbeat/common/schema" c "github.com/elastic/beats/libbeat/common/schema/mapstrstr" ) @@ -14,85 +13,37 @@ var ( schemaMetricSetFields = s.Schema{ "cpu": s.Object{ "total": s.Object{ - "pct": c.Float("cpu.total.pct", s.Optional), + "pct": c.Float("CPUUtilization"), }, - "credit_usage": c.Float("cpu.credit_usage", s.Optional), - "credit_balance": c.Float("cpu.credit_balance", s.Optional), - "surplus_credit_balance": c.Float("cpu.surplus_credit_balance", s.Optional), - "surplus_credits_charged": c.Float("cpu.surplus_credits_charged", s.Optional), + "credit_usage": c.Float("CPUCreditUsage"), + "credit_balance": c.Float("CPUCreditBalance"), + "surplus_credit_balance": c.Float("CPUSurplusCreditBalance"), + "surplus_credits_charged": c.Float("CPUSurplusCreditsCharged"), }, "diskio": s.Object{ "read": s.Object{ - "bytes": c.Float("diskio.read.bytes", s.Optional), - "count": c.Float("diskio.read.count", s.Optional), + "bytes": c.Float("DiskReadBytes"), + "count": c.Float("DiskReadOps"), }, "write": s.Object{ - "bytes": c.Float("diskio.write.bytes", s.Optional), - "count": c.Float("diskio.write.count", s.Optional), + "bytes": c.Float("DiskWriteBytes"), + "count": c.Float("DiskWriteOps"), }, }, "network": s.Object{ "in": s.Object{ - "bytes": c.Float("network.in.bytes", s.Optional), - "packets": c.Float("network.in.packets", s.Optional), + "bytes": c.Float("NetworkIn"), + "packets": c.Float("NetworkPacketsIn"), }, "out": s.Object{ - "bytes": c.Float("network.out.bytes", s.Optional), - "packets": c.Float("network.out.packets", s.Optional), + "bytes": c.Float("NetworkOut"), + "packets": c.Float("NetworkPacketsOut"), }, }, "status": s.Object{ - "check_failed": c.Int("status.check_failed", s.Optional), - "check_failed_instance": c.Int("status.check_failed_instance", s.Optional), - "check_failed_system": c.Int("status.check_failed_system", s.Optional), - }, - "instance": s.Object{ - "image": s.Object{ - "id": c.Str("instance.image.id", s.Optional), - }, - "state": s.Object{ - "name": c.Str("instance.state.name", s.Optional), - "code": c.Int("instance.state.code", s.Optional), - }, - "monitoring": s.Object{ - "state": c.Str("instance.monitoring.state", s.Optional), - }, - "core": s.Object{ - "count": c.Int("instance.core.count", s.Optional), - }, - "threads_per_core": c.Int("instance.threads_per_core", s.Optional), - "public": s.Object{ - "ip": c.Str("instance.public.ip", s.Optional), - "dns_name": c.Str("instance.public.dns_name", s.Optional), - }, - "private": s.Object{ - "ip": c.Str("instance.private.ip", s.Optional), - "dns_name": c.Str("instance.private.dns_name", s.Optional), - }, - }, - } -) - -var ( - schemaRootFields = s.Schema{ - "service": s.Object{ - "name": c.Str("service.name", s.Optional), - }, - "cloud": s.Object{ - "provider": c.Str("cloud.provider", s.Optional), - "availability_zone": c.Str("cloud.availability_zone", s.Optional), - "region": c.Str("cloud.region", s.Optional), - "instance": s.Object{ - "id": c.Str("cloud.instance.id", s.Optional), - "name": c.Str("cloud.instance.name", s.Optional), - }, - "machine": s.Object{ - "type": c.Str("cloud.machine.type", s.Optional), - }, + "check_failed": c.Int("StatusCheckFailed"), + "check_failed_instance": c.Int("StatusCheckFailed_Instance"), + "check_failed_system": c.Int("StatusCheckFailed_System"), }, } ) - -func eventMapping(input map[string]interface{}, schema s.Schema) (common.MapStr, error) { - return schema.Apply(input) -} diff --git a/x-pack/metricbeat/module/aws/ec2/ec2.go b/x-pack/metricbeat/module/aws/ec2/ec2.go index a4c0bf18b62..96b459cc6f2 100644 --- a/x-pack/metricbeat/module/aws/ec2/ec2.go +++ b/x-pack/metricbeat/module/aws/ec2/ec2.go @@ -6,10 +6,10 @@ package ec2 import ( "fmt" - "time" + "strconv" + "strings" "github.com/aws/aws-sdk-go-v2/service/cloudwatch" - "github.com/aws/aws-sdk-go-v2/service/cloudwatch/cloudwatchiface" "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/ec2iface" "github.com/pkg/errors" @@ -41,42 +41,10 @@ type MetricSet struct { logger *logp.Logger } -// metricIDNameMap is a translating map between createMetricDataQuery id -// and aws ec2 module metric name, cloudwatch ec2 metric name. -var metricIDNameMap = map[string][]string{ - "cpu1": {"cpu.total.pct", "CPUUtilization"}, - "cpu2": {"cpu.credit_usage", "CPUCreditUsage"}, - "cpu3": {"cpu.credit_balance", "CPUCreditBalance"}, - "cpu4": {"cpu.surplus_credit_balance", "CPUSurplusCreditBalance"}, - "cpu5": {"cpu.surplus_credits_charged", "CPUSurplusCreditsCharged"}, - "network1": {"network.in.packets", "NetworkPacketsIn"}, - "network2": {"network.out.packets", "NetworkPacketsOut"}, - "network3": {"network.in.bytes", "NetworkIn"}, - "network4": {"network.out.bytes", "NetworkOut"}, - "disk1": {"diskio.read.bytes", "DiskReadBytes"}, - "disk2": {"diskio.write.bytes", "DiskWriteBytes"}, - "disk3": {"diskio.read.count", "DiskReadOps"}, - "disk4": {"diskio.write.count", "DiskWriteOps"}, - "status1": {"status.check_failed", "StatusCheckFailed"}, - "status2": {"status.check_failed_system", "StatusCheckFailed_System"}, - "status3": {"status.check_failed_instance", "StatusCheckFailed_Instance"}, -} - // New creates a new instance of the MetricSet. New is responsible for unpacking // any MetricSet specific configuration options if there are any. func New(base mb.BaseMetricSet) (mb.MetricSet, error) { ec2Logger := logp.NewLogger(aws.ModuleName) - - moduleConfig := aws.Config{} - if err := base.Module().UnpackConfig(&moduleConfig); err != nil { - return nil, err - } - - if moduleConfig.Period == "" { - err := errors.New("period is not set in AWS module config") - ec2Logger.Error(err) - } - metricSet, err := aws.NewMetricSet(base) if err != nil { return nil, errors.Wrap(err, "error creating aws metricset") @@ -102,6 +70,14 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) { // format. It publishes the event which is then forwarded to the output. In case // of an error set the Error field of mb.Event or simply call report.Error(). func (m *MetricSet) Fetch(report mb.ReporterV2) { + // Get startTime and endTime + startTime, endTime, err := aws.GetStartTimeEndTime(m.DurationString) + if err != nil { + m.logger.Error(errors.Wrap(err, "Error ParseDuration")) + report.Error(err) + return + } + for _, regionName := range m.MetricSet.RegionsList { m.MetricSet.AwsConfig.Region = regionName svcEC2 := ec2.New(*m.MetricSet.AwsConfig) @@ -114,30 +90,46 @@ func (m *MetricSet) Fetch(report mb.ReporterV2) { } svcCloudwatch := cloudwatch.New(*m.MetricSet.AwsConfig) + namespace := "AWS/EC2" + listMetricsOutput, err := aws.GetListMetricsOutput(namespace, regionName, svcCloudwatch) + if err != nil { + m.logger.Error(err.Error()) + report.Error(err) + continue + } + + if listMetricsOutput == nil || len(listMetricsOutput) == 0 { + continue + } + for _, instanceID := range instanceIDs { - init := true - getMetricDataOutput := &cloudwatch.GetMetricDataOutput{NextToken: nil} - for init || getMetricDataOutput.NextToken != nil { - init = false - output, err := getMetricDataPerRegion(m.MetricSet.DurationString, m.MetricSet.PeriodInSec, instanceID, getMetricDataOutput.NextToken, svcCloudwatch) - if err != nil { - err = errors.Wrap(err, "getMetricDataPerRegion failed, skipping region "+regionName+" for instance "+instanceID) - m.logger.Error(err.Error()) - report.Error(err) - continue - } - getMetricDataOutput.MetricDataResults = append(getMetricDataOutput.MetricDataResults, output.MetricDataResults...) + if instanceID != "i-77f84332" { + continue + } + + metricDataQueries := constructMetricQueries(listMetricsOutput, instanceID, m.PeriodInSec) + if len(metricDataQueries) == 0 { + continue } - event, info, err := createCloudWatchEvents(getMetricDataOutput, instanceID, instancesOutputs[instanceID], regionName) + // Use metricDataQueries to make GetMetricData API calls + metricDataOutput, err := aws.GetMetricDataResults(metricDataQueries, svcCloudwatch, startTime, endTime) + if err != nil { + err = errors.Wrap(err, "GetMetricDataResults failed, skipping region "+regionName+" for instance "+instanceID) + m.logger.Error(err.Error()) + report.Error(err) + continue + } + + // Create Cloudwatch Events for EC2 + event, info, err := createCloudWatchEvents(metricDataOutput, instanceID, instancesOutputs[instanceID], regionName) if info != "" { m.logger.Info(info) } if err != nil { m.logger.Error(err.Error()) - event.Error = err - report.Event(event) + report.Error(err) continue } report.Event(event) @@ -145,34 +137,57 @@ func (m *MetricSet) Fetch(report mb.ReporterV2) { } } -func createCloudWatchEvents(getMetricDataOutput *cloudwatch.GetMetricDataOutput, instanceID string, instanceOutput ec2.Instance, regionName string) (event mb.Event, info string, err error) { +func constructMetricQueries(listMetricsOutput []cloudwatch.Metric, instanceID string, periodInSec int) []cloudwatch.MetricDataQuery { + metricDataQueries := []cloudwatch.MetricDataQuery{} + metricDataQueryEmpty := cloudwatch.MetricDataQuery{} + for i, listMetric := range listMetricsOutput { + metricDataQuery := createMetricDataQuery(listMetric, instanceID, i, periodInSec) + if metricDataQuery == metricDataQueryEmpty { + continue + } + metricDataQueries = append(metricDataQueries, metricDataQuery) + } + return metricDataQueries +} + +func createCloudWatchEvents(getMetricDataResults []cloudwatch.MetricDataResult, instanceID string, instanceOutput ec2.Instance, regionName string) (event mb.Event, info string, err error) { event.Service = metricsetName event.RootFields = common.MapStr{} - mapOfRootFieldsResults := make(map[string]interface{}) - mapOfRootFieldsResults["service.name"] = metricsetName - // Cloud fields in ECS - mapOfRootFieldsResults["cloud.provider"] = metricsetName - mapOfRootFieldsResults["cloud.availability_zone"] = *instanceOutput.Placement.AvailabilityZone - mapOfRootFieldsResults["cloud.region"] = regionName - mapOfRootFieldsResults["cloud.instance.id"] = instanceID machineType, err := instanceOutput.InstanceType.MarshalValue() if err != nil { err = errors.Wrap(err, "instance.InstanceType.MarshalValue failed") return } - mapOfRootFieldsResults["cloud.machine.type"] = machineType - resultRootFields, err := eventMapping(mapOfRootFieldsResults, schemaRootFields) + event.RootFields.Put("service.name", metricsetName) + event.RootFields.Put("cloud.provider", metricsetName) + event.RootFields.Put("cloud.availability_zone", *instanceOutput.Placement.AvailabilityZone) + event.RootFields.Put("cloud.region", regionName) + event.RootFields.Put("cloud.instance.id", instanceID) + event.RootFields.Put("cloud.machine.type", machineType) + + // AWS EC2 Metrics + mapOfMetricSetFieldResults := make(map[string]interface{}) + for _, output := range getMetricDataResults { + if len(output.Values) == 0 { + continue + } + labels := strings.Split(*output.Label, " ") + mapOfMetricSetFieldResults[labels[1]] = fmt.Sprint(output.Values[0]) + } + + resultMetricSetFields, err := aws.EventMapping(mapOfMetricSetFieldResults, schemaMetricSetFields) if err != nil { - err = errors.Wrap(err, "Error trying to apply schema schemaRootFields in AWS EC2 metricbeat module.") + err = errors.Wrap(err, "Error trying to apply schema schemaMetricSetFields in AWS EC2 metricbeat module.") return } - event.RootFields = resultRootFields - // AWS EC2 Metrics - mapOfMetricSetFieldResults := make(map[string]interface{}) - mapOfMetricSetFieldResults["instance.image.id"] = *instanceOutput.ImageId + if len(mapOfMetricSetFieldResults) <= 11 { + info = "Missing Cloudwatch data for instance " + instanceID + ". This is expected for a new instance during the " + + "first data collection. If this shows up multiple times, please recheck the period setting in config." + } + instanceStateName, err := instanceOutput.State.Name.MarshalValue() if err != nil { err = errors.Wrap(err, "instance.State.Name.MarshalValue failed") @@ -185,42 +200,23 @@ func createCloudWatchEvents(getMetricDataOutput *cloudwatch.GetMetricDataOutput, return } - mapOfMetricSetFieldResults["instance.state.name"] = instanceStateName - mapOfMetricSetFieldResults["instance.state.code"] = fmt.Sprint(*instanceOutput.State.Code) - mapOfMetricSetFieldResults["instance.monitoring.state"] = monitoringState - mapOfMetricSetFieldResults["instance.core.count"] = fmt.Sprint(*instanceOutput.CpuOptions.CoreCount) - mapOfMetricSetFieldResults["instance.threads_per_core"] = fmt.Sprint(*instanceOutput.CpuOptions.ThreadsPerCore) + resultMetricSetFields.Put("instance.image.id", *instanceOutput.ImageId) + resultMetricSetFields.Put("instance.state.name", instanceStateName) + resultMetricSetFields.Put("instance.state.code", *instanceOutput.State.Code) + resultMetricSetFields.Put("instance.monitoring.state", monitoringState) + resultMetricSetFields.Put("instance.core.count", *instanceOutput.CpuOptions.CoreCount) + resultMetricSetFields.Put("instance.threads_per_core", *instanceOutput.CpuOptions.ThreadsPerCore) publicIP := instanceOutput.PublicIpAddress if publicIP != nil { - mapOfMetricSetFieldResults["instance.public.ip"] = *publicIP + resultMetricSetFields.Put("instance.public.ip", *publicIP) } - - mapOfMetricSetFieldResults["instance.public.dns_name"] = *instanceOutput.PublicDnsName - mapOfMetricSetFieldResults["instance.private.dns_name"] = *instanceOutput.PrivateDnsName + resultMetricSetFields.Put("instance.public.dns_name", *instanceOutput.PublicDnsName) + resultMetricSetFields.Put("instance.private.dns_name", *instanceOutput.PrivateDnsName) privateIP := instanceOutput.PrivateIpAddress if privateIP != nil { - mapOfMetricSetFieldResults["instance.private.ip"] = *privateIP - } - - for _, output := range getMetricDataOutput.MetricDataResults { - if len(output.Values) == 0 { - continue - } - metricKey := metricIDNameMap[*output.Id] - mapOfMetricSetFieldResults[metricKey[0]] = fmt.Sprint(output.Values[0]) + resultMetricSetFields.Put("instance.private.ip", *privateIP) } - if len(mapOfMetricSetFieldResults) <= 11 { - info = "Missing Cloudwatch data for instance " + instanceID + ". This is expected for a new instance during the " + - "first data collection. If this shows up multiple times, please recheck the period setting in config." - return - } - - resultMetricSetFields, err := eventMapping(mapOfMetricSetFieldResults, schemaMetricSetFields) - if err != nil { - err = errors.Wrap(err, "Error trying to apply schema schemaMetricSetFields in AWS EC2 metricbeat module.") - return - } event.MetricSetFields = resultMetricSetFields return } @@ -249,62 +245,27 @@ func getInstancesPerRegion(svc ec2iface.EC2API) (instanceIDs []string, instances return } -func getMetricDataPerRegion(durationString string, periodInSec int, instanceID string, nextToken *string, svc cloudwatchiface.CloudWatchAPI) (*cloudwatch.GetMetricDataOutput, error) { - endTime := time.Now() - duration, err := time.ParseDuration(durationString) - if err != nil { - logp.Error(errors.Wrap(err, "Error ParseDuration")) - return nil, err - } - - startTime := endTime.Add(duration) - - dimName := "InstanceId" - dim := cloudwatch.Dimension{ - Name: &dimName, - Value: &instanceID, - } - - metricDataQueries := []cloudwatch.MetricDataQuery{} - for metricID, metricName := range metricIDNameMap { - metricDataQuery := createMetricDataQuery(metricID, metricName[1], periodInSec, []cloudwatch.Dimension{dim}) - metricDataQueries = append(metricDataQueries, metricDataQuery) - } - - getMetricDataInput := &cloudwatch.GetMetricDataInput{ - NextToken: nextToken, - StartTime: &startTime, - EndTime: &endTime, - MetricDataQueries: metricDataQueries, - } - - req := svc.GetMetricDataRequest(getMetricDataInput) - getMetricDataOutput, err := req.Send() - if err != nil { - logp.Error(errors.Wrap(err, "Error GetMetricDataInput")) - return nil, err - } - return getMetricDataOutput, nil -} - -func createMetricDataQuery(id string, metricName string, periodInSec int, dimensions []cloudwatch.Dimension) (metricDataQuery cloudwatch.MetricDataQuery) { - namespace := "AWS/EC2" +func createMetricDataQuery(metric cloudwatch.Metric, instanceID string, index int, periodInSec int) (metricDataQuery cloudwatch.MetricDataQuery) { statistic := "Average" period := int64(periodInSec) - - metric := cloudwatch.Metric{ - Namespace: &namespace, - MetricName: &metricName, - Dimensions: dimensions, - } - - metricDataQuery = cloudwatch.MetricDataQuery{ - Id: &id, - MetricStat: &cloudwatch.MetricStat{ - Period: &period, - Stat: &statistic, - Metric: &metric, - }, + id := "e" + strconv.Itoa(index) + metricDims := metric.Dimensions + + for _, dim := range metricDims { + if *dim.Name == "InstanceId" && *dim.Value == instanceID { + metricName := *metric.MetricName + label := instanceID + " " + metricName + metricDataQuery = cloudwatch.MetricDataQuery{ + Id: &id, + MetricStat: &cloudwatch.MetricStat{ + Period: &period, + Stat: &statistic, + Metric: &metric, + }, + Label: &label, + } + return + } } return } diff --git a/x-pack/metricbeat/module/aws/ec2/ec2_integration_test.go b/x-pack/metricbeat/module/aws/ec2/ec2_integration_test.go index 1c75f450886..6525e3f3b7c 100644 --- a/x-pack/metricbeat/module/aws/ec2/ec2_integration_test.go +++ b/x-pack/metricbeat/module/aws/ec2/ec2_integration_test.go @@ -2,116 +2,75 @@ // or more contributor license agreements. Licensed under the Elastic License; // you may not use this file except in compliance with the Elastic License. -// +build integration - package ec2 import ( - "os" "testing" "github.com/stretchr/testify/assert" - "github.com/elastic/beats/metricbeat/mb" mbtest "github.com/elastic/beats/metricbeat/mb/testing" + "github.com/elastic/beats/x-pack/metricbeat/module/aws/mtest" ) func TestFetch(t *testing.T) { - accessKeyID, okAccessKeyID := os.LookupEnv("AWS_ACCESS_KEY_ID") - secretAccessKey, okSecretAccessKey := os.LookupEnv("AWS_SECRET_ACCESS_KEY") - sessionToken, okSessionToken := os.LookupEnv("AWS_SESSION_TOKEN") - defaultRegion, _ := os.LookupEnv("AWS_REGION") - - if !okAccessKeyID || accessKeyID == "" { - t.Skip("Skipping TestFetch; $AWS_ACCESS_KEY_ID not set or set to empty") - } else if !okSecretAccessKey || secretAccessKey == "" { - t.Skip("Skipping TestFetch; $AWS_SECRET_ACCESS_KEY not set or set to empty") - } else { - tempCreds := map[string]interface{}{ - "module": "aws", - "period": "300s", - "metricsets": []string{"ec2"}, - "access_key_id": accessKeyID, - "secret_access_key": secretAccessKey, - "default_region": defaultRegion, - } - - if okSessionToken && sessionToken != "" { - tempCreds["session_token"] = sessionToken - } - - ec2MetricSet := mbtest.NewReportingMetricSetV2(t, tempCreds) - events, errs := mbtest.ReportingFetchV2(ec2MetricSet) - if errs != nil { - t.Skip("Skipping TestFetch: failed to make api calls. Please check $AWS_ACCESS_KEY_ID, " + - "$AWS_SECRET_ACCESS_KEY and $AWS_SESSION_TOKEN in config.yml") - } + config, info := mtest.GetConfigForTest("ec2") + if info != "" { + t.Skip("Skipping TestFetch: " + info) + } - assert.Empty(t, errs) - if !assert.NotEmpty(t, events) { - t.FailNow() - } - t.Logf("Module: %s Metricset: %s", ec2MetricSet.Module().Name(), ec2MetricSet.Name()) + ec2MetricSet := mbtest.NewReportingMetricSetV2(t, config) + events, errs := mbtest.ReportingFetchV2(ec2MetricSet) + if errs != nil { + t.Skip("Skipping TestFetch: failed to make api calls. Please check $AWS_ACCESS_KEY_ID, " + + "$AWS_SECRET_ACCESS_KEY and $AWS_SESSION_TOKEN in config.yml") + } - for _, event := range events { - // RootField - checkEventField("service.name", "string", event, t) - checkEventField("cloud.availability_zone", "string", event, t) - checkEventField("cloud.provider", "string", event, t) - checkEventField("cloud.image.id", "string", event, t) - checkEventField("cloud.instance.id", "string", event, t) - checkEventField("cloud.machine.type", "string", event, t) - checkEventField("cloud.provider", "string", event, t) - checkEventField("cloud.region", "string", event, t) - // MetricSetField - checkEventField("cpu.total.pct", "float", event, t) - checkEventField("cpu.credit_usage", "float", event, t) - checkEventField("cpu.credit_balance", "float", event, t) - checkEventField("cpu.surplus_credit_balance", "float", event, t) - checkEventField("cpu.surplus_credits_charged", "float", event, t) - checkEventField("network.in.packets", "float", event, t) - checkEventField("network.out.packets", "float", event, t) - checkEventField("network.in.bytes", "float", event, t) - checkEventField("network.out.bytes", "float", event, t) - checkEventField("diskio.read.bytes", "float", event, t) - checkEventField("diskio.write.bytes", "float", event, t) - checkEventField("diskio.read.ops", "float", event, t) - checkEventField("diskio.write.ops", "float", event, t) - checkEventField("status.check_failed", "int", event, t) - checkEventField("status.check_failed_system", "int", event, t) - checkEventField("status.check_failed_instance", "int", event, t) - } + assert.Empty(t, errs) + if !assert.NotEmpty(t, events) { + t.FailNow() + } + t.Logf("Module: %s Metricset: %s", ec2MetricSet.Module().Name(), ec2MetricSet.Name()) - err := mbtest.WriteEventsReporterV2(ec2MetricSet, t, "/") - if err != nil { - t.Fatal("write", err) - } + for _, event := range events { + // RootField + mtest.CheckEventField("service.name", "string", event, t) + mtest.CheckEventField("cloud.availability_zone", "string", event, t) + mtest.CheckEventField("cloud.provider", "string", event, t) + mtest.CheckEventField("cloud.image.id", "string", event, t) + mtest.CheckEventField("cloud.instance.id", "string", event, t) + mtest.CheckEventField("cloud.machine.type", "string", event, t) + mtest.CheckEventField("cloud.provider", "string", event, t) + mtest.CheckEventField("cloud.region", "string", event, t) + // MetricSetField + mtest.CheckEventField("cpu.total.pct", "float", event, t) + mtest.CheckEventField("cpu.credit_usage", "float", event, t) + mtest.CheckEventField("cpu.credit_balance", "float", event, t) + mtest.CheckEventField("cpu.surplus_credit_balance", "float", event, t) + mtest.CheckEventField("cpu.surplus_credits_charged", "float", event, t) + mtest.CheckEventField("network.in.packets", "float", event, t) + mtest.CheckEventField("network.out.packets", "float", event, t) + mtest.CheckEventField("network.in.bytes", "float", event, t) + mtest.CheckEventField("network.out.bytes", "float", event, t) + mtest.CheckEventField("diskio.read.bytes", "float", event, t) + mtest.CheckEventField("diskio.write.bytes", "float", event, t) + mtest.CheckEventField("diskio.read.ops", "float", event, t) + mtest.CheckEventField("diskio.write.ops", "float", event, t) + mtest.CheckEventField("status.check_failed", "int", event, t) + mtest.CheckEventField("status.check_failed_system", "int", event, t) + mtest.CheckEventField("status.check_failed_instance", "int", event, t) } } -func checkEventField(metricName string, expectedType string, event mb.Event, t *testing.T) { - if ok, err := event.MetricSetFields.HasKey(metricName); ok { - assert.NoError(t, err) - metricValue, err := event.MetricSetFields.GetValue(metricName) - assert.NoError(t, err) +func TestData(t *testing.T) { + config, info := mtest.GetConfigForTest("ec2") + if info != "" { + t.Skip("Skipping TestData: " + info) + } - switch metricValue.(type) { - case float64: - if expectedType != "float" { - t.Log("Failed: Field " + metricName + " is not in type " + expectedType) - t.Fail() - } - case string: - if expectedType != "string" { - t.Log("Failed: Field " + metricName + " is not in type " + expectedType) - t.Fail() - } - case int64: - if expectedType != "int" { - t.Log("Failed: Field " + metricName + " is not in type " + expectedType) - t.Fail() - } - } - t.Log("Succeed: Field " + metricName + " matches type " + expectedType) + ec2MetricSet := mbtest.NewReportingMetricSetV2(t, config) + errs := mbtest.WriteEventsReporterV2(ec2MetricSet, t, "/") + if errs != nil { + t.Fatal("write", errs) } } diff --git a/x-pack/metricbeat/module/aws/ec2/ec2_test.go b/x-pack/metricbeat/module/aws/ec2/ec2_test.go index 3353dedaf15..a24f5326206 100644 --- a/x-pack/metricbeat/module/aws/ec2/ec2_test.go +++ b/x-pack/metricbeat/module/aws/ec2/ec2_test.go @@ -7,7 +7,6 @@ package ec2 import ( - "fmt" "testing" awssdk "github.com/aws/aws-sdk-go-v2/aws" @@ -33,7 +32,27 @@ type MockCloudWatchClient struct { cloudwatchiface.CloudWatchAPI } -var regionName = "us-west-1" +var ( + regionName = "us-west-1" + instanceID = "i-123" + namespace = "AWS/EC2" + + id1 = "cpu1" + metricName1 = "CPUUtilization" + label1 = instanceID + " " + metricName1 + + id2 = "status1" + metricName2 = "StatusCheckFailed" + label2 = instanceID + " " + metricName2 + + id3 = "status2" + metricName3 = "StatusCheckFailed_System" + label3 = instanceID + " " + metricName3 + + id4 = "status3" + metricName4 = "StatusCheckFailed_Instance" + label4 = instanceID + " " + metricName4 +) func (m *MockEC2Client) DescribeRegionsRequest(input *ec2.DescribeRegionsInput) ec2.DescribeRegionsRequest { return ec2.DescribeRegionsRequest{ @@ -59,7 +78,7 @@ func (m *MockEC2Client) DescribeInstancesRequest(input *ec2.DescribeInstancesInp privateIP := "5.6.7.8" instance := ec2.Instance{ - InstanceId: awssdk.String("i-123"), + InstanceId: awssdk.String(instanceID), InstanceType: ec2.InstanceTypeT2Medium, Placement: &ec2.Placement{ AvailabilityZone: awssdk.String("us-west-1a"), @@ -92,93 +111,20 @@ func (m *MockEC2Client) DescribeInstancesRequest(input *ec2.DescribeInstancesInp } } -func (m *MockCloudWatchClient) GetMetricDataRequest(input *cloudwatch.GetMetricDataInput) cloudwatch.GetMetricDataRequest { - id1 := "cpu1" - label1 := "CPUUtilization" - value1 := 0.25 - - id2 := "status1" - label2 := "StatusCheckFailed" - value2 := 0.0 - - id3 := "status2" - label3 := "StatusCheckFailed_System" - value3 := 0.0 - - id4 := "status3" - label4 := "StatusCheckFailed_Instance" - value4 := 0.0 - - return cloudwatch.GetMetricDataRequest{ - Request: &awssdk.Request{ - Data: &cloudwatch.GetMetricDataOutput{ - MetricDataResults: []cloudwatch.MetricDataResult{ - { - Id: &id1, - Label: &label1, - Values: []float64{value1}, - }, - { - Id: &id2, - Label: &label2, - Values: []float64{value2}, - }, - { - Id: &id3, - Label: &label3, - Values: []float64{value3}, - }, - { - Id: &id4, - Label: &label4, - Values: []float64{value4}, - }, - }, - }, - }, - } -} - -func TestGetInstanceIDs(t *testing.T) { +func TestGetinstanceIDs(t *testing.T) { mockSvc := &MockEC2Client{} instanceIDs, instancesOutputs, err := getInstancesPerRegion(mockSvc) if err != nil { - fmt.Println("failed getInstancesPerRegion: ", err) t.FailNow() } assert.Equal(t, 1, len(instanceIDs)) assert.Equal(t, 1, len(instancesOutputs)) - assert.Equal(t, "i-123", instanceIDs[0]) - assert.Equal(t, ec2.InstanceType("t2.medium"), instancesOutputs["i-123"].InstanceType) - assert.Equal(t, awssdk.String("image-123"), instancesOutputs["i-123"].ImageId) - assert.Equal(t, awssdk.String("us-west-1a"), instancesOutputs["i-123"].Placement.AvailabilityZone) -} - -func TestGetMetricDataPerRegion(t *testing.T) { - mockSvc := &MockCloudWatchClient{} - getMetricDataOutput, err := getMetricDataPerRegion("-10m", 300, "i-123", nil, mockSvc) - if err != nil { - fmt.Println("failed getMetricDataPerRegion: ", err) - t.FailNow() - } - assert.Equal(t, 4, len(getMetricDataOutput.MetricDataResults)) - assert.Equal(t, "cpu1", *getMetricDataOutput.MetricDataResults[0].Id) - assert.Equal(t, "CPUUtilization", *getMetricDataOutput.MetricDataResults[0].Label) - assert.Equal(t, 0.25, getMetricDataOutput.MetricDataResults[0].Values[0]) - - assert.Equal(t, "status1", *getMetricDataOutput.MetricDataResults[1].Id) - assert.Equal(t, "StatusCheckFailed", *getMetricDataOutput.MetricDataResults[1].Label) - assert.Equal(t, 0.0, getMetricDataOutput.MetricDataResults[1].Values[0]) - - assert.Equal(t, "status2", *getMetricDataOutput.MetricDataResults[2].Id) - assert.Equal(t, "StatusCheckFailed_System", *getMetricDataOutput.MetricDataResults[2].Label) - assert.Equal(t, 0.0, getMetricDataOutput.MetricDataResults[2].Values[0]) - - assert.Equal(t, "status3", *getMetricDataOutput.MetricDataResults[3].Id) - assert.Equal(t, "StatusCheckFailed_Instance", *getMetricDataOutput.MetricDataResults[3].Label) - assert.Equal(t, 0.0, getMetricDataOutput.MetricDataResults[3].Values[0]) + assert.Equal(t, instanceID, instanceIDs[0]) + assert.Equal(t, ec2.InstanceType("t2.medium"), instancesOutputs[instanceID].InstanceType) + assert.Equal(t, awssdk.String("image-123"), instancesOutputs[instanceID].ImageId) + assert.Equal(t, awssdk.String("us-west-1a"), instancesOutputs[instanceID].Placement.AvailabilityZone) } func TestCreateCloudWatchEvents(t *testing.T) { @@ -224,20 +170,56 @@ func TestCreateCloudWatchEvents(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 1, len(instanceIDs)) instanceID := instanceIDs[0] - assert.Equal(t, "i-123", instanceID) + assert.Equal(t, instanceID, instanceID) - svcCloudwatchMock := &MockCloudWatchClient{} - getMetricDataOutput, err := getMetricDataPerRegion("-600s", 300, instanceID, nil, svcCloudwatchMock) - assert.NoError(t, err) - assert.Equal(t, 4, len(getMetricDataOutput.MetricDataResults)) - assert.Equal(t, "cpu1", *getMetricDataOutput.MetricDataResults[0].Id) - assert.Equal(t, "CPUUtilization", *getMetricDataOutput.MetricDataResults[0].Label) - assert.Equal(t, 0.25, getMetricDataOutput.MetricDataResults[0].Values[0]) + getMetricDataOutput := []cloudwatch.MetricDataResult{ + { + Id: &id1, + Label: &label1, + Values: []float64{0.25}, + }, + { + Id: &id2, + Label: &label2, + Values: []float64{0.0}, + }, + { + Id: &id3, + Label: &label3, + Values: []float64{0.0}, + }, + { + Id: &id4, + Label: &label4, + Values: []float64{0.0}, + }, + } - event, info, err := createCloudWatchEvents(getMetricDataOutput, instanceID, instancesOutputs[instanceID], mockModuleConfig.DefaultRegion) + event, _, err := createCloudWatchEvents(getMetricDataOutput, instanceID, instancesOutputs[instanceID], mockModuleConfig.DefaultRegion) assert.NoError(t, err) - assert.Equal(t, "", info) assert.Equal(t, expectedEvent.RootFields, event.RootFields) assert.Equal(t, expectedEvent.MetricSetFields["cpu"], event.MetricSetFields["cpu"]) assert.Equal(t, expectedEvent.MetricSetFields["instance"], event.MetricSetFields["instance"]) } + +func TestConstructMetricQueries(t *testing.T) { + name := "InstanceId" + dim := cloudwatch.Dimension{ + Name: &name, + Value: &instanceID, + } + + listMetric := cloudwatch.Metric{ + Dimensions: []cloudwatch.Dimension{dim}, + MetricName: &metricName1, + Namespace: &namespace, + } + + listMetricsOutput := []cloudwatch.Metric{listMetric} + metricDataQuery := constructMetricQueries(listMetricsOutput, instanceID, 300) + assert.Equal(t, 1, len(metricDataQuery)) + assert.Equal(t, "i-123 CPUUtilization", *metricDataQuery[0].Label) + assert.Equal(t, "Average", *metricDataQuery[0].MetricStat.Stat) + assert.Equal(t, metricName1, *metricDataQuery[0].MetricStat.Metric.MetricName) + assert.Equal(t, namespace, *metricDataQuery[0].MetricStat.Metric.Namespace) +} diff --git a/x-pack/metricbeat/module/aws/mtest/integration.go b/x-pack/metricbeat/module/aws/mtest/integration.go new file mode 100644 index 00000000000..117ec5e5d42 --- /dev/null +++ b/x-pack/metricbeat/module/aws/mtest/integration.go @@ -0,0 +1,83 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package mtest + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/elastic/beats/metricbeat/mb" +) + +// GetConfigForTest function gets aws credentials for integration tests. +func GetConfigForTest(metricSetName string) (map[string]interface{}, string) { + accessKeyID, okAccessKeyID := os.LookupEnv("AWS_ACCESS_KEY_ID") + secretAccessKey, okSecretAccessKey := os.LookupEnv("AWS_SECRET_ACCESS_KEY") + sessionToken, okSessionToken := os.LookupEnv("AWS_SESSION_TOKEN") + defaultRegion, _ := os.LookupEnv("AWS_REGION") + if defaultRegion == "" { + defaultRegion = "us-west-1" + } + + info := "" + config := map[string]interface{}{} + if !okAccessKeyID || accessKeyID == "" { + info = "Skipping TestFetch; $AWS_ACCESS_KEY_ID not set or set to empty" + } else if !okSecretAccessKey || secretAccessKey == "" { + info = "Skipping TestFetch; $AWS_SECRET_ACCESS_KEY not set or set to empty" + } else { + config = map[string]interface{}{ + "module": "aws", + "period": "300s", + "metricsets": []string{metricSetName}, + "access_key_id": accessKeyID, + "secret_access_key": secretAccessKey, + "default_region": defaultRegion, + } + + if okSessionToken && sessionToken != "" { + config["session_token"] = sessionToken + } + } + return config, info +} + +// CheckEventField function checks a given field type and compares it with the expected type for integration tests. +func CheckEventField(metricName string, expectedType string, event mb.Event, t *testing.T) { + if ok, err := event.MetricSetFields.HasKey(metricName); ok { + assert.NoError(t, err) + metricValue, err := event.MetricSetFields.GetValue(metricName) + assert.NoError(t, err) + compareType(metricValue, expectedType, t) + } else if ok, err := event.RootFields.HasKey(metricName); ok { + assert.NoError(t, err) + rootValue, err := event.RootFields.GetValue(metricName) + assert.NoError(t, err) + compareType(rootValue, expectedType, t) + } +} + +func compareType(metricValue interface{}, expectedType string, t *testing.T) { + switch metricValue.(type) { + case float64: + if expectedType != "float" { + t.Log("Failed: Field is not in type " + expectedType) + t.Fail() + } + case string: + if expectedType != "string" { + t.Log("Failed: Field is not in type " + expectedType) + t.Fail() + } + case int64: + if expectedType != "int" { + t.Log("Failed: Field is not in type " + expectedType) + t.Fail() + } + } + t.Log("Succeed: Field matches type " + expectedType) +} diff --git a/x-pack/metricbeat/module/aws/utils.go b/x-pack/metricbeat/module/aws/utils.go new file mode 100644 index 00000000000..e66ad82d605 --- /dev/null +++ b/x-pack/metricbeat/module/aws/utils.go @@ -0,0 +1,86 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package aws + +import ( + "time" + + "github.com/aws/aws-sdk-go-v2/service/cloudwatch" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch/cloudwatchiface" + "github.com/pkg/errors" + + "github.com/elastic/beats/libbeat/common" + s "github.com/elastic/beats/libbeat/common/schema" +) + +// GetStartTimeEndTime function uses durationString to create startTime and endTime for queries. +func GetStartTimeEndTime(durationString string) (startTime time.Time, endTime time.Time, err error) { + endTime = time.Now() + duration, err := time.ParseDuration(durationString) + if err != nil { + return + } + startTime = endTime.Add(duration) + return startTime, endTime, nil +} + +// GetListMetricsOutput function gets listMetrics results from cloudwatch per namespace for each region. +// ListMetrics Cloudwatch API is used to list the specified metrics. The returned metrics can be used with GetMetricData +// to obtain statistical data. +func GetListMetricsOutput(namespace string, regionName string, svcCloudwatch cloudwatchiface.CloudWatchAPI) ([]cloudwatch.Metric, error) { + listMetricsInput := &cloudwatch.ListMetricsInput{Namespace: &namespace} + reqListMetrics := svcCloudwatch.ListMetricsRequest(listMetricsInput) + + // List metrics of a given namespace for each region + listMetricsOutput, err := reqListMetrics.Send() + if err != nil { + err = errors.Wrap(err, "ListMetricsRequest failed, skipping region "+regionName) + return nil, err + } + + if listMetricsOutput.Metrics == nil || len(listMetricsOutput.Metrics) == 0 { + // No metrics in this region + return nil, nil + } + return listMetricsOutput.Metrics, nil +} + +func getMetricDataPerRegion(metricDataQueries []cloudwatch.MetricDataQuery, nextToken *string, svc cloudwatchiface.CloudWatchAPI, startTime time.Time, endTime time.Time) (*cloudwatch.GetMetricDataOutput, error) { + getMetricDataInput := &cloudwatch.GetMetricDataInput{ + NextToken: nextToken, + StartTime: &startTime, + EndTime: &endTime, + MetricDataQueries: metricDataQueries, + } + + reqGetMetricData := svc.GetMetricDataRequest(getMetricDataInput) + getMetricDataOutput, err := reqGetMetricData.Send() + if err != nil { + err = errors.Wrap(err, "Error GetMetricDataInput") + return nil, err + } + return getMetricDataOutput, nil +} + +// GetMetricDataResults function uses MetricDataQueries to get metric data output. +func GetMetricDataResults(metricDataQueries []cloudwatch.MetricDataQuery, svc cloudwatchiface.CloudWatchAPI, startTime time.Time, endTime time.Time) ([]cloudwatch.MetricDataResult, error) { + init := true + getMetricDataOutput := &cloudwatch.GetMetricDataOutput{NextToken: nil} + for init || getMetricDataOutput.NextToken != nil { + init = false + output, err := getMetricDataPerRegion(metricDataQueries, getMetricDataOutput.NextToken, svc, startTime, endTime) + if err != nil { + err = errors.Wrap(err, "getMetricDataPerRegion failed") + return getMetricDataOutput.MetricDataResults, err + } + getMetricDataOutput.MetricDataResults = append(getMetricDataOutput.MetricDataResults, output.MetricDataResults...) + } + return getMetricDataOutput.MetricDataResults, nil +} + +// EventMapping maps data in input to a predefined schema. +func EventMapping(input map[string]interface{}, schema s.Schema) (common.MapStr, error) { + return schema.Apply(input, s.FailOnRequired) +} diff --git a/x-pack/metricbeat/module/aws/utils_test.go b/x-pack/metricbeat/module/aws/utils_test.go new file mode 100644 index 00000000000..9b57ecd3e67 --- /dev/null +++ b/x-pack/metricbeat/module/aws/utils_test.go @@ -0,0 +1,171 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package aws + +import ( + "fmt" + "testing" + + awssdk "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch/cloudwatchiface" + "github.com/stretchr/testify/assert" +) + +// MockCloudwatchClient struct is used for unit tests. +type MockCloudWatchClient struct { + cloudwatchiface.CloudWatchAPI +} + +var ( + metricName = "CPUUtilization" + namespace = "AWS/EC2" + dimName = "InstanceId" + instanceID = "i-123" + + id1 = "cpu1" + metricName1 = "CPUUtilization" + label1 = instanceID + " " + metricName1 + + id2 = "status1" + metricName2 = "StatusCheckFailed" + label2 = instanceID + " " + metricName2 + + id3 = "status2" + metricName3 = "StatusCheckFailed_System" + label3 = instanceID + " " + metricName3 + + id4 = "status3" + metricName4 = "StatusCheckFailed_Instance" + label4 = instanceID + " " + metricName4 +) + +func (m *MockCloudWatchClient) ListMetricsRequest(input *cloudwatch.ListMetricsInput) cloudwatch.ListMetricsRequest { + dim := cloudwatch.Dimension{ + Name: &dimName, + Value: &instanceID, + } + return cloudwatch.ListMetricsRequest{ + Request: &awssdk.Request{ + Data: &cloudwatch.ListMetricsOutput{ + Metrics: []cloudwatch.Metric{ + { + MetricName: &metricName, + Namespace: &namespace, + Dimensions: []cloudwatch.Dimension{dim}, + }, + }, + }, + }, + } +} + +func (m *MockCloudWatchClient) GetMetricDataRequest(input *cloudwatch.GetMetricDataInput) cloudwatch.GetMetricDataRequest { + value1 := 0.25 + value2 := 0.0 + value3 := 0.0 + value4 := 0.0 + + return cloudwatch.GetMetricDataRequest{ + Request: &awssdk.Request{ + Data: &cloudwatch.GetMetricDataOutput{ + MetricDataResults: []cloudwatch.MetricDataResult{ + { + Id: &id1, + Label: &label1, + Values: []float64{value1}, + }, + { + Id: &id2, + Label: &label2, + Values: []float64{value2}, + }, + { + Id: &id3, + Label: &label3, + Values: []float64{value3}, + }, + { + Id: &id4, + Label: &label4, + Values: []float64{value4}, + }, + }, + }, + }, + } +} + +func TestGetListMetricsOutput(t *testing.T) { + svcCloudwatch := &MockCloudWatchClient{} + listMetricsOutput, err := GetListMetricsOutput("AWS/EC2", "us-west-1", svcCloudwatch) + assert.NoError(t, err) + assert.Equal(t, 1, len(listMetricsOutput)) + assert.Equal(t, namespace, *listMetricsOutput[0].Namespace) + assert.Equal(t, metricName, *listMetricsOutput[0].MetricName) + assert.Equal(t, 1, len(listMetricsOutput[0].Dimensions)) + assert.Equal(t, dimName, *listMetricsOutput[0].Dimensions[0].Name) + assert.Equal(t, instanceID, *listMetricsOutput[0].Dimensions[0].Value) +} + +func TestGetMetricDataPerRegion(t *testing.T) { + startTime, endTime, err := GetStartTimeEndTime("-10m") + assert.NoError(t, err) + + mockSvc := &MockCloudWatchClient{} + metricDataQueries := []cloudwatch.MetricDataQuery{} + getMetricDataOutput, err := getMetricDataPerRegion(metricDataQueries, nil, mockSvc, startTime, endTime) + if err != nil { + fmt.Println("failed getMetricDataPerRegion: ", err) + t.FailNow() + } + + assert.Equal(t, 4, len(getMetricDataOutput.MetricDataResults)) + assert.Equal(t, id1, *getMetricDataOutput.MetricDataResults[0].Id) + assert.Equal(t, label1, *getMetricDataOutput.MetricDataResults[0].Label) + assert.Equal(t, 0.25, getMetricDataOutput.MetricDataResults[0].Values[0]) + + assert.Equal(t, id2, *getMetricDataOutput.MetricDataResults[1].Id) + assert.Equal(t, label2, *getMetricDataOutput.MetricDataResults[1].Label) + assert.Equal(t, 0.0, getMetricDataOutput.MetricDataResults[1].Values[0]) + + assert.Equal(t, id3, *getMetricDataOutput.MetricDataResults[2].Id) + assert.Equal(t, label3, *getMetricDataOutput.MetricDataResults[2].Label) + assert.Equal(t, 0.0, getMetricDataOutput.MetricDataResults[2].Values[0]) + + assert.Equal(t, id4, *getMetricDataOutput.MetricDataResults[3].Id) + assert.Equal(t, label4, *getMetricDataOutput.MetricDataResults[3].Label) + assert.Equal(t, 0.0, getMetricDataOutput.MetricDataResults[3].Values[0]) +} + +func TestGetMetricDataResults(t *testing.T) { + startTime, endTime, err := GetStartTimeEndTime("-10m") + assert.NoError(t, err) + + mockSvc := &MockCloudWatchClient{} + metricDataQueries := []cloudwatch.MetricDataQuery{} + getMetricDataResults, err := GetMetricDataResults(metricDataQueries, mockSvc, startTime, endTime) + if err != nil { + fmt.Println("failed getMetricDataPerRegion: ", err) + t.FailNow() + } + + assert.Equal(t, 4, len(getMetricDataResults)) + assert.Equal(t, id1, *getMetricDataResults[0].Id) + assert.Equal(t, label1, *getMetricDataResults[0].Label) + assert.Equal(t, 0.25, getMetricDataResults[0].Values[0]) + + assert.Equal(t, id2, *getMetricDataResults[1].Id) + assert.Equal(t, label2, *getMetricDataResults[1].Label) + assert.Equal(t, 0.0, getMetricDataResults[1].Values[0]) + + assert.Equal(t, id3, *getMetricDataResults[2].Id) + assert.Equal(t, label3, *getMetricDataResults[2].Label) + assert.Equal(t, 0.0, getMetricDataResults[2].Values[0]) + + assert.Equal(t, id4, *getMetricDataResults[3].Id) + assert.Equal(t, label4, *getMetricDataResults[3].Label) + assert.Equal(t, 0.0, getMetricDataResults[3].Values[0]) +}