diff --git a/generator/test_case_generator.go b/generator/test_case_generator.go index 3c9a0ed91..655d1c8ee 100644 --- a/generator/test_case_generator.go +++ b/generator/test_case_generator.go @@ -187,6 +187,9 @@ var testTypeToTestConfig = map[string][]testConfig{ targets: map[string]map[string]struct{}{"arc": {"amd64": {}}}, }, {testDir: "./test/fluent", terraformDir: "terraform/eks/daemon/fluent/bit"}, + {testDir: "./test/app_signals", terraformDir: "terraform/eks/daemon/app_signals", + targets: map[string]map[string]struct{}{"arc": {"amd64": {}}}, + }, }, "eks_deployment": { {testDir: "./test/metric_value_benchmark"}, diff --git a/terraform/eks/daemon/app_signals/main.tf b/terraform/eks/daemon/app_signals/main.tf new file mode 100644 index 000000000..56aba8a37 --- /dev/null +++ b/terraform/eks/daemon/app_signals/main.tf @@ -0,0 +1,514 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +module "common" { + source = "../../../common" + cwagent_image_repo = var.cwagent_image_repo + cwagent_image_tag = var.cwagent_image_tag +} + +module "basic_components" { + source = "../../../basic_components" + + region = var.region +} + +data "aws_eks_cluster_auth" "this" { + name = aws_eks_cluster.this.name +} + +resource "aws_eks_cluster" "this" { + name = "cwagent-eks-integ-${module.common.testing_id}" + role_arn = module.basic_components.role_arn + version = var.k8s_version + enabled_cluster_log_types = [ + "api", + "audit", + "authenticator", + "controllerManager", + "scheduler" + ] + vpc_config { + subnet_ids = module.basic_components.public_subnet_ids + security_group_ids = [module.basic_components.security_group] + } +} + +# EKS Node Groups +resource "aws_eks_node_group" "this" { + cluster_name = aws_eks_cluster.this.name + node_group_name = "cwagent-eks-integ-node" + node_role_arn = aws_iam_role.node_role.arn + subnet_ids = module.basic_components.public_subnet_ids + + scaling_config { + desired_size = 1 + max_size = 1 + min_size = 1 + } + + ami_type = "AL2_x86_64" + capacity_type = "ON_DEMAND" + disk_size = 20 + instance_types = ["t3.medium"] + + depends_on = [ + aws_iam_role_policy_attachment.node_AmazonEC2ContainerRegistryReadOnly, + aws_iam_role_policy_attachment.node_AmazonEKS_CNI_Policy, + aws_iam_role_policy_attachment.node_AmazonEKSWorkerNodePolicy, + aws_iam_role_policy_attachment.node_CloudWatchAgentServerPolicy, + aws_iam_role_policy_attachment.node_AWSXRayDaemonWriteAccess + ] +} + +# EKS Node IAM Role +resource "aws_iam_role" "node_role" { + name = "cwagent-eks-Worker-Role-${module.common.testing_id}" + + assume_role_policy = < traceid_generator.go && chmod +x traceid_generator.go; export START_TIME=$(date +%s%N); export TRACE_ID=$(go run ./traceid_generator.go); do echo '${data.template_file.server_consumer.rendered}' | sed -e \"s/START_TIME/$START_TIME/\" > server_consumer.json; curl -H 'Content-Type: application/json' -d @server_consumer.json -i http://127.0.0.1:4316/v1/metrics --verbose; echo '${data.template_file.client_producer.rendered}' | sed -e \"s/START_TIME/$START_TIME/\" > client_producer.json; curl -H 'Content-Type: application/json' -d @client_producer.json -i http://127.0.0.1:4316/v1/metrics --verbose; echo '${data.template_file.traces.rendered}' | sed -e \"s/START_TIME/$START_TIME/\" | sed -e \"s/TRACE_ID/$TRACE_ID/\" > traces.json; curl -H 'Content-Type: application/json' -d @traces.json -i http://127.0.0.1:4316/v1/traces --verbose; sleep 1; done" + ] + env { + name = "HOST_IP" + value_from { + field_ref { + field_path = "status.hostIP" + } + } + } + env { + name = "HOST_NAME" + value_from { + field_ref { + field_path = "spec.nodeName" + } + } + } + env { + name = "K8S_NAMESPACE" + value_from { + field_ref { + field_path = "metadata.namespace" + } + } + } + volume_mount { + mount_path = "/etc/cwagentconfig" + name = "cwagentconfig" + } + } + service_account_name = "cloudwatch-agent" + termination_grace_period_seconds = 60 + } + } + } +} + +########################################## +# Template Files +########################################## +locals { + cwagent_config = "../../../../${var.test_dir}/resources/config.json" + server_consumer = "../../../../${var.test_dir}/resources/metrics/server_consumer.json" + client_producer = "../../../../${var.test_dir}/resources/metrics/client_producer.json" + traces = "../../../../${var.test_dir}/resources/traces/traces.json" + traceid_generator = "../../../../${var.test_dir}/resources/traceid_generator.go" +} + +data "template_file" "cwagent_config" { + template = file(local.cwagent_config) + vars = { + } +} + +resource "kubernetes_config_map" "cwagentconfig" { + depends_on = [ + kubernetes_namespace.namespace, + kubernetes_service_account.cwagentservice + ] + metadata { + name = "cwagentconfig" + namespace = "amazon-cloudwatch" + } + data = { + "cwagentconfig.json" : data.template_file.cwagent_config.rendered + } +} + +data "template_file" "server_consumer" { + template = file(local.server_consumer) + vars = { + } +} + +data "template_file" "client_producer" { + template = file(local.client_producer) + vars = { + } +} + +data "template_file" "traces" { + template = file(local.traces) + vars = { + } +} + +data "template_file" "traceid_generator" { + template = file(local.traceid_generator) + vars = { + } +} + +resource "kubernetes_service_account" "cwagentservice" { + depends_on = [kubernetes_namespace.namespace] + metadata { + name = "cloudwatch-agent" + namespace = "amazon-cloudwatch" + } +} + +resource "kubernetes_cluster_role" "clusterrole" { + depends_on = [kubernetes_namespace.namespace] + metadata { + name = "cloudwatch-agent-role" + } + rule { + verbs = ["list", "watch"] + resources = ["pods", "nodes", "endpoints", "services"] + api_groups = [""] + } + rule { + verbs = ["list", "watch"] + resources = ["replicasets"] + api_groups = ["apps"] + } + rule { + verbs = ["list", "watch"] + resources = ["jobs"] + api_groups = ["batch"] + } + rule { + verbs = ["get"] + resources = ["nodes/proxy", "configmaps"] + api_groups = [""] + } + rule { + verbs = ["create"] + resources = ["nodes/stats", "configmaps", "events"] + api_groups = [""] + } + rule { + verbs = ["get", "update"] + resource_names = ["cwagent-clusterleader"] + resources = ["configmaps"] + api_groups = [""] + } +} + +resource "kubernetes_cluster_role_binding" "rolebinding" { + depends_on = [kubernetes_namespace.namespace] + metadata { + name = "cloudwatch-agent-role-binding" + } + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = "cloudwatch-agent-role" + } + subject { + kind = "ServiceAccount" + name = "cloudwatch-agent" + namespace = "amazon-cloudwatch" + } +} + +resource "null_resource" "validator" { + depends_on = [ + aws_eks_node_group.this, + kubernetes_daemonset.service, + kubernetes_cluster_role_binding.rolebinding, + kubernetes_service_account.cwagentservice, + ] + provisioner "local-exec" { + command = <<-EOT + echo "Validating EKS metrics/traces for AppSignals" + cd ../../../.. + go test ${var.test_dir} -timeout 1h -eksClusterName=${aws_eks_cluster.this.name} -computeType=EKS -v -eksDeploymentStrategy=DAEMON + EOT + } +} diff --git a/terraform/eks/daemon/app_signals/providers.tf b/terraform/eks/daemon/app_signals/providers.tf new file mode 100644 index 000000000..9bd2885f5 --- /dev/null +++ b/terraform/eks/daemon/app_signals/providers.tf @@ -0,0 +1,17 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +provider "aws" { + region = var.region +} + +provider "kubernetes" { + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "aws" + args = ["eks", "get-token", "--cluster-name", aws_eks_cluster.this.name] + } + host = aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(aws_eks_cluster.this.certificate_authority.0.data) + token = data.aws_eks_cluster_auth.this.token +} \ No newline at end of file diff --git a/terraform/eks/daemon/app_signals/variables.tf b/terraform/eks/daemon/app_signals/variables.tf new file mode 100644 index 000000000..a0434630e --- /dev/null +++ b/terraform/eks/daemon/app_signals/variables.tf @@ -0,0 +1,37 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +variable "region" { + type = string + default = "us-west-2" +} + +variable "test_dir" { + type = string + default = "./test/app_signals" +} + +variable "cwagent_image_repo" { + type = string + default = "public.ecr.aws/cloudwatch-agent/cloudwatch-agent" +} + +variable "cwagent_image_tag" { + type = string + default = "latest" +} + +variable "k8s_version" { + type = string + default = "1.24" +} + +variable "ami_type" { + type = string + default = "AL2_x86_64" +} + +variable "instance_type" { + type = string + default = "t3a.medium" +} \ No newline at end of file diff --git a/test/app_signals/app_signals_test.go b/test/app_signals/app_signals_test.go new file mode 100644 index 000000000..01e9976b8 --- /dev/null +++ b/test/app_signals/app_signals_test.go @@ -0,0 +1,93 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +//go:build !windows + +package app_signals + +import ( + "fmt" + "log" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/aws/amazon-cloudwatch-agent-test/environment" + "github.com/aws/amazon-cloudwatch-agent-test/environment/computetype" + "github.com/aws/amazon-cloudwatch-agent-test/test/metric/dimension" + "github.com/aws/amazon-cloudwatch-agent-test/test/status" + "github.com/aws/amazon-cloudwatch-agent-test/test/test_runner" +) + +const ( + AppSignalsServerConsumerTestName = "AppSignals-Server-Consumer" + AppSignalsClientProducerTestName = "AppSignals-Client-Producer" + AppSignalsTracesTestName = "AppSignals-Traces" +) + +type AppSignalsTestSuite struct { + suite.Suite + test_runner.TestSuite +} + +func (suite *AppSignalsTestSuite) SetupSuite() { + fmt.Println(">>>> Starting AppSignalsTestSuite") +} + +func (suite *AppSignalsTestSuite) TearDownSuite() { + suite.Result.Print() + fmt.Println(">>>> Finished AppSignalsTestSuite") +} + +func init() { + environment.RegisterEnvironmentMetaDataFlags() +} + +var ( + eksTestRunners []*test_runner.EKSTestRunner +) + +func getEksTestRunners(env *environment.MetaData) []*test_runner.EKSTestRunner { + if eksTestRunners == nil { + factory := dimension.GetDimensionFactory(*env) + + eksTestRunners = []*test_runner.EKSTestRunner{ + { + Runner: &AppSignalsMetricsRunner{test_runner.BaseTestRunner{DimensionFactory: factory}, AppSignalsServerConsumerTestName, "HostedIn.EKS.Cluster"}, + Env: *env, + }, + { + Runner: &AppSignalsMetricsRunner{test_runner.BaseTestRunner{DimensionFactory: factory}, AppSignalsClientProducerTestName, "HostedIn.EKS.Cluster"}, + Env: *env, + }, + { + Runner: &AppSignalsTracesRunner{test_runner.BaseTestRunner{DimensionFactory: factory}, AppSignalsTracesTestName, env.EKSClusterName}, + Env: *env, + }, + } + } + return eksTestRunners +} + +func (suite *AppSignalsTestSuite) TestAllInSuite() { + env := environment.GetEnvironmentMetaData() + switch env.ComputeType { + case computetype.EKS: + log.Println("Environment compute type is EKS") + for _, testRunner := range getEksTestRunners(env) { + testRunner.Run(suite, env) + } + default: + return + } + + suite.Assert().Equal(status.SUCCESSFUL, suite.Result.GetStatus(), "AppSignals Test Suite Failed") +} + +func (suite *AppSignalsTestSuite) AddToSuiteResult(r status.TestGroupResult) { + suite.Result.TestGroupResults = append(suite.Result.TestGroupResults, r) +} + +func TestAppSignalsSuite(t *testing.T) { + suite.Run(t, new(AppSignalsTestSuite)) +} diff --git a/test/app_signals/metrics_test.go b/test/app_signals/metrics_test.go new file mode 100644 index 000000000..4771cba10 --- /dev/null +++ b/test/app_signals/metrics_test.go @@ -0,0 +1,76 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +//go:build !windows + +package app_signals + +import ( + "time" + + "github.com/aws/amazon-cloudwatch-agent-test/test/metric" + "github.com/aws/amazon-cloudwatch-agent-test/test/metric/dimension" + "github.com/aws/amazon-cloudwatch-agent-test/test/status" + "github.com/aws/amazon-cloudwatch-agent-test/test/test_runner" +) + +const testRetryCount = 6 +const namespace = "AppSignals" + +type AppSignalsMetricsRunner struct { + test_runner.BaseTestRunner + testName string + dimensionKey string +} + +func (t *AppSignalsMetricsRunner) Validate() status.TestGroupResult { + metricsToFetch := t.GetMeasuredMetrics() + testResults := make([]status.TestResult, len(metricsToFetch)) + instructions := GetInstructionsFromTestName(t.testName) + + for i, metricName := range metricsToFetch { + var testResult status.TestResult + for j := 0; j < testRetryCount; j++ { + testResult = metric.ValidateAppSignalsMetric(t.DimensionFactory, namespace, metricName, instructions) + if testResult.Status == status.SUCCESSFUL { + break + } + time.Sleep(30 * time.Second) + } + testResults[i] = testResult + } + + return status.TestGroupResult{ + Name: t.GetTestName(), + TestResults: testResults, + } +} + +func (t *AppSignalsMetricsRunner) GetTestName() string { + return t.testName +} + +func (t *AppSignalsMetricsRunner) GetAgentRunDuration() time.Duration { + return 3 * time.Minute +} + +func (t *AppSignalsMetricsRunner) GetMeasuredMetrics() []string { + return metric.AppSignalsMetricNames +} + +func (e *AppSignalsMetricsRunner) GetAgentConfigFileName() string { + return "" +} + +func GetInstructionsFromTestName(testName string) []dimension.Instruction { + switch testName { + case AppSignalsClientProducerTestName: + return metric.ClientProducerInstructions + case AppSignalsServerConsumerTestName: + return metric.ServerConsumerInstructions + default: + return nil + } +} + +var _ test_runner.ITestRunner = (*AppSignalsMetricsRunner)(nil) diff --git a/test/app_signals/resources/config.json b/test/app_signals/resources/config.json new file mode 100644 index 000000000..9c03d5915 --- /dev/null +++ b/test/app_signals/resources/config.json @@ -0,0 +1,15 @@ +{ + "agent": { + "debug": true + }, + "logs": { + "metrics_collected": { + "app_signals": {} + } + }, + "traces": { + "traces_collected": { + "app_signals": {} + } + } +} \ No newline at end of file diff --git a/test/app_signals/resources/metrics/client_producer.json b/test/app_signals/resources/metrics/client_producer.json new file mode 100644 index 000000000..8a39c60d6 --- /dev/null +++ b/test/app_signals/resources/metrics/client_producer.json @@ -0,0 +1,199 @@ +{ + "resourceMetrics": [ + { + "resource": { + "attributes": [ + { + "key": "k8s.namespace.name", + "value": { + "stringValue": "default" + } + }, + { + "key": "k8s.pod.name", + "value": { + "stringValue": "pod-name" + } + }, + { + "key": "aws.deployment.name", + "value": { + "stringValue": "deployment-name" + } + }, + { + "key": "host.id", + "value": { + "stringValue": "i-00000000000000000" + } + } + ] + }, + "scopeMetrics": [ + { + "metrics": [ + { + "name": "Error", + "unit": "Milliseconds", + "sum": { + "dataPoints": [ + { + "attributes": [ + { + "key": "aws.span.kind", + "value": { + "stringValue": "CLIENT" + } + }, + { + "key": "aws.local.operation", + "value": { + "stringValue": "operation" + } + }, + { + "key": "aws.local.service", + "value": { + "stringValue": "service-name" + } + }, + { + "key": "aws.remote.operation", + "value": { + "stringValue": "remote-operation" + } + }, + { + "key": "aws.remote.service", + "value": { + "stringValue": "service-name-remote" + } + }, + { + "key": "aws.remote.target", + "value": { + "stringValue": "remote-target" + } + } + ], + "startTimeUnixNano": START_TIME, + "timeUnixNano": START_TIME, + "sum": 0, + "min": 0, + "max": 0 + } + ] + } + }, + { + "name": "Fault", + "unit": "Milliseconds", + "sum": { + "dataPoints": [ + { + "attributes": [ + { + "key": "aws.span.kind", + "value": { + "stringValue": "CLIENT" + } + }, + { + "key": "aws.local.operation", + "value": { + "stringValue": "operation" + } + }, + { + "key": "aws.local.service", + "value": { + "stringValue": "service-name" + } + }, + { + "key": "aws.remote.operation", + "value": { + "stringValue": "remote-operation" + } + }, + { + "key": "aws.remote.service", + "value": { + "stringValue": "service-name-remote" + } + }, + { + "key": "aws.remote.target", + "value": { + "stringValue": "remote-target" + } + } + ], + "startTimeUnixNano": START_TIME, + "timeUnixNano": START_TIME, + "sum": 0, + "min": 0, + "max": 0 + } + ] + } + }, + { + "name": "Latency", + "unit": "Milliseconds", + "sum": { + "dataPoints": [ + { + "attributes": [ + { + "key": "aws.span.kind", + "value": { + "stringValue": "CLIENT" + } + }, + { + "key": "aws.local.operation", + "value": { + "stringValue": "operation" + } + }, + { + "key": "aws.local.service", + "value": { + "stringValue": "service-name" + } + }, + { + "key": "aws.remote.operation", + "value": { + "stringValue": "remote-operation" + } + }, + { + "key": "aws.remote.service", + "value": { + "stringValue": "service-name-remote" + } + }, + { + "key": "aws.remote.target", + "value": { + "stringValue": "remote-target" + } + } + ], + "startTimeUnixNano": START_TIME, + "timeUnixNano": START_TIME, + "sum": 0, + "min": 0, + "max": 0 + } + ] + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/app_signals/resources/metrics/server_consumer.json b/test/app_signals/resources/metrics/server_consumer.json new file mode 100644 index 000000000..6748e6fd0 --- /dev/null +++ b/test/app_signals/resources/metrics/server_consumer.json @@ -0,0 +1,219 @@ +{ + "resourceMetrics": [ + { + "resource": { + "attributes": [ + { + "key": "k8s.namespace.name", + "value": { + "stringValue": "default" + } + }, + { + "key": "k8s.pod.name", + "value": { + "stringValue": "pod-name" + } + }, + { + "key": "aws.deployment.name", + "value": { + "stringValue": "deployment-name" + } + }, + { + "key": "host.id", + "value": { + "stringValue": "i-00000000000000000" + } + } + ] + }, + "scopeMetrics": [ + { + "metrics": [ + { + "name": "Error", + "unit": "Milliseconds", + "sum": { + "dataPoints": [ + { + "attributes": [ + { + "key": "aws.span.kind", + "value": { + "stringValue": "SERVER" + } + }, + { + "key": "Operation", + "value": { + "stringValue": "operation" + } + }, + { + "key": "Service", + "value": { + "stringValue": "service-name" + } + }, + { + "key": "K8s.Namespace", + "value": { + "stringValue": "default" + } + }, + { + "key": "K8s.Pod", + "value": { + "stringValue": "pod-name" + } + } + , + { + "key": "K8s.Node", + "value": { + "stringValue": "i-00000000000000000" + } + }, + { + "key": "K8s.Workload", + "value": { + "stringValue": "sample-app" + } + } + ], + "startTimeUnixNano": START_TIME, + "timeUnixNano": START_TIME, + "sum": 0, + "min": 0, + "max": 0 + } + ] + } + }, + { + "name": "Fault", + "unit": "Milliseconds", + "sum": { + "dataPoints": [ + { + "attributes": [ + { + "key": "aws.span.kind", + "value": { + "stringValue": "SERVER" + } + }, + { + "key": "Operation", + "value": { + "stringValue": "operation" + } + }, + { + "key": "Service", + "value": { + "stringValue": "service-name" + } + }, + { + "key": "K8s.Namespace", + "value": { + "stringValue": "default" + } + }, + { + "key": "K8s.Pod", + "value": { + "stringValue": "pod-name" + } + } + , + { + "key": "K8s.Node", + "value": { + "stringValue": "i-00000000000000000" + } + }, + { + "key": "K8s.Workload", + "value": { + "stringValue": "sample-app" + } + } + ], + "startTimeUnixNano": START_TIME, + "timeUnixNano": START_TIME, + "sum": 0, + "min": 0, + "max": 0 + } + ] + } + }, + { + "name": "Latency", + "unit": "Milliseconds", + "sum": { + "dataPoints": [ + { + "attributes": [ + { + "key": "aws.span.kind", + "value": { + "stringValue": "SERVER" + } + }, + { + "key": "Operation", + "value": { + "stringValue": "operation" + } + }, + { + "key": "Service", + "value": { + "stringValue": "service-name" + } + }, + { + "key": "K8s.Namespace", + "value": { + "stringValue": "default" + } + }, + { + "key": "K8s.Pod", + "value": { + "stringValue": "pod-name" + } + }, + { + "key": "K8s.Node", + "value": { + "stringValue": "i-00000000000000000" + } + }, + { + "key": "K8s.Workload", + "value": { + "stringValue": "sample-app" + } + } + ], + "startTimeUnixNano": START_TIME, + "timeUnixNano": START_TIME, + "sum": 0, + "min": 0, + "max": 0 + } + ] + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/app_signals/resources/traceid_generator.go b/test/app_signals/resources/traceid_generator.go new file mode 100644 index 000000000..a56fde0f8 --- /dev/null +++ b/test/app_signals/resources/traceid_generator.go @@ -0,0 +1,20 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package main + +import ( + "crypto/rand" + "encoding/binary" + "encoding/hex" + "fmt" + "time" +) + +func main() { + var r [16]byte + epochNow := time.Now().Unix() + binary.BigEndian.PutUint32(r[0:4], uint32(epochNow)) + rand.Read(r[4:]) + fmt.Printf("%s", hex.EncodeToString(r[:])) +} \ No newline at end of file diff --git a/test/app_signals/resources/traces/traces.json b/test/app_signals/resources/traces/traces.json new file mode 100644 index 000000000..b809f1dd7 --- /dev/null +++ b/test/app_signals/resources/traces/traces.json @@ -0,0 +1,90 @@ +{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "k8s.namespace.name", + "value": { + "stringValue": "default" + } + }, + { + "key": "k8s.pod.name", + "value": { + "stringValue": "pod-name" + } + }, + { + "key": "aws.deployment.name", + "value": { + "stringValue": "deployment-name" + } + }, + { + "key": "host.id", + "value": { + "stringValue": "i-00000000000000000" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "app-signals-integration-test" + }, + "spans": [ + { + "traceId": "TRACE_ID", + "spanId": "EEE19B7EC3C1B174", + "parentSpanId": "EEE19B7EC3C1B173", + "name": "app-signals-integration-test-traces", + "startTimeUnixNano": START_TIME, + "endTimeUnixNano": START_TIME, + "kind": 2, + "attributes": [ + { + "key": "aws.span.kind", + "value": { + "stringValue": "CLIENT" + } + }, + { + "key": "aws.local.operation", + "value": { + "stringValue": "operation" + } + }, + { + "key": "aws.local.service", + "value": { + "stringValue": "service-name" + } + }, + { + "key": "aws.remote.operation", + "value": { + "stringValue": "remote-operation" + } + }, + { + "key": "aws.remote.service", + "value": { + "stringValue": "service-name-remote" + } + }, + { + "key": "aws.remote.target", + "value": { + "stringValue": "remote-target" + } + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/app_signals/traces_test.go b/test/app_signals/traces_test.go new file mode 100644 index 000000000..fe84a2b0f --- /dev/null +++ b/test/app_signals/traces_test.go @@ -0,0 +1,77 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +//go:build !windows + +package app_signals + +import ( + "fmt" + "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" +) + +const ( + lookbackDuration = time.Duration(-5) * time.Minute + EKSClusterAnnotation = "HostedIn_EKS_Cluster" +) + +var annotations = map[string]interface{}{ + "aws_remote_target": "remote-target", + "aws_remote_operation": "remote-operation", + "aws_local_service": "service-name", + "aws_remote_service": "service-name-remote", + "HostedIn_K8s_Namespace": "default", + "aws_local_operation": "operation", +} + +type AppSignalsTracesRunner struct { + test_runner.BaseTestRunner + testName string + clusterName string +} + +func (t *AppSignalsTracesRunner) Validate() status.TestGroupResult { + testResults := status.TestResult{ + Name: t.testName, + Status: status.FAILED, + } + timeNow := time.Now() + annotations[EKSClusterAnnotation] = t.clusterName + xrayFilter := awsservice.FilterExpression(annotations) + traceIds, err := awsservice.GetTraceIDs(timeNow.Add(lookbackDuration), timeNow, xrayFilter) + if err != nil { + fmt.Printf("error getting trace ids: %v", err) + } else { + fmt.Printf("Trace IDs: %v\n", traceIds) + if len(traceIds) > 0 { + testResults.Status = status.SUCCESSFUL + } + } + + return status.TestGroupResult{ + Name: t.GetTestName(), + TestResults: []status.TestResult{testResults}, + } +} + +func (t *AppSignalsTracesRunner) GetTestName() string { + return t.testName +} + +func (t *AppSignalsTracesRunner) GetAgentRunDuration() time.Duration { + return 3 * time.Minute +} + +func (t *AppSignalsTracesRunner) GetMeasuredMetrics() []string { + return nil +} + +func (e *AppSignalsTracesRunner) GetAgentConfigFileName() string { + return "" +} + +var _ test_runner.ITestRunner = (*AppSignalsTracesRunner)(nil) diff --git a/test/metric/app_signals_util.go b/test/metric/app_signals_util.go new file mode 100644 index 000000000..781209ab8 --- /dev/null +++ b/test/metric/app_signals_util.go @@ -0,0 +1,96 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +//go:build !windows + +package metric + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + + "github.com/aws/amazon-cloudwatch-agent-test/test/metric/dimension" + "github.com/aws/amazon-cloudwatch-agent-test/test/status" +) + +var ( + AppSignalsMetricNames = []string{ + "Error", + "Fault", + "Latency", + } + + ServerConsumerInstructions = []dimension.Instruction{ + { + Key: "HostedIn.EKS.Cluster", + Value: dimension.UnknownDimensionValue(), + }, + { + Key: "HostedIn.K8s.Namespace", + Value: dimension.ExpectedDimensionValue{Value: aws.String("default")}, + }, + { + Key: "Service", + Value: dimension.ExpectedDimensionValue{Value: aws.String("service-name")}, + }, + { + Key: "Operation", + Value: dimension.ExpectedDimensionValue{Value: aws.String("operation")}, + }, + } + + ClientProducerInstructions = []dimension.Instruction{ + { + Key: "HostedIn.EKS.Cluster", + Value: dimension.UnknownDimensionValue(), + }, + { + Key: "HostedIn.K8s.Namespace", + Value: dimension.ExpectedDimensionValue{Value: aws.String("default")}, + }, + { + Key: "Service", + Value: dimension.ExpectedDimensionValue{Value: aws.String("service-name")}, + }, + { + Key: "RemoteService", + Value: dimension.ExpectedDimensionValue{Value: aws.String("service-name-remote")}, + }, + { + Key: "Operation", + Value: dimension.ExpectedDimensionValue{Value: aws.String("operation")}, + }, + { + Key: "RemoteOperation", + Value: dimension.ExpectedDimensionValue{Value: aws.String("remote-operation")}, + }, + { + Key: "RemoteTarget", + Value: dimension.ExpectedDimensionValue{Value: aws.String("remote-target")}, + }, + } +) + +func ValidateAppSignalsMetric(dimFactory dimension.Factory, namespace string, metricName string, instructions []dimension.Instruction) status.TestResult { + testResult := status.TestResult{ + Name: metricName, + Status: status.FAILED, + } + + dims, failed := dimFactory.GetDimensions(instructions) + if len(failed) > 0 { + return testResult + } + + fetcher := MetricValueFetcher{} + values, err := fetcher.Fetch(namespace, metricName, dims, SUM, HighResolutionStatPeriod) + if err != nil { + return testResult + } + + if !IsAllValuesGreaterThanOrEqualToExpectedValue(metricName, values, 0) { + return testResult + } + + testResult.Status = status.SUCCESSFUL + return testResult +} diff --git a/test/metric/dimension/instanceid_provider.go b/test/metric/dimension/instanceid_provider.go index 3ecdbf377..c07b09e38 100644 --- a/test/metric/dimension/instanceid_provider.go +++ b/test/metric/dimension/instanceid_provider.go @@ -81,6 +81,14 @@ func (p *EKSClusterNameProvider) IsApplicable() bool { } func (p *EKSClusterNameProvider) GetDimension(instruction Instruction) types.Dimension { + // For AppSignals metrics, cluster name is under EKS.Cluster dimension + if instruction.Key == "HostedIn.EKS.Cluster" { + return types.Dimension{ + Name: aws.String("HostedIn.EKS.Cluster"), + Value: aws.String(p.env.EKSClusterName), + } + } + if instruction.Key != "ClusterName" || instruction.Value.IsKnown() { return types.Dimension{} } diff --git a/test/metric/stat.go b/test/metric/stat.go index ecb9615d3..763028566 100644 --- a/test/metric/stat.go +++ b/test/metric/stat.go @@ -11,5 +11,6 @@ const ( SAMPLE_COUNT Statistics = "SampleCount" MINIMUM Statistics = "Minimum" MAXUMUM Statistics = "Maxmimum" + SUM Statistics = "Sum" HighResolutionStatPeriod = 10 )