Skip to content

Commit

Permalink
Add resource metric integration test (#453)
Browse files Browse the repository at this point in the history
  • Loading branch information
varunch77 authored Jan 28, 2025
1 parent 54ee01d commit 9c7f0db
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 0 deletions.
32 changes: 32 additions & 0 deletions test/metric_value_benchmark/agent_configs/entity_metrics.json
Original file line number Diff line number Diff line change
@@ -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}"
}
}
}
161 changes: 161 additions & 0 deletions test/metric_value_benchmark/entity_metrics_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions util/common/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
Expand All @@ -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"
)

Expand Down Expand Up @@ -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
}

0 comments on commit 9c7f0db

Please sign in to comment.