Repository: open-telemetry/opentelemetry-go-contrib
Pull request #2626 (2022-08-06 20:46:36)

Implement aws.ecs.* resource attributes
Implemented retrieving the [`aws.ecs.*` resource attributes](https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud_provider/aws/ecs/) in `go.opentelemetry.io/detectors/aws/ecs` based on the ECS Metadata v4 endpoint.

codecov[bot]: # [Codecov](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2626?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) Report
> Merging [#2626](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2626?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) (74496cc) into [main](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/commit/bfa71cf8537512b6bdbf99831455c5f0dfd716c6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) (bfa71cf) will **decrease** coverage by `0.0%`.
> The diff coverage is `70.0%`.

[![Impacted file tree graph](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2626/graphs/tree.svg?width=650&height=150&src=pr&token=P6F3W9WA7Q&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry)](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2626?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry)

```diff
@@           Coverage Diff           @@
##            main   #2626     +/-   ##
=======================================
- Coverage   74.3%   74.3%   -0.1%     
=======================================
  Files        144     144             
  Lines       6569    6598     +29     
=======================================
+ Hits        4883    4904     +21     
- Misses      1543    1549      +6     
- Partials     143     145      +2     
```


| [Impacted Files](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2626?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) | Coverage Δ | |
|---|---|---|
| [detectors/aws/ecs/ecs.go](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2626/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-ZGV0ZWN0b3JzL2F3cy9lY3MvZWNzLmdv) | `61.9% <70.0%> (+7.2%)` | :arrow_up: |

mmanciop: There's discussion on https://github.com/open-telemetry/opentelemetry-java/pull/4574#issuecomment-1213233736 whether the ARNs like the one this PR builds are actually correct. Until we are use, let's not merge :D
mmanciop: **Update:** The uncertainty about ARNs seems sorted out
bryan-aguilar: Can we add test cases that utilize the [fargate examples responses](https://docs.aws.amazon.com/AmazonECS/latest/userguide/task-metadata-endpoint-v4-fargate.html) also?
bryan-aguilar: Looks like there may be slight differences between fargate and ec2 metadata response. As a sanity check can we add those example responses linked in the comment above to the unit tests?
mmanciop: > Looks like there may be slight differences between fargate and ec2 metadata response. As a sanity check can we add those example responses linked in the comment above to the unit tests?

Done in 2ef2026553d481e14f3c5c051dc9df5afc8a2bb7. There are no factual differences that this patch cares about.
Aneurysm9: All of the log group/stream fields are optional.  There is no requirement to include all of them if any of them are included.
mmanciop: > All of the log group/stream fields are optional. There is no requirement to include all of them if any of them are included.

IMO, this would wreak havoc with less sophisticated tracing backends that do not have processing capabilities to normalize the attributes, as the same Log stream or group could be represented with different values. 
mmanciop: Rebased on main, solved a bunch of merge conflicts; @Aneurysm9 care to have a look?
Aneurysm9: @mmanciop This requires an additional approval from @open-telemetry/go-approvers before it can be merged.
mmanciop: @Aneurysm9 does this ring a bell?

> panic: httptest: failed to listen on a port: listen tcp6 [::1]:0: socket: The requested service provider could not be loaded or initialized. [recovered]
> 	panic: httptest: failed to listen on a port: listen tcp6 [::1]:0: socket: The requested service provider could not be loaded or initialized.

I copied the test code from the AksDetector and the tests pass on my Mac; am I perhaps missing something in the CI configs?

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3b624b21b..4ce8ae133 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 
 ### Added
 
+- Implemented retrieving the [`aws.ecs.*` resource attributes](https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud_provider/aws/ecs/) in `go.opentelemetry.io/detectors/aws/ecs` based on the ECS Metadata v4 endpoint.
 - The `WithLogger` option to `go.opentelemetry.io/contrib/samplers/jaegerremote` to allow users to pass a `logr.Logger` and have operations logged. (#2566)
 - Add the `messaging.url` & `messaging.system` attributes to all appropriate SQS operations in the `go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws` package. (#2879)
 - Add example use of the metrics signal to `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/example`. (#2610)
diff --git a/detectors/aws/ecs/ecs.go b/detectors/aws/ecs/ecs.go
index 236288a6f..34c3ba83d 100644
--- a/detectors/aws/ecs/ecs.go
+++ b/detectors/aws/ecs/ecs.go
@@ -17,9 +17,15 @@ package ecs // import "go.opentelemetry.io/contrib/detectors/aws/ecs"
 import (
 	"context"
 	"errors"
+	"fmt"
+	"net/http"
 	"os"
+	"regexp"
+	"runtime"
 	"strings"
 
+	ecsmetadata "github.com/brunoscheufler/aws-ecs-metadata-go"
+
 	"go.opentelemetry.io/otel/attribute"
 	"go.opentelemetry.io/otel/sdk/resource"
 	semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
@@ -35,10 +41,10 @@ const (
 )
 
 var (
-	empty                      = resource.Empty()
-	errCannotReadContainerID   = errors.New("failed to read container ID from cGroupFile")
-	errCannotReadContainerName = errors.New("failed to read hostname")
-	errCannotReadCGroupFile    = errors.New("ECS resource detector failed to read cGroupFile")
+	empty                                 = resource.Empty()
+	errCannotReadContainerName            = errors.New("failed to read hostname")
+	errCannotRetrieveLogsGroupMetadataV4  = errors.New("the ECS Metadata v4 did not return a AwsLogGroup name")
+	errCannotRetrieveLogsStreamMetadataV4 = errors.New("the ECS Metadata v4 did not return a AwsLogStream name")
 )
 
 // Create interface for methods needing to be mocked.
@@ -63,7 +69,9 @@ var _ resource.Detector = (*resourceDetector)(nil)
 
 // NewResourceDetector returns a resource detector that will detect AWS ECS resources.
 func NewResourceDetector() resource.Detector {
-	return &resourceDetector{utils: ecsDetectorUtils{}}
+	return &resourceDetector{
+		utils: ecsDetectorUtils{},
+	}
 }
 
 // Detect finds associated resources when running on ECS environment.
@@ -89,14 +97,94 @@ func (detector *resourceDetector) Detect(ctx context.Context) (*resource.Resourc
 		semconv.ContainerIDKey.String(containerID),
 	}
 
+	if len(metadataURIV4) > 0 {
+		containerMetadata, err := ecsmetadata.GetContainerV4(ctx, &http.Client{})
+		if err != nil {
+			return empty, err
+		}
+		attributes = append(
+			attributes,
+			semconv.AWSECSContainerARNKey.String(containerMetadata.ContainerARN),
+		)
+
+		taskMetadata, err := ecsmetadata.GetTaskV4(ctx, &http.Client{})
+		if err != nil {
+			return empty, err
+		}
+
+		clusterArn := taskMetadata.Cluster
+		if !strings.HasPrefix(clusterArn, "arn:") {
+			baseArn := containerMetadata.ContainerARN[:strings.LastIndex(containerMetadata.ContainerARN, ":")]
+			clusterArn = fmt.Sprintf("%s:cluster/%s", baseArn, clusterArn)
+		}
+
+		logAttributes, err := detector.getLogsAttributes(containerMetadata)
+		if err != nil {
+			return empty, err
+		}
+
+		if len(logAttributes) > 0 {
+			attributes = append(attributes, logAttributes...)
+		}
+
+		attributes = append(
+			attributes,
+			semconv.AWSECSClusterARNKey.String(clusterArn),
+			semconv.AWSECSLaunchtypeKey.String(strings.ToLower(taskMetadata.LaunchType)),
+			semconv.AWSECSTaskARNKey.String(taskMetadata.TaskARN),
+			semconv.AWSECSTaskFamilyKey.String(taskMetadata.Family),
+			semconv.AWSECSTaskRevisionKey.String(taskMetadata.Revision),
+		)
+	}
+
 	return resource.NewWithAttributes(semconv.SchemaURL, attributes...), nil
 }
 
+func (detector *resourceDetector) getLogsAttributes(metadata *ecsmetadata.ContainerMetadataV4) ([]attribute.KeyValue, error) {
+	if metadata.LogDriver != "awslogs" {
+		return []attribute.KeyValue{}, nil
+	}
+
+	logsOptions := metadata.LogOptions
+
+	if len(logsOptions.AwsLogsGroup) < 1 {
+		return nil, errCannotRetrieveLogsGroupMetadataV4
+	}
+
+	if len(logsOptions.AwsLogsStream) < 1 {
+		return nil, errCannotRetrieveLogsStreamMetadataV4
+	}
+
+	containerArn := metadata.ContainerARN
+	logsRegion := logsOptions.AwsRegion
+	if len(logsRegion) < 1 {
+		r := regexp.MustCompile(`arn:aws:ecs:([^:]+):.*`)
+		logsRegion = r.FindStringSubmatch(containerArn)[1]
+	}
+
+	r := regexp.MustCompile(`arn:aws:ecs:[^:]+:([^:]+):.*`)
+	awsAccount := r.FindStringSubmatch(containerArn)[1]
+
+	return []attribute.KeyValue{
+		semconv.AWSLogGroupNamesKey.String(logsOptions.AwsLogsGroup),
+		semconv.AWSLogGroupARNsKey.String(fmt.Sprintf("arn:aws:logs:%s:%s:log-group:%s:*", logsRegion, awsAccount, logsOptions.AwsLogsGroup)),
+		semconv.AWSLogStreamNamesKey.String(logsOptions.AwsLogsStream),
+		semconv.AWSLogStreamARNsKey.String(fmt.Sprintf("arn:aws:logs:%s:%s:log-group:%s:log-stream:%s", logsRegion, awsAccount, logsOptions.AwsLogsGroup, logsOptions.AwsLogsStream)),
+	}, nil
+}
+
 // returns docker container ID from default c group path.
 func (ecsUtils ecsDetectorUtils) getContainerID() (string, error) {
+	if runtime.GOOS != "linux" {
+		// Cgroups are used only under Linux.
+		return "", nil
+	}
+
 	fileData, err := os.ReadFile(defaultCgroupPath)
 	if err != nil {
-		return "", errCannotReadCGroupFile
+		// Cgroups file not found.
+		// For example, windows; or when running integration tests outside of a container.
+		return "", nil
 	}
 	splitData := strings.Split(strings.TrimSpace(string(fileData)), "\n")
 	for _, str := range splitData {
@@ -104,7 +192,7 @@ func (ecsUtils ecsDetectorUtils) getContainerID() (string, error) {
 			return str[len(str)-containerIDLength:], nil
 		}
 	}
-	return "", errCannotReadContainerID
+	return "", nil
 }
 
 // returns host name reported by the kernel.
diff --git a/detectors/aws/ecs/go.mod b/detectors/aws/ecs/go.mod
index 12faa850b..ca0f5b665 100644
--- a/detectors/aws/ecs/go.mod
+++ b/detectors/aws/ecs/go.mod
@@ -3,6 +3,7 @@ module go.opentelemetry.io/contrib/detectors/aws/ecs
 go 1.18
 
 require (
+	github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220812150832-b6b31c6eeeaf
 	github.com/stretchr/testify v1.8.1
 	go.opentelemetry.io/otel v1.11.1
 	go.opentelemetry.io/otel/sdk v1.11.1
diff --git a/detectors/aws/ecs/go.sum b/detectors/aws/ecs/go.sum
index 99accfa63..accc3385a 100644
--- a/detectors/aws/ecs/go.sum
+++ b/detectors/aws/ecs/go.sum
@@ -1,3 +1,5 @@
+github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220812150832-b6b31c6eeeaf h1:WCnJxXZXx9c8gwz598wvdqmu+YTzB9wx2X1OovK3Le8=
+github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220812150832-b6b31c6eeeaf/go.mod h1:CeKhh8xSs3WZAc50xABMxu+FlfAAd5PNumo7NfOv7EE=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=


Test patch:
diff --git a/detectors/aws/ecs/ecs_test.go b/detectors/aws/ecs/ecs_test.go
index 71b2c1e54..4d155809d 100644
--- a/detectors/aws/ecs/ecs_test.go
+++ b/detectors/aws/ecs/ecs_test.go
@@ -19,12 +19,12 @@ import (
 	"os"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/mock"
-
 	"go.opentelemetry.io/otel/attribute"
 	"go.opentelemetry.io/otel/sdk/resource"
 	semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 )
 
 // Create interface for functions that need to be mocked.
@@ -42,11 +42,11 @@ func (detectorUtils *MockDetectorUtils) getContainerName() (string, error) {
 	return args.String(0), args.Error(1)
 }
 
-// successfully return resource when process is running on Amazon ECS environment.
-func TestDetect(t *testing.T) {
+// successfully returns resource when process is running on Amazon ECS environment
+// with no Metadata v4.
+func TestDetectV3(t *testing.T) {
 	os.Clearenv()
 	_ = os.Setenv(metadataV3EnvVar, "3")
-	_ = os.Setenv(metadataV4EnvVar, "4")
 
 	detectorUtils := new(MockDetectorUtils)
 
@@ -63,24 +63,30 @@ func TestDetect(t *testing.T) {
 	detector := &resourceDetector{utils: detectorUtils}
 	res, _ := detector.Detect(context.Background())
 
-	assert.Equal(t, res, expectedResource, "Resource returned is incorrect")
+	assert.Equal(t, expectedResource, res, "Resource returned is incorrect")
 }
 
 // returns empty resource when detector cannot read container ID.
 func TestDetectCannotReadContainerID(t *testing.T) {
 	os.Clearenv()
 	_ = os.Setenv(metadataV3EnvVar, "3")
-	_ = os.Setenv(metadataV4EnvVar, "4")
 	detectorUtils := new(MockDetectorUtils)
 
 	detectorUtils.On("getContainerName").Return("container-Name", nil)
-	detectorUtils.On("getContainerID").Return("", errCannotReadContainerID)
+	detectorUtils.On("getContainerID").Return("", nil)
 
+	attributes := []attribute.KeyValue{
+		semconv.CloudProviderAWS,
+		semconv.CloudPlatformAWSECS,
+		semconv.ContainerNameKey.String("container-Name"),
+		semconv.ContainerIDKey.String(""),
+	}
+	expectedResource := resource.NewWithAttributes(semconv.SchemaURL, attributes...)
 	detector := &resourceDetector{utils: detectorUtils}
 	res, err := detector.Detect(context.Background())
 
-	assert.Equal(t, errCannotReadContainerID, err)
-	assert.Equal(t, 0, len(res.Attributes()))
+	assert.Equal(t, nil, err)
+	assert.Equal(t, expectedResource, res, "Resource returned is incorrect")
 }
 
 // returns empty resource when detector cannot read container Name.
diff --git a/detectors/aws/ecs/test/ecs_test.go b/detectors/aws/ecs/test/ecs_test.go
new file mode 100644
index 000000000..1cdcc9686
--- /dev/null
+++ b/detectors/aws/ecs/test/ecs_test.go
@@ -0,0 +1,149 @@
+// Copyright The OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ecs
+
+import (
+	"context"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"strings"
+	"testing"
+
+	ecs "go.opentelemetry.io/contrib/detectors/aws/ecs"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/sdk/resource"
+	semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	metadataV4EnvVar = "ECS_CONTAINER_METADATA_URI_V4"
+)
+
+// successfully returns resource when process is running on Amazon ECS environment
+// with Metadata v4 with the EC2 Launch type.
+func TestDetectV4LaunchTypeEc2(t *testing.T) {
+	testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+		if strings.HasSuffix(req.URL.String(), "/task") {
+			content, err := os.ReadFile("metadatav4-response-task-ec2.json")
+			if err == nil {
+				_, err = res.Write(content)
+				if err != nil {
+					t.Fatal(err)
+				}
+			}
+		} else {
+			content, err := os.ReadFile("metadatav4-response-container-ec2.json")
+			if err == nil {
+				_, err = res.Write(content)
+				if err != nil {
+					t.Fatal(err)
+				}
+			}
+		}
+	}))
+	defer testServer.Close()
+
+	os.Clearenv()
+	_ = os.Setenv(metadataV4EnvVar, testServer.URL)
+
+	hostname, err := os.Hostname()
+	assert.NoError(t, err, "Error")
+
+	attributes := []attribute.KeyValue{
+		semconv.CloudProviderAWS,
+		semconv.CloudPlatformAWSECS,
+		semconv.ContainerNameKey.String(hostname),
+		// We are not running the test in an actual container,
+		// the container id is tested with mocks of the cgroup
+		// file in the unit tests
+		semconv.ContainerIDKey.String(""),
+		semconv.AWSECSContainerARNKey.String("arn:aws:ecs:us-west-2:111122223333:container/0206b271-b33f-47ab-86c6-a0ba208a70a9"),
+		semconv.AWSECSClusterARNKey.String("arn:aws:ecs:us-west-2:111122223333:cluster/default"),
+		semconv.AWSECSLaunchtypeKey.String("ec2"),
+		semconv.AWSECSTaskARNKey.String("arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c"),
+		semconv.AWSECSTaskFamilyKey.String("curltest"),
+		semconv.AWSECSTaskRevisionKey.String("26"),
+		semconv.AWSLogGroupNamesKey.String("/ecs/metadata"),
+		semconv.AWSLogGroupARNsKey.String("arn:aws:logs:us-west-2:111122223333:log-group:/ecs/metadata:*"),
+		semconv.AWSLogStreamNamesKey.String("ecs/curl/8f03e41243824aea923aca126495f665"),
+		semconv.AWSLogStreamARNsKey.String("arn:aws:logs:us-west-2:111122223333:log-group:/ecs/metadata:log-stream:ecs/curl/8f03e41243824aea923aca126495f665"),
+	}
+	expectedResource := resource.NewWithAttributes(semconv.SchemaURL, attributes...)
+	detector := ecs.NewResourceDetector()
+	res, err := detector.Detect(context.Background())
+
+	assert.Equal(t, nil, err, "Detector should not fail")
+	assert.Equal(t, expectedResource, res, "Resource returned is incorrect")
+}
+
+// successfully returns resource when process is running on Amazon ECS environment
+// with Metadata v4 with the Fargate Launch type.
+func TestDetectV4LaunchTypeFargate(t *testing.T) {
+	testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+		if strings.HasSuffix(req.URL.String(), "/task") {
+			content, err := os.ReadFile("metadatav4-response-task-fargate.json")
+			if err == nil {
+				_, err = res.Write(content)
+				if err != nil {
+					panic(err)
+				}
+			}
+		} else {
+			content, err := os.ReadFile("metadatav4-response-container-fargate.json")
+			if err == nil {
+				_, err = res.Write(content)
+				if err != nil {
+					panic(err)
+				}
+			}
+		}
+	}))
+	defer testServer.Close()
+
+	os.Clearenv()
+	_ = os.Setenv(metadataV4EnvVar, testServer.URL)
+
+	hostname, err := os.Hostname()
+	assert.NoError(t, err, "Error")
+
+	attributes := []attribute.KeyValue{
+		semconv.CloudProviderAWS,
+		semconv.CloudPlatformAWSECS,
+		semconv.ContainerNameKey.String(hostname),
+		// We are not running the test in an actual container,
+		// the container id is tested with mocks of the cgroup
+		// file in the unit tests
+		semconv.ContainerIDKey.String(""),
+		semconv.AWSECSContainerARNKey.String("arn:aws:ecs:us-west-2:111122223333:container/05966557-f16c-49cb-9352-24b3a0dcd0e1"),
+		semconv.AWSECSClusterARNKey.String("arn:aws:ecs:us-west-2:111122223333:cluster/default"),
+		semconv.AWSECSLaunchtypeKey.String("fargate"),
+		semconv.AWSECSTaskARNKey.String("arn:aws:ecs:us-west-2:111122223333:task/default/e9028f8d5d8e4f258373e7b93ce9a3c3"),
+		semconv.AWSECSTaskFamilyKey.String("curltest"),
+		semconv.AWSECSTaskRevisionKey.String("3"),
+		semconv.AWSLogGroupNamesKey.String("/ecs/containerlogs"),
+		semconv.AWSLogGroupARNsKey.String("arn:aws:logs:us-west-2:111122223333:log-group:/ecs/containerlogs:*"),
+		semconv.AWSLogStreamNamesKey.String("ecs/curl/cd189a933e5849daa93386466019ab50"),
+		semconv.AWSLogStreamARNsKey.String("arn:aws:logs:us-west-2:111122223333:log-group:/ecs/containerlogs:log-stream:ecs/curl/cd189a933e5849daa93386466019ab50"),
+	}
+	expectedResource := resource.NewWithAttributes(semconv.SchemaURL, attributes...)
+	detector := ecs.NewResourceDetector()
+	res, err := detector.Detect(context.Background())
+
+	assert.Equal(t, nil, err, "Detector should not fail")
+	assert.Equal(t, expectedResource, res, "Resource returned is incorrect")
+}
diff --git a/detectors/aws/ecs/test/metadatav4-response-container-ec2.json b/detectors/aws/ecs/test/metadatav4-response-container-ec2.json
new file mode 100644
index 000000000..b43c2b0d7
--- /dev/null
+++ b/detectors/aws/ecs/test/metadatav4-response-container-ec2.json
@@ -0,0 +1,44 @@
+{
+    "DockerId": "ea32192c8553fbff06c9340478a2ff089b2bb5646fb718b4ee206641c9086d66",
+    "Name": "curl",
+    "DockerName": "ecs-curltest-24-curl-cca48e8dcadd97805600",
+    "Image": "111122223333.dkr.ecr.us-west-2.amazonaws.com/curltest:latest",
+    "ImageID": "sha256:d691691e9652791a60114e67b365688d20d19940dde7c4736ea30e660d8d3553",
+    "Labels": {
+        "com.amazonaws.ecs.cluster": "default",
+        "com.amazonaws.ecs.container-name": "curl",
+        "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:111122223333:task/default/8f03e41243824aea923aca126495f665",
+        "com.amazonaws.ecs.task-definition-family": "curltest",
+        "com.amazonaws.ecs.task-definition-version": "24"
+    },
+    "DesiredStatus": "RUNNING",
+    "KnownStatus": "RUNNING",
+    "Limits": {
+        "CPU": 10,
+        "Memory": 128
+    },
+    "CreatedAt": "2020-10-02T00:15:07.620912337Z",
+    "StartedAt": "2020-10-02T00:15:08.062559351Z",
+    "Type": "NORMAL",
+    "LogDriver": "awslogs",
+    "LogOptions": {
+        "awslogs-create-group": "true",
+        "awslogs-group": "/ecs/metadata",
+        "awslogs-region": "us-west-2",
+        "awslogs-stream": "ecs/curl/8f03e41243824aea923aca126495f665"
+    },
+    "ContainerARN": "arn:aws:ecs:us-west-2:111122223333:container/0206b271-b33f-47ab-86c6-a0ba208a70a9",
+    "Networks": [
+        {
+            "NetworkMode": "awsvpc",
+            "IPv4Addresses": [
+                "10.0.2.100"
+            ],
+            "AttachmentIndex": 0,
+            "MACAddress": "0e:9e:32:c7:48:85",
+            "IPv4SubnetCIDRBlock": "10.0.2.0/24",
+            "PrivateDNSName": "ip-10-0-2-100.us-west-2.compute.internal",
+            "SubnetGatewayIpv4Address": "10.0.2.1/24"
+        }
+    ]
+}
diff --git a/detectors/aws/ecs/test/metadatav4-response-container-fargate.json b/detectors/aws/ecs/test/metadatav4-response-container-fargate.json
new file mode 100644
index 000000000..ccbe70bc4
--- /dev/null
+++ b/detectors/aws/ecs/test/metadatav4-response-container-fargate.json
@@ -0,0 +1,50 @@
+{
+    "DockerId": "cd189a933e5849daa93386466019ab50-2495160603",
+    "Name": "curl",
+    "DockerName": "curl",
+    "Image": "111122223333.dkr.ecr.us-west-2.amazonaws.com/curltest:latest",
+    "ImageID": "sha256:25f3695bedfb454a50f12d127839a68ad3caf91e451c1da073db34c542c4d2cb",
+    "Labels": {
+        "com.amazonaws.ecs.cluster": "arn:aws:ecs:us-west-2:111122223333:cluster/default",
+        "com.amazonaws.ecs.container-name": "curl",
+        "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:111122223333:task/default/cd189a933e5849daa93386466019ab50",
+        "com.amazonaws.ecs.task-definition-family": "curltest",
+        "com.amazonaws.ecs.task-definition-version": "2"
+    },
+    "DesiredStatus": "RUNNING",
+    "KnownStatus": "RUNNING",
+    "Limits": {
+        "CPU": 10,
+        "Memory": 128
+    },
+    "CreatedAt": "2020-10-08T20:09:11.44527186Z",
+    "StartedAt": "2020-10-08T20:09:11.44527186Z",
+    "Type": "NORMAL",
+    "Networks": [
+        {
+            "NetworkMode": "awsvpc",
+            "IPv4Addresses": [
+                "192.0.2.3"
+            ],
+            "AttachmentIndex": 0,
+            "MACAddress": "0a:de:f6:10:51:e5",
+            "IPv4SubnetCIDRBlock": "192.0.2.0/24",
+            "DomainNameServers": [
+                "192.0.2.2"
+            ],
+            "DomainNameSearchList": [
+                "us-west-2.compute.internal"
+            ],
+            "PrivateDNSName": "ip-10-0-0-222.us-west-2.compute.internal",
+            "SubnetGatewayIpv4Address": "192.0.2.0/24"
+        }
+    ],
+    "ContainerARN": "arn:aws:ecs:us-west-2:111122223333:container/05966557-f16c-49cb-9352-24b3a0dcd0e1",
+    "LogOptions": {
+        "awslogs-create-group": "true",
+        "awslogs-group": "/ecs/containerlogs",
+        "awslogs-region": "us-west-2",
+        "awslogs-stream": "ecs/curl/cd189a933e5849daa93386466019ab50"
+    },
+    "LogDriver": "awslogs"
+}
\ No newline at end of file
diff --git a/detectors/aws/ecs/test/metadatav4-response-task-ec2.json b/detectors/aws/ecs/test/metadatav4-response-task-ec2.json
new file mode 100644
index 000000000..101efe021
--- /dev/null
+++ b/detectors/aws/ecs/test/metadatav4-response-task-ec2.json
@@ -0,0 +1,94 @@
+{
+    "Cluster": "default",
+    "TaskARN": "arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c",
+    "Family": "curltest",
+    "Revision": "26",
+    "DesiredStatus": "RUNNING",
+    "KnownStatus": "RUNNING",
+    "PullStartedAt": "2020-10-02T00:43:06.202617438Z",
+    "PullStoppedAt": "2020-10-02T00:43:06.31288465Z",
+    "AvailabilityZone": "us-west-2d",
+    "LaunchType": "EC2",
+    "Containers": [
+        {
+            "DockerId": "598cba581fe3f939459eaba1e071d5c93bb2c49b7d1ba7db6bb19deeb70d8e38",
+            "Name": "~internal~ecs~pause",
+            "DockerName": "ecs-curltest-26-internalecspause-e292d586b6f9dade4a00",
+            "Image": "amazon/amazon-ecs-pause:0.1.0",
+            "ImageID": "",
+            "Labels": {
+                "com.amazonaws.ecs.cluster": "default",
+                "com.amazonaws.ecs.container-name": "~internal~ecs~pause",
+                "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c",
+                "com.amazonaws.ecs.task-definition-family": "curltest",
+                "com.amazonaws.ecs.task-definition-version": "26"
+            },
+            "DesiredStatus": "RESOURCES_PROVISIONED",
+            "KnownStatus": "RESOURCES_PROVISIONED",
+            "Limits": {
+                "CPU": 0,
+                "Memory": 0
+            },
+            "CreatedAt": "2020-10-02T00:43:05.602352471Z",
+            "StartedAt": "2020-10-02T00:43:06.076707576Z",
+            "Type": "CNI_PAUSE",
+            "Networks": [
+                {
+                    "NetworkMode": "awsvpc",
+                    "IPv4Addresses": [
+                        "10.0.2.61"
+                    ],
+                    "AttachmentIndex": 0,
+                    "MACAddress": "0e:10:e2:01:bd:91",
+                    "IPv4SubnetCIDRBlock": "10.0.2.0/24",
+                    "PrivateDNSName": "ip-10-0-2-61.us-west-2.compute.internal",
+                    "SubnetGatewayIpv4Address": "10.0.2.1/24"
+                }
+            ]
+        },
+        {
+            "DockerId": "ee08638adaaf009d78c248913f629e38299471d45fe7dc944d1039077e3424ca",
+            "Name": "curl",
+            "DockerName": "ecs-curltest-26-curl-a0e7dba5aca6d8cb2e00",
+            "Image": "111122223333.dkr.ecr.us-west-2.amazonaws.com/curltest:latest",
+            "ImageID": "sha256:d691691e9652791a60114e67b365688d20d19940dde7c4736ea30e660d8d3553",
+            "Labels": {
+                "com.amazonaws.ecs.cluster": "default",
+                "com.amazonaws.ecs.container-name": "curl",
+                "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c",
+                "com.amazonaws.ecs.task-definition-family": "curltest",
+                "com.amazonaws.ecs.task-definition-version": "26"
+            },
+            "DesiredStatus": "RUNNING",
+            "KnownStatus": "RUNNING",
+            "Limits": {
+                "CPU": 10,
+                "Memory": 128
+            },
+            "CreatedAt": "2020-10-02T00:43:06.326590752Z",
+            "StartedAt": "2020-10-02T00:43:06.767535449Z",
+            "Type": "NORMAL",
+            "LogDriver": "awslogs",
+            "LogOptions": {
+                "awslogs-create-group": "true",
+                "awslogs-group": "/ecs/metadata",
+                "awslogs-region": "us-west-2",
+                "awslogs-stream": "ecs/curl/158d1c8083dd49d6b527399fd6414f5c"
+            },
+            "ContainerARN": "arn:aws:ecs:us-west-2:111122223333:container/abb51bdd-11b4-467f-8f6c-adcfe1fe059d",
+            "Networks": [
+                {
+                    "NetworkMode": "awsvpc",
+                    "IPv4Addresses": [
+                        "10.0.2.61"
+                    ],
+                    "AttachmentIndex": 0,
+                    "MACAddress": "0e:10:e2:01:bd:91",
+                    "IPv4SubnetCIDRBlock": "10.0.2.0/24",
+                    "PrivateDNSName": "ip-10-0-2-61.us-west-2.compute.internal",
+                    "SubnetGatewayIpv4Address": "10.0.2.1/24"
+                }
+            ]
+        }
+    ]
+}
diff --git a/detectors/aws/ecs/test/metadatav4-response-task-fargate.json b/detectors/aws/ecs/test/metadatav4-response-task-fargate.json
new file mode 100644
index 000000000..7979db708
--- /dev/null
+++ b/detectors/aws/ecs/test/metadatav4-response-task-fargate.json
@@ -0,0 +1,77 @@
+{
+    "Cluster": "arn:aws:ecs:us-west-2:111122223333:cluster/default",
+    "TaskARN": "arn:aws:ecs:us-west-2:111122223333:task/default/e9028f8d5d8e4f258373e7b93ce9a3c3",
+    "Family": "curltest",
+    "Revision": "3",
+    "DesiredStatus": "RUNNING",
+    "KnownStatus": "RUNNING",
+    "Limits": {
+        "CPU": 0.25,
+        "Memory": 512
+    },
+    "PullStartedAt": "2020-10-08T20:47:16.053330955Z",
+    "PullStoppedAt": "2020-10-08T20:47:19.592684631Z",
+    "AvailabilityZone": "us-west-2a",
+    "Containers": [
+        {
+            "DockerId": "e9028f8d5d8e4f258373e7b93ce9a3c3-2495160603",
+            "Name": "curl",
+            "DockerName": "curl",
+            "Image": "111122223333.dkr.ecr.us-west-2.amazonaws.com/curltest:latest",
+            "ImageID": "sha256:25f3695bedfb454a50f12d127839a68ad3caf91e451c1da073db34c542c4d2cb",
+            "Labels": {
+                "com.amazonaws.ecs.cluster": "arn:aws:ecs:us-west-2:111122223333:cluster/default",
+                "com.amazonaws.ecs.container-name": "curl",
+                "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:111122223333:task/default/e9028f8d5d8e4f258373e7b93ce9a3c3",
+                "com.amazonaws.ecs.task-definition-family": "curltest",
+                "com.amazonaws.ecs.task-definition-version": "3"
+            },
+            "DesiredStatus": "RUNNING",
+            "KnownStatus": "RUNNING",
+            "Limits": {
+                "CPU": 10,
+                "Memory": 128
+            },
+            "CreatedAt": "2020-10-08T20:47:20.567813946Z",
+            "StartedAt": "2020-10-08T20:47:20.567813946Z",
+            "Type": "NORMAL",
+            "Networks": [
+                {
+                    "NetworkMode": "awsvpc",
+                    "IPv4Addresses": [
+                        "192.0.2.3"
+                    ],
+                    "IPv6Addresses": [
+                        "2001:dB8:10b:1a00:32bf:a372:d80f:e958"
+                    ],
+                    "AttachmentIndex": 0,
+                    "MACAddress": "02:b7:20:19:72:39",
+                    "IPv4SubnetCIDRBlock": "192.0.2.0/24",
+                    "IPv6SubnetCIDRBlock": "2600:1f13:10b:1a00::/64",
+                    "DomainNameServers": [
+                        "192.0.2.2"
+                    ],
+                    "DomainNameSearchList": [
+                        "us-west-2.compute.internal"
+                    ],
+                    "PrivateDNSName": "ip-172-31-30-173.us-west-2.compute.internal",
+                    "SubnetGatewayIpv4Address": "192.0.2.0/24"
+                }
+            ],
+            "ClockDrift": {
+                "ClockErrorBound": 0.5458234999999999,
+                "ReferenceTimestamp": "2021-09-07T16:57:44Z",
+                "ClockSynchronizationStatus": "SYNCHRONIZED"
+             },
+            "ContainerARN": "arn:aws:ecs:us-west-2:111122223333:container/1bdcca8b-f905-4ee6-885c-4064cb70f6e6",
+            "LogOptions": {
+                "awslogs-create-group": "true",
+                "awslogs-group": "/ecs/containerlogs",
+                "awslogs-region": "us-west-2",
+                "awslogs-stream": "ecs/curl/e9028f8d5d8e4f258373e7b93ce9a3c3"
+            },
+            "LogDriver": "awslogs"
+        }
+    ],
+    "LaunchType": "FARGATE"
+}
\ No newline at end of file
diff --git a/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/test/aws_test.go b/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/test/aws_test.go
index 2280821f1..ea87660d0 100644
--- a/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/test/aws_test.go
+++ b/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/test/aws_test.go
@@ -91,7 +91,7 @@ func TestAppendMiddlewares(t *testing.T) {
 	}
 
 	for name, c := range cases {
-		server := httptest.NewServer(http.HandlerFunc(
+		srv := httptest.NewServer(http.HandlerFunc(
 			func(w http.ResponseWriter, r *http.Request) {
 				w.WriteHeader(c.responseStatus)
 				_, err := w.Write(c.responseBody)
@@ -109,7 +109,7 @@ func TestAppendMiddlewares(t *testing.T) {
 				EndpointResolverWithOptions: aws.EndpointResolverWithOptionsFunc(
 					func(service, region string, _ ...interface{}) (aws.Endpoint, error) {
 						return aws.Endpoint{
-							URL:         server.URL,
+							URL:         srv.URL,
 							SigningName: "route53",
 						}, nil
 					},
@@ -151,6 +151,6 @@ func TestAppendMiddlewares(t *testing.T) {
 			assert.Contains(t, attrs, attribute.String("aws.operation", "ChangeResourceRecordSets"))
 		})
 
-		server.Close()
+		srv.Close()
 	}
 }