diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index ac794ec6d04..d4c01ab8d16 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -326,6 +326,7 @@ https://github.com/elastic/beats/compare/v7.0.0-alpha2...master[Check the HEAD d - Groups same timestamp metric values to one event in the app_insights metricset. {pull}20403[20403] - Updates vm_compute metricset with more info on guest metrics. {pull}20448[20448] - Fix resource tags in aws cloudwatch metricset {issue}20326[20326] {pull}20385[20385] +- Fix ec2 disk and network metrics to use Sum statistic method. {pull}20680[20680] *Packetbeat* diff --git a/metricbeat/_meta/fields.common.yml b/metricbeat/_meta/fields.common.yml index 7a0fb0057ff..8e38e5d129f 100644 --- a/metricbeat/_meta/fields.common.yml +++ b/metricbeat/_meta/fields.common.yml @@ -47,22 +47,27 @@ fields: - name: cpu.pct type: scaled_float + format: percent description: Percent CPU used. This value is normalized by the number of CPU cores and it ranges from 0 to 1. - name: network.in.bytes - type: scaled_float + type: long + format: bytes description: The number of bytes received on all network interfaces by the host in a given period of time. - name: network.out.bytes - type: scaled_float + type: long + format: bytes description: The number of bytes sent out on all network interfaces by the host in a given period of time. - name: network.in.packets - type: scaled_float + type: long description: The number of packets received on all network interfaces by the host in a given period of time. - name: network.out.packets - type: scaled_float + type: long description: The number of packets sent out on all network interfaces by the host in a given period of time. - name: disk.read.bytes - type: scaled_float + type: long + format: bytes description: The total number of bytes read successfully in a given period of time. - name: disk.write.bytes - type: scaled_float + type: long + format: bytes description: The total number of bytes write successfully in a given period of time. diff --git a/metricbeat/docs/fields.asciidoc b/metricbeat/docs/fields.asciidoc index 8bcf3ca3448..f5f564472a3 100644 --- a/metricbeat/docs/fields.asciidoc +++ b/metricbeat/docs/fields.asciidoc @@ -2041,7 +2041,7 @@ type: long *`aws.ec2.network.in.packets`*:: + -- -The number of packets received on all network interfaces by the instance. +The total number of packets received on all network interfaces by the instance in collection period. type: long @@ -2054,14 +2054,14 @@ type: long The number of packets per second sent out on all network interfaces by the instance. -type: long +type: scaled_float -- *`aws.ec2.network.out.packets`*:: + -- -The number of packets sent out on all network interfaces by the instance. +The total number of packets sent out on all network interfaces by the instance in collection period. type: long @@ -2074,14 +2074,14 @@ type: long The number of packets per second sent out on all network interfaces by the instance. -type: long +type: scaled_float -- *`aws.ec2.network.in.bytes`*:: + -- -The number of bytes received on all network interfaces by the instance. +The total number of bytes received on all network interfaces by the instance in collection period. type: long @@ -2096,14 +2096,14 @@ format: bytes The number of bytes per second received on all network interfaces by the instance. -type: long +type: scaled_float -- *`aws.ec2.network.out.bytes`*:: + -- -The number of bytes sent out on all network interfaces by the instance. +The total number of bytes sent out on all network interfaces by the instance in collection period. type: long @@ -2118,14 +2118,14 @@ format: bytes The number of bytes per second sent out on all network interfaces by the instance. -type: long +type: scaled_float -- *`aws.ec2.diskio.read.bytes`*:: + -- -Bytes read from all instance store volumes available to the instance. +Total bytes read from all instance store volumes available to the instance in collection period. type: long @@ -2140,14 +2140,14 @@ format: bytes Bytes read per second from all instance store volumes available to the instance. -type: long +type: scaled_float -- *`aws.ec2.diskio.write.bytes`*:: + -- -Bytes written to all instance store volumes available to the instance. +Total bytes written to all instance store volumes available to the instance in collection period. type: long @@ -2162,14 +2162,14 @@ format: bytes Bytes written per second to all instance store volumes available to the instance. -type: long +type: scaled_float -- *`aws.ec2.diskio.read.ops`*:: + -- -Completed read operations from all instance store volumes available to the instance in a specified period of time. +Total completed read operations from all instance store volumes available to the instance in collection period. type: long @@ -2189,7 +2189,7 @@ type: long *`aws.ec2.diskio.write.ops`*:: + -- -Completed write operations to all instance store volumes available to the instance in a specified period of time. +Total completed write operations to all instance store volumes available to the instance in collection period. type: long @@ -6403,6 +6403,8 @@ Percent CPU used. This value is normalized by the number of CPU cores and it ran type: scaled_float +format: percent + -- *`host.network.in.bytes`*:: @@ -6410,7 +6412,9 @@ type: scaled_float -- The number of bytes received on all network interfaces by the host in a given period of time. -type: scaled_float +type: long + +format: bytes -- @@ -6419,7 +6423,9 @@ type: scaled_float -- The number of bytes sent out on all network interfaces by the host in a given period of time. -type: scaled_float +type: long + +format: bytes -- @@ -6428,7 +6434,7 @@ type: scaled_float -- The number of packets received on all network interfaces by the host in a given period of time. -type: scaled_float +type: long -- @@ -6437,7 +6443,7 @@ type: scaled_float -- The number of packets sent out on all network interfaces by the host in a given period of time. -type: scaled_float +type: long -- @@ -6446,7 +6452,9 @@ type: scaled_float -- The total number of bytes read successfully in a given period of time. -type: scaled_float +type: long + +format: bytes -- @@ -6455,7 +6463,9 @@ type: scaled_float -- The total number of bytes write successfully in a given period of time. -type: scaled_float +type: long + +format: bytes -- diff --git a/x-pack/metricbeat/module/aws/ec2/_meta/data.json b/x-pack/metricbeat/module/aws/ec2/_meta/data.json index 6b8e8bcb720..d807de2f1f6 100644 --- a/x-pack/metricbeat/module/aws/ec2/_meta/data.json +++ b/x-pack/metricbeat/module/aws/ec2/_meta/data.json @@ -3,12 +3,12 @@ "aws": { "ec2": { "cpu": { - "credit_balance": 144, - "credit_usage": 0.058005, + "credit_balance": 1944, + "credit_usage": 0.019738, "surplus_credit_balance": 0, "surplus_credits_charged": 0, "total": { - "pct": 1.1631008613503082 + "pct": 0.054166666666484745 } }, "diskio": { @@ -27,21 +27,21 @@ }, "instance": { "core": { - "count": 1 + "count": 8 }, "image": { - "id": "ami-04bc3da8f14823e88" + "id": "ami-0b418580298265d5c" }, "monitoring": { "state": "disabled" }, "private": { - "dns_name": "ip-172-31-9-119.us-west-1.compute.internal", - "ip": "172.31.9.119" + "dns_name": "ip-172-31-47-161.eu-central-1.compute.internal", + "ip": "172.31.47.161" }, "public": { - "dns_name": "ec2-13-52-163-56.us-west-1.compute.amazonaws.com", - "ip": "13.52.163.56" + "dns_name": "ec2-3-126-207-95.eu-central-1.compute.amazonaws.com", + "ip": "3.126.207.95" }, "state": { "code": 16, @@ -51,16 +51,16 @@ }, "network": { "in": { - "bytes": 786.6, - "bytes_per_sec": 2.622, - "packets": 5.8, - "packets_per_sec": 0.019333333333333334 + "bytes": 420, + "bytes_per_sec": 1.4, + "packets": 10, + "packets_per_sec": 0.03333333333333333 }, "out": { - "bytes": 627, - "bytes_per_sec": 2.09, - "packets": 5.8, - "packets_per_sec": 0.019333333333333334 + "bytes": 280, + "bytes_per_sec": 0.9333333333333333, + "packets": 10, + "packets_per_sec": 0.03333333333333333 } }, "status": { @@ -68,10 +68,6 @@ "check_failed_instance": 0, "check_failed_system": 0 } - }, - "tags": { - "Name": "mysql-test", - "created-by": "ks" } }, "cloud": { @@ -79,16 +75,15 @@ "id": "428152502467", "name": "elastic-beats" }, - "availability_zone": "us-west-1b", + "availability_zone": "eu-central-1b", "instance": { - "id": "i-0516ddaca5c1d231f", - "name": "mysql-test" + "id": "i-061884169c1e2ba3f" }, "machine": { - "type": "t2.micro" + "type": "t2.2xlarge" }, "provider": "aws", - "region": "us-west-1" + "region": "eu-central-1" }, "event": { "dataset": "aws.ec2", @@ -96,9 +91,6 @@ "module": "aws" }, "host": { - "cpu": { - "pct": 0.011631008613503082 - }, "disk": { "read": { "bytes": 0 @@ -107,16 +99,16 @@ "bytes": 0 } }, - "id": "i-0516ddaca5c1d231f", - "name": "mysql-test", + "id": "i-061884169c1e2ba3f", + "name": "i-061884169c1e2ba3f", "network": { "in": { - "bytes": 786.6, - "packets": 5.8 + "bytes": 420, + "packets": 10 }, "out": { - "bytes": 627, - "packets": 5.8 + "bytes": 280, + "packets": 10 } } }, diff --git a/x-pack/metricbeat/module/aws/ec2/_meta/fields.yml b/x-pack/metricbeat/module/aws/ec2/_meta/fields.yml index 75fe4e9bf39..c7280fce998 100644 --- a/x-pack/metricbeat/module/aws/ec2/_meta/fields.yml +++ b/x-pack/metricbeat/module/aws/ec2/_meta/fields.yml @@ -27,59 +27,59 @@ - name: network.in.packets type: long description: > - The number of packets received on all network interfaces by the instance. + The total number of packets received on all network interfaces by the instance in collection period. - name: network.in.packets_per_sec - type: long + type: scaled_float description: > The number of packets per second sent out on all network interfaces by the instance. - name: network.out.packets type: long description: > - The number of packets sent out on all network interfaces by the instance. + The total number of packets sent out on all network interfaces by the instance in collection period. - name: network.out.packets_per_sec - type: long + type: scaled_float description: > The number of packets per second sent out on all network interfaces by the instance. - name: network.in.bytes type: long format: bytes description: > - The number of bytes received on all network interfaces by the instance. + The total number of bytes received on all network interfaces by the instance in collection period. - name: network.in.bytes_per_sec - type: long + type: scaled_float description: > The number of bytes per second received on all network interfaces by the instance. - name: network.out.bytes type: long format: bytes description: > - The number of bytes sent out on all network interfaces by the instance. + The total number of bytes sent out on all network interfaces by the instance in collection period. - name: network.out.bytes_per_sec - type: long + type: scaled_float description: > The number of bytes per second sent out on all network interfaces by the instance. - name: diskio.read.bytes type: long format: bytes description: > - Bytes read from all instance store volumes available to the instance. + Total bytes read from all instance store volumes available to the instance in collection period. - name: diskio.read.bytes_per_sec - type: long + type: scaled_float description: > Bytes read per second from all instance store volumes available to the instance. - name: diskio.write.bytes type: long format: bytes description: > - Bytes written to all instance store volumes available to the instance. + Total bytes written to all instance store volumes available to the instance in collection period. - name: diskio.write.bytes_per_sec - type: long + type: scaled_float description: > Bytes written per second to all instance store volumes available to the instance. - name: diskio.read.ops type: long description: > - Completed read operations from all instance store volumes available to the instance in a specified period of time. + Total completed read operations from all instance store volumes available to the instance in collection period. - name: diskio.read.ops_per_sec type: long description: > @@ -87,7 +87,7 @@ - name: diskio.write.ops type: long description: > - Completed write operations to all instance store volumes available to the instance in a specified period of time. + Total completed write operations to all instance store volumes available to the instance in collection period. - name: diskio.write.ops_per_sec type: long description: > diff --git a/x-pack/metricbeat/module/aws/ec2/data.go b/x-pack/metricbeat/module/aws/ec2/data.go index 0e496c4edb1..6dbc8749b35 100644 --- a/x-pack/metricbeat/module/aws/ec2/data.go +++ b/x-pack/metricbeat/module/aws/ec2/data.go @@ -10,7 +10,7 @@ import ( ) var ( - schemaMetricSetFields = s.Schema{ + schemaMetricSetFieldsAverage = s.Schema{ "cpu": s.Object{ "total": s.Object{ "pct": c.Float("CPUUtilization"), @@ -20,6 +20,14 @@ var ( "surplus_credit_balance": c.Float("CPUSurplusCreditBalance"), "surplus_credits_charged": c.Float("CPUSurplusCreditsCharged"), }, + "status": s.Object{ + "check_failed": c.Int("StatusCheckFailed"), + "check_failed_instance": c.Int("StatusCheckFailed_Instance"), + "check_failed_system": c.Int("StatusCheckFailed_System"), + }, + } + + schemaMetricSetFieldsSum = s.Schema{ "diskio": s.Object{ "read": s.Object{ "bytes": c.Float("DiskReadBytes"), @@ -40,10 +48,5 @@ var ( "packets": c.Float("NetworkPacketsOut"), }, }, - "status": s.Object{ - "check_failed": c.Int("StatusCheckFailed"), - "check_failed_instance": c.Int("StatusCheckFailed_Instance"), - "check_failed_system": c.Int("StatusCheckFailed_System"), - }, } ) diff --git a/x-pack/metricbeat/module/aws/ec2/ec2.go b/x-pack/metricbeat/module/aws/ec2/ec2.go index c61ca5ad08b..36ad9a1ca02 100644 --- a/x-pack/metricbeat/module/aws/ec2/ec2.go +++ b/x-pack/metricbeat/module/aws/ec2/ec2.go @@ -6,9 +6,9 @@ package ec2 import ( "context" + "encoding/json" "fmt" "strconv" - "strings" "time" "github.com/aws/aws-sdk-go-v2/service/cloudwatch" @@ -17,18 +17,28 @@ import ( "github.com/pkg/errors" "github.com/elastic/beats/v7/libbeat/common" + "github.com/elastic/beats/v7/libbeat/logp" "github.com/elastic/beats/v7/metricbeat/mb" awscommon "github.com/elastic/beats/v7/x-pack/libbeat/common/aws" "github.com/elastic/beats/v7/x-pack/metricbeat/module/aws" ) var ( - metricsetName = "ec2" - instanceIDIdx = 0 - metricNameIdx = 1 - labelSeparator = "|" + metricsetName = "ec2" + statistics = []string{"Average", "Sum"} ) +type label struct { + InstanceID string + MetricName string + Statistic string +} + +type idStat struct { + instanceID string + statistic string +} + // init registers the MetricSet with the central registry as soon as the program // starts. The New function will be called later to instantiate an instance of // the MetricSet for each host defined in the module's configuration. After the @@ -45,11 +55,13 @@ func init() { // interface methods except for Fetch. type MetricSet struct { *aws.MetricSet + logger *logp.Logger } // 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) { + logger := logp.NewLogger(metricsetName) metricSet, err := aws.NewMetricSet(base) if err != nil { return nil, errors.Wrap(err, "error creating aws metricset") @@ -62,11 +74,12 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) { err := errors.New("period needs to be set to 60s (or a multiple of 60s) if detailed monitoring is " + "enabled for EC2 instances or set to 300s (or a multiple of 300s) if EC2 instances has basic monitoring. " + "To avoid data missing or extra costs, please make sure period is set correctly in config.yml") - base.Logger().Info(err) + logger.Info(err) } return &MetricSet{ MetricSet: metricSet, + logger: logger, }, nil } @@ -87,7 +100,7 @@ func (m *MetricSet) Fetch(report mb.ReporterV2) error { instanceIDs, instancesOutputs, err := getInstancesPerRegion(svcEC2) if err != nil { err = errors.Wrap(err, "getInstancesPerRegion failed, skipping region "+regionName) - m.Logger().Errorf(err.Error()) + m.logger.Errorf(err.Error()) report.Error(err) continue } @@ -98,7 +111,7 @@ func (m *MetricSet) Fetch(report mb.ReporterV2) error { namespace := "AWS/EC2" listMetricsOutput, err := aws.GetListMetricsOutput(namespace, regionName, svcCloudwatch) if err != nil { - m.Logger().Error(err.Error()) + m.logger.Error(err.Error()) report.Error(err) continue } @@ -118,7 +131,7 @@ func (m *MetricSet) Fetch(report mb.ReporterV2) error { metricDataOutput, err = aws.GetMetricDataResults(metricDataQueriesTotal, svcCloudwatch, startTime, endTime) if err != nil { err = errors.Wrap(err, "GetMetricDataResults failed, skipping region "+regionName) - m.Logger().Error(err.Error()) + m.logger.Error(err.Error()) report.Error(err) continue } @@ -126,7 +139,7 @@ func (m *MetricSet) Fetch(report mb.ReporterV2) error { // Create Cloudwatch Events for EC2 events, err := m.createCloudWatchEvents(metricDataOutput, instancesOutputs, regionName) if err != nil { - m.Logger().Error(err.Error()) + m.logger.Error(err.Error()) report.Error(err) continue } @@ -134,7 +147,7 @@ func (m *MetricSet) Fetch(report mb.ReporterV2) error { for _, event := range events { if len(event.MetricSetFields) != 0 { if reported := report.Event(event); !reported { - m.Logger().Debug("Fetch interrupted, failed to emit event") + m.logger.Debug("Fetch interrupted, failed to emit event") return nil } } @@ -149,11 +162,13 @@ func constructMetricQueries(listMetricsOutput []cloudwatch.Metric, instanceID st var metricDataQueries []cloudwatch.MetricDataQuery metricDataQueryEmpty := cloudwatch.MetricDataQuery{} for i, listMetric := range listMetricsOutput { - metricDataQuery := createMetricDataQuery(listMetric, instanceID, i, period) - if metricDataQuery == metricDataQueryEmpty { - continue + for _, statistic := range statistics { + metricDataQuery := createMetricDataQuery(listMetric, instanceID, i, period, statistic) + if metricDataQuery == metricDataQueryEmpty { + continue + } + metricDataQueries = append(metricDataQueries, metricDataQuery) } - metricDataQueries = append(metricDataQueries, metricDataQuery) } return metricDataQueries } @@ -161,10 +176,12 @@ func constructMetricQueries(listMetricsOutput []cloudwatch.Metric, instanceID st func (m *MetricSet) createCloudWatchEvents(getMetricDataResults []cloudwatch.MetricDataResult, instanceOutput map[string]ec2.Instance, regionName string) (map[string]mb.Event, error) { // Initialize events and metricSetFieldResults per instanceID events := map[string]mb.Event{} - metricSetFieldResults := map[string]map[string]interface{}{} + metricSetFieldResults := map[idStat]map[string]interface{}{} for instanceID := range instanceOutput { - events[instanceID] = aws.InitEvent(regionName, m.AccountName, m.AccountID) - metricSetFieldResults[instanceID] = map[string]interface{}{} + for _, statistic := range statistics { + events[instanceID] = aws.InitEvent(regionName, m.AccountName, m.AccountID) + metricSetFieldResults[idStat{instanceID: instanceID, statistic: statistic}] = map[string]interface{}{} + } } // monitoring state for each instance @@ -180,8 +197,14 @@ func (m *MetricSet) createCloudWatchEvents(getMetricDataResults []cloudwatch.Met exists, timestampIdx := aws.CheckTimestampInArray(timestamp, output.Timestamps) if exists { - labels := strings.Split(*output.Label, labelSeparator) - instanceID := labels[instanceIDIdx] + label, err := newLabelFromJSON(*output.Label) + if err != nil { + m.logger.Errorf("convert cloudwatch MetricDataResult label failed for label = %s: %w", *output.Label, err) + continue + } + + instanceID := label.InstanceID + statistic := label.Statistic // Add tags tags := instanceOutput[instanceID].Tags @@ -222,7 +245,7 @@ func (m *MetricSet) createCloudWatchEvents(getMetricDataResults []cloudwatch.Met } if len(output.Values) > timestampIdx { - metricSetFieldResults[instanceID][labels[metricNameIdx]] = fmt.Sprint(output.Values[timestampIdx]) + metricSetFieldResults[idStat{instanceID: instanceID, statistic: statistic}][label.MetricName] = fmt.Sprint(output.Values[timestampIdx]) } instanceStateName, err := instanceOutput[instanceID].State.Name.MarshalValue() @@ -263,26 +286,33 @@ func (m *MetricSet) createCloudWatchEvents(getMetricDataResults []cloudwatch.Met } } - for instanceID, metricSetFieldsPerInstance := range metricSetFieldResults { + for idStat, metricSetFieldsPerInstance := range metricSetFieldResults { + instanceID := idStat.instanceID + statistic := idStat.statistic + + var resultMetricsetFields common.MapStr + var err error + if len(metricSetFieldsPerInstance) != 0 { - resultMetricsetFields, err := aws.EventMapping(metricSetFieldsPerInstance, schemaMetricSetFields) + if statistic == "Average" { + // Use "Average" statistic method for CPU and status metrics + resultMetricsetFields, err = aws.EventMapping(metricSetFieldsPerInstance, schemaMetricSetFieldsAverage) + } else if statistic == "Sum" { + // Use "Sum" statistic method for disk and network metrics + resultMetricsetFields, err = aws.EventMapping(metricSetFieldsPerInstance, schemaMetricSetFieldsSum) + } + if err != nil { return events, errors.Wrap(err, "EventMapping failed") } // add host cpu/network/disk fields and host.id - hostFields := addHostFields(resultMetricsetFields, events[instanceID].RootFields, instanceID) - events[instanceID].RootFields.Update(hostFields) + addHostFields(resultMetricsetFields, events[instanceID].RootFields, instanceID) // add rate metrics calculateRate(resultMetricsetFields, monitoringStates[instanceID]) events[instanceID].MetricSetFields.Update(resultMetricsetFields) - if len(events[instanceID].MetricSetFields) < 5 { - m.Logger().Info("Missing Cloudwatch data, this is expected for non-running instances" + - " or a new instance during the first data collection. If this shows up multiple times," + - " please recheck the period setting in config. Instance ID: " + instanceID) - } } } @@ -314,16 +344,15 @@ func calculateRate(resultMetricsetFields common.MapStr, monitoringState string) } } -func addHostFields(resultMetricsetFields common.MapStr, rootFields common.MapStr, instanceID string) common.MapStr { - hostRootFields := common.MapStr{} - hostRootFields.Put("host.id", instanceID) +func addHostFields(resultMetricsetFields common.MapStr, rootFields common.MapStr, instanceID string) { + rootFields.Put("host.id", instanceID) // If there is no instance name, use instance ID as the host.name hostName, err := rootFields.GetValue("host.name") if err == nil && hostName != nil { - hostRootFields.Put("host.name", hostName) + rootFields.Put("host.name", hostName) } else { - hostRootFields.Put("host.name", instanceID) + rootFields.Put("host.name", instanceID) } hostFieldTable := map[string]string{ @@ -338,14 +367,17 @@ func addHostFields(resultMetricsetFields common.MapStr, rootFields common.MapStr for ec2MetricName, hostMetricName := range hostFieldTable { metricValue, err := resultMetricsetFields.GetValue(ec2MetricName) - if ec2MetricName == "cpu.total.pct" { - metricValue = metricValue.(float64) / 100 + if err != nil { + continue } - if err == nil && metricValue != nil { - hostRootFields.Put(hostMetricName, metricValue) + + if value, ok := metricValue.(float64); ok { + if ec2MetricName == "cpu.total.pct" { + value = value / 100 + } + rootFields.Put(hostMetricName, value) } } - return hostRootFields } func getInstancesPerRegion(svc ec2iface.ClientAPI) (instanceIDs []string, instancesOutputs map[string]ec2.Instance, err error) { @@ -372,16 +404,15 @@ func getInstancesPerRegion(svc ec2iface.ClientAPI) (instanceIDs []string, instan return } -func createMetricDataQuery(metric cloudwatch.Metric, instanceID string, index int, period time.Duration) (metricDataQuery cloudwatch.MetricDataQuery) { - statistic := "Average" +func createMetricDataQuery(metric cloudwatch.Metric, instanceID string, index int, period time.Duration, statistic string) (metricDataQuery cloudwatch.MetricDataQuery) { periodInSeconds := int64(period.Seconds()) - id := metricsetName + strconv.Itoa(index) + id := metricsetName + statistic + strconv.Itoa(index) metricDims := metric.Dimensions for _, dim := range metricDims { if *dim.Name == "InstanceId" && *dim.Value == instanceID { metricName := *metric.MetricName - label := instanceID + labelSeparator + metricName + label := newLabel(instanceID, metricName, statistic).JSON() metricDataQuery = cloudwatch.MetricDataQuery{ Id: &id, MetricStat: &cloudwatch.MetricStat{ @@ -396,3 +427,23 @@ func createMetricDataQuery(metric cloudwatch.Metric, instanceID string, index in } return } + +func newLabel(instanceID string, metricName string, statistic string) *label { + return &label{InstanceID: instanceID, MetricName: metricName, Statistic: statistic} +} + +// JSON is a method of label object for converting label to string +func (l *label) JSON() string { + // Ignore error, this cannot fail + out, _ := json.Marshal(l) + return string(out) +} + +func newLabelFromJSON(labelJSON string) (label, error) { + labelStruct := label{} + err := json.Unmarshal([]byte(labelJSON), &labelStruct) + if err != nil { + return labelStruct, fmt.Errorf("json.Unmarshal failed: %w", err) + } + return labelStruct, nil +} diff --git a/x-pack/metricbeat/module/aws/ec2/ec2_test.go b/x-pack/metricbeat/module/aws/ec2/ec2_test.go index b98e3758c65..6fdf98c635d 100644 --- a/x-pack/metricbeat/module/aws/ec2/ec2_test.go +++ b/x-pack/metricbeat/module/aws/ec2/ec2_test.go @@ -11,8 +11,6 @@ import ( "testing" "time" - "github.com/elastic/beats/v7/x-pack/metricbeat/module/aws" - 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/ec2" @@ -20,7 +18,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/elastic/beats/v7/libbeat/common" + "github.com/elastic/beats/v7/libbeat/logp" "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/aws" ) // MockEC2Client struct is used for unit tests. @@ -32,22 +32,23 @@ var ( regionName = "us-west-1" instanceID = "i-123" namespace = "AWS/EC2" + statistic = "Average" id1 = "cpu1" metricName1 = "CPUUtilization" - label1 = instanceID + labelSeparator + metricName1 + label1 = newLabel(instanceID, metricName1, statistic).JSON() id2 = "status1" metricName2 = "StatusCheckFailed" - label2 = instanceID + labelSeparator + metricName2 + label2 = newLabel(instanceID, metricName2, statistic).JSON() id3 = "status2" metricName3 = "StatusCheckFailed_System" - label3 = instanceID + labelSeparator + metricName3 + label3 = newLabel(instanceID, metricName3, statistic).JSON() id4 = "status3" metricName4 = "StatusCheckFailed_Instance" - label4 = instanceID + labelSeparator + metricName4 + label4 = newLabel(instanceID, metricName4, statistic).JSON() ) func (m *MockEC2Client) DescribeRegionsRequest(input *ec2.DescribeRegionsInput) ec2.DescribeRegionsRequest { @@ -221,7 +222,9 @@ func TestCreateCloudWatchEventsDedotTags(t *testing.T) { metricSet := MetricSet{ &aws.MetricSet{}, + logp.NewLogger("test"), } + events, err := metricSet.createCloudWatchEvents(getMetricDataOutput, instancesOutputs, "us-west-1") assert.NoError(t, err) assert.Equal(t, 1, len(events)) @@ -316,6 +319,7 @@ func TestCreateCloudWatchEventsWithTagsFilter(t *testing.T) { Value: "foo", }}, }, + logp.NewLogger("test"), } events, err := metricSet.createCloudWatchEvents(getMetricDataOutput, instancesOutputs, "us-west-1") @@ -370,6 +374,7 @@ func TestCreateCloudWatchEventsWithNotMatchingTagsFilter(t *testing.T) { Value: "not_foo", }}, }, + logp.NewLogger("test"), } events, err := metricSet.createCloudWatchEvents(getMetricDataOutput, instancesOutputs, "us-west-1") assert.NoError(t, err) @@ -391,8 +396,8 @@ func TestConstructMetricQueries(t *testing.T) { listMetricsOutput := []cloudwatch.Metric{listMetric} metricDataQuery := constructMetricQueries(listMetricsOutput, instanceID, 5*time.Minute) - assert.Equal(t, 1, len(metricDataQuery)) - assert.Equal(t, "i-123|CPUUtilization", *metricDataQuery[0].Label) + assert.Equal(t, 2, len(metricDataQuery)) + assert.Equal(t, "{\"InstanceID\":\"i-123\",\"MetricName\":\"CPUUtilization\",\"Statistic\":\"Average\"}", *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) @@ -514,6 +519,7 @@ func TestCreateCloudWatchEventsWithInstanceName(t *testing.T) { metricSet := MetricSet{ &aws.MetricSet{}, + logp.NewLogger("test"), } events, err := metricSet.createCloudWatchEvents(getMetricDataOutput, instancesOutputs, "us-west-1") @@ -530,3 +536,20 @@ func TestCreateCloudWatchEventsWithInstanceName(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "test-instance", instanceName) } + +func TestNewLabel(t *testing.T) { + instanceID := "i-123" + metricName := "CPUUtilization" + statistic := "Average" + label := newLabel(instanceID, metricName, statistic).JSON() + assert.Equal(t, "{\"InstanceID\":\"i-123\",\"MetricName\":\"CPUUtilization\",\"Statistic\":\"Average\"}", label) +} + +func TestConvertLabel(t *testing.T) { + labelStr := "{\"InstanceID\":\"i-123\",\"MetricName\":\"CPUUtilization\",\"Statistic\":\"Average\"}" + label, err := newLabelFromJSON(labelStr) + assert.NoError(t, err) + assert.Equal(t, "i-123", label.InstanceID) + assert.Equal(t, "CPUUtilization", label.MetricName) + assert.Equal(t, "Average", label.Statistic) +} diff --git a/x-pack/metricbeat/module/aws/fields.go b/x-pack/metricbeat/module/aws/fields.go index 32fb4eb68c2..6124034a43a 100644 --- a/x-pack/metricbeat/module/aws/fields.go +++ b/x-pack/metricbeat/module/aws/fields.go @@ -19,5 +19,5 @@ func init() { // AssetAws returns asset data. // This is the base64 encoded gzipped contents of module/aws. func AssetAws() string { - return "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" + return "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" }