diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d1a258e846..2e11840606b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Added + +- Retrieve the [`aws.ecs.*`](https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud_provider/aws/ecs/) and [`aws.logs.*`](https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud_provider/aws/logs/) resource attributes in `go.opentelemetry.io/detectors/aws/ecs` based on the Amazon ECS Metadata v4 endpoint. + ## [1.10.0/0.35.0/0.5.0] ### Changed diff --git a/detectors/aws/ecs/ecs.go b/detectors/aws/ecs/ecs.go index 236288a6feb..454cca0b7fe 100644 --- a/detectors/aws/ecs/ecs.go +++ b/detectors/aws/ecs/ecs.go @@ -17,9 +17,13 @@ package ecs // import "go.opentelemetry.io/contrib/detectors/aws/ecs" import ( "context" "errors" + "fmt" + "net/http" "os" "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 +39,12 @@ 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() + 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") + errCannotRetrieveMetadataV4 = errors.New("ECS resource detector failed to retrieve metadata from the ECS Metada v4 container endpoint") + errCannotRetrieveMetadataV4Task = errors.New("ECS resource detector failed to retrieve metadata from the ECS Metada v4 task endpoint") ) // 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,6 +97,37 @@ 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) + } + + attributes = append( + attributes, + semconv.AWSECSClusterARNKey.String(clusterArn), + semconv.AWSECSLaunchtypeKey.String(taskMetadata.LaunchType), + semconv.AWSECSTaskARNKey.String(taskMetadata.TaskARN), + semconv.AWSECSTaskFamilyKey.String(taskMetadata.Family), + semconv.AWSECSTaskRevisionKey.String(taskMetadata.Revision), + ) + } + return resource.NewWithAttributes(semconv.SchemaURL, attributes...), nil } diff --git a/detectors/aws/ecs/ecs_test.go b/detectors/aws/ecs/ecs_test.go index 71b2c1e5451..ad6aa4d4586 100644 --- a/detectors/aws/ecs/ecs_test.go +++ b/detectors/aws/ecs/ecs_test.go @@ -16,7 +16,10 @@ package ecs import ( "context" + http "net/http" + "net/http/httptest" "os" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -42,11 +45,10 @@ 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) { +// succesfully return 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,7 +65,52 @@ 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") +} + +// succesfully return resource when process is running on Amazon ECS environment with Metadata v4. +func TestDetectV4(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("testdata/metadatav4-response-task.json") + if err == nil { + res.Write(content) + } + } else { + content, err := os.ReadFile("testdata/metadatav4-response-container.json") + if err == nil { + res.Write(content) + } + } + })) + defer func() { testServer.Close() }() + + os.Clearenv() + _ = os.Setenv(metadataV3EnvVar, "3") + _ = os.Setenv(metadataV4EnvVar, testServer.URL) + + detectorUtils := new(MockDetectorUtils) + + detectorUtils.On("getContainerName").Return("container-Name", nil) + detectorUtils.On("getContainerID").Return("0123456789A", nil) + + attributes := []attribute.KeyValue{ + semconv.CloudProviderAWS, + semconv.CloudPlatformAWSECS, + semconv.ContainerNameKey.String("container-Name"), + semconv.ContainerIDKey.String("0123456789A"), + 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"), + } + expectedResource := resource.NewWithAttributes(semconv.SchemaURL, attributes...) + detector := &resourceDetector{utils: detectorUtils} + res, _ := detector.Detect(context.Background()) + + assert.Equal(t, expectedResource, res, "Resource returned is incorrect") } // returns empty resource when detector cannot read container ID. diff --git a/detectors/aws/ecs/go.mod b/detectors/aws/ecs/go.mod index 00ddfa8ef4b..8d9e3b9b2fd 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.17 require ( + github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220806202253-1e93a1d357d6 github.com/stretchr/testify v1.8.0 go.opentelemetry.io/otel v1.10.0 go.opentelemetry.io/otel/sdk v1.10.0 diff --git a/detectors/aws/ecs/go.sum b/detectors/aws/ecs/go.sum index e6a225d2791..ae7d2f65681 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-20220806202253-1e93a1d357d6 h1:Izqf3e8tWDmFCnSRdmtej33tKPWlPYIvPPXqcI9qimM= +github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220806202253-1e93a1d357d6/go.mod h1:5BBxNAuQFkDjY3l9UHlJ8NnnM6NEi0bocZR2xc3xGxU= 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= diff --git a/detectors/aws/ecs/testdata/metadatav4-response-container.json b/detectors/aws/ecs/testdata/metadatav4-response-container.json new file mode 100644 index 00000000000..b43c2b0d7dc --- /dev/null +++ b/detectors/aws/ecs/testdata/metadatav4-response-container.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/testdata/metadatav4-response-task.json b/detectors/aws/ecs/testdata/metadatav4-response-task.json new file mode 100644 index 00000000000..101efe02148 --- /dev/null +++ b/detectors/aws/ecs/testdata/metadatav4-response-task.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" + } + ] + } + ] +}