diff --git a/test/metric_value_benchmark/agent_configs/entity_metrics.json b/test/metric_value_benchmark/agent_configs/entity_metrics.json new file mode 100644 index 000000000..5f1ec6ec1 --- /dev/null +++ b/test/metric_value_benchmark/agent_configs/entity_metrics.json @@ -0,0 +1,32 @@ +{ + "agent": { + "metrics_collection_interval": 10, + "run_as_user": "root", + "debug": true + }, + "metrics": { + "metrics_collected": { + "cpu": { + "resources": [ + "*" + ], + "measurement": [ + "cpu_usage_idle", + "cpu_usage_nice", + "cpu_usage_guest" + ], + "metrics_collection_interval": 10 + }, + "memory": { + "metrics_collection_interval": 10, + "measurement": [ + "mem_used", + "mem_free" + ] + } + }, + "append_dimensions": { + "InstanceId": "${aws:InstanceId}" + } + } +} \ No newline at end of file diff --git a/test/metric_value_benchmark/entity_metrics_test.go b/test/metric_value_benchmark/entity_metrics_test.go new file mode 100644 index 000000000..a4231e262 --- /dev/null +++ b/test/metric_value_benchmark/entity_metrics_test.go @@ -0,0 +1,161 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +//go:build !windows + +package metric_value_benchmark + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/aws/amazon-cloudwatch-agent-test/test/status" + "github.com/aws/amazon-cloudwatch-agent-test/test/test_runner" + "github.com/aws/amazon-cloudwatch-agent-test/util/awsservice" + "github.com/aws/amazon-cloudwatch-agent-test/util/common" +) + +type EntityMetricsTestRunner struct { + test_runner.BaseTestRunner +} + +var _ test_runner.ITestRunner = (*EntityMetricsTestRunner)(nil) + +type expectedEntity struct { + entityType string + resourceType string + instanceId string +} + +const ( + region = "us-west-2" +) + +func (t *EntityMetricsTestRunner) Validate() status.TestGroupResult { + instanceId := awsservice.GetInstanceId() + + testCases := map[string]struct { + requestBody []byte + expectedEntity expectedEntity + }{ + "ResourceMetrics/CPU": { + requestBody: []byte(fmt.Sprintf(`{ + "Namespace": "CWAgent", + "MetricName": "cpu_usage_idle", + "Dimensions": [ + {"Name": "InstanceId", "Value": "%s"}, + {"Name": "cpu", "Value": "cpu-total"} + ] + }`, instanceId)), + expectedEntity: expectedEntity{ + entityType: "AWS::Resource", + resourceType: "AWS::EC2::Instance", + instanceId: instanceId, + }, + }, + } + + var testResults []status.TestResult + + for name, testCase := range testCases { + testResult := t.validateTestCase(name, testCase) + testResults = append(testResults, testResult) + } + + return status.TestGroupResult{ + Name: t.GetTestName(), + TestResults: testResults, + } +} + +func (t *EntityMetricsTestRunner) validateTestCase(name string, testCase struct { + requestBody []byte + expectedEntity expectedEntity +}) status.TestResult { + testResult := status.TestResult{ + Name: name, + Status: status.FAILED, + } + + req, err := common.BuildListEntitiesForMetricRequest(testCase.requestBody, region) + if err != nil { + log.Printf("Failed to build ListEntitiesForMetric request for test case '%s': %v", name, err) + return testResult + } + + // send the request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Printf("Failed to send request for test case '%s': %v", name, err) + return testResult + } + defer resp.Body.Close() + + // parse and verify the response + var response struct { + Entities []struct { + KeyAttributes struct { + Type string `json:"Type"` + ResourceType string `json:"ResourceType"` + Identifier string `json:"Identifier"` + } `json:"KeyAttributes"` + } `json:"Entities"` + } + + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + log.Printf("Failed to decode response for test case '%s': %v", name, err) + return testResult + } + + if len(response.Entities) == 0 { + log.Printf("Response contains no entities for test case '%s'", name) + return testResult + } + + entity := response.Entities[0] + if entity.KeyAttributes.Type != testCase.expectedEntity.entityType || + entity.KeyAttributes.ResourceType != testCase.expectedEntity.resourceType || + entity.KeyAttributes.Identifier != testCase.expectedEntity.instanceId { + + log.Printf("Entity mismatch for test case '%s':\n"+ + "Expected:\n"+ + " Type: %s\n"+ + " ResourceType: %s\n"+ + " InstanceId: %s\n"+ + "Got:\n"+ + " Type: %s\n"+ + " ResourceType: %s\n"+ + " InstanceId: %s", + name, + testCase.expectedEntity.entityType, + testCase.expectedEntity.resourceType, + testCase.expectedEntity.instanceId, + entity.KeyAttributes.Type, + entity.KeyAttributes.ResourceType, + entity.KeyAttributes.Identifier) + return testResult + } + + testResult.Status = status.SUCCESSFUL + return testResult +} + +func (t *EntityMetricsTestRunner) GetTestName() string { + return "EntityMetrics" +} + +func (t *EntityMetricsTestRunner) GetAgentConfigFileName() string { + return "entity_metrics.json" +} + +func (t *EntityMetricsTestRunner) GetMeasuredMetrics() []string { + return []string{"cpu-total"} +} + +func (t *EntityMetricsTestRunner) GetAgentRunDuration() time.Duration { + return 4 * time.Minute +} diff --git a/test/metric_value_benchmark/metrics_value_benchmark_test.go b/test/metric_value_benchmark/metrics_value_benchmark_test.go index eb7acd491..6e97f8172 100644 --- a/test/metric_value_benchmark/metrics_value_benchmark_test.go +++ b/test/metric_value_benchmark/metrics_value_benchmark_test.go @@ -125,6 +125,7 @@ func getEc2TestRunners(env *environment.MetaData) []*test_runner.TestRunner { {TestRunner: &RenameSSMTestRunner{test_runner.BaseTestRunner{DimensionFactory: factory}}}, {TestRunner: &JMXTomcatJVMTestRunner{test_runner.BaseTestRunner{DimensionFactory: factory}}}, {TestRunner: &JMXKafkaTestRunner{test_runner.BaseTestRunner{DimensionFactory: factory}}}, + {TestRunner: &EntityMetricsTestRunner{test_runner.BaseTestRunner{DimensionFactory: factory}}}, } } return ec2TestRunners diff --git a/util/common/metrics.go b/util/common/metrics.go index bf9435f8a..787f72553 100644 --- a/util/common/metrics.go +++ b/util/common/metrics.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "crypto/rand" + "crypto/sha256" "encoding/binary" "encoding/hex" "errors" @@ -23,6 +24,8 @@ import ( "collectd.org/exec" "collectd.org/network" "github.com/DataDog/datadog-go/statsd" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/aws/aws-sdk-go-v2/config" "github.com/prozz/aws-embedded-metrics-golang/emf" ) @@ -350,3 +353,56 @@ func SendEMFMetrics(metricPerInterval int, metricLogGroup, metricNamespace strin } } + +// This function builds and signs an ListEntitiesForMetric call, essentially trying to replicate this curl command: +// +// curl -i -X POST monitoring.us-west-2.amazonaws.com -H 'Content-Type: application/json' \ +// -H 'Content-Encoding: amz-1.0' \ +// --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \ +// -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ +// --aws-sigv4 "aws:amz:us-west-2:monitoring" \ +// -H 'X-Amz-Target: com.amazonaws.cloudwatch.v2013_01_16.CloudWatchVersion20130116.ListEntitiesForMetric' \ +// -d '{ +// // sample request body: +// "Namespace": "CWAgent", +// "MetricName": "cpu_usage_idle", +// "Dimensions": [{"Name": "InstanceId", "Value": "i-0123456789012"}, { "Name": "cpu", "Value": "cpu-total"}] +// }' +func BuildListEntitiesForMetricRequest(body []byte, region string) (*http.Request, error) { + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) + if err != nil { + return nil, err + } + signer := v4.NewSigner() + h := sha256.New() + + h.Write(body) + payloadHash := hex.EncodeToString(h.Sum(nil)) + + // build the request + req, err := http.NewRequest("POST", "https://monitoring."+region+".amazonaws.com/", bytes.NewReader(body)) + if err != nil { + return nil, err + } + + // set headers + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Amz-Target", "com.amazonaws.cloudwatch.v2013_01_16.CloudWatchVersion20130116.ListEntitiesForMetric") + req.Header.Set("Content-Encoding", "amz-1.0") + + // set creds + credentials, err := cfg.Credentials.Retrieve(context.TODO()) + if err != nil { + return nil, err + } + + req.Header.Set("x-amz-security-token", credentials.SessionToken) + + // sign the request + err = signer.SignHTTP(context.TODO(), credentials, req, payloadHash, "monitoring", region, time.Now()) + if err != nil { + return nil, err + } + + return req, nil +}