diff --git a/CHANGELOG.md b/CHANGELOG.md index c416a72ea2c..ba75d1a28ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add the new `go.opentelemetry.io/contrib/instrgen` package to provide auto-generated source code instrumentation. (#3068, #3108) - Add client metric support to `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp`. (#4707) - Add peer attributes to spans recorded by `NewClientHandler`, `NewServerHandler` in `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc`. (#4873) +- Add support for `cloud.account.id`, `cloud.availability_zone` and `cloud.region` in the AWS ECS detector. (#4860) ### Changed diff --git a/detectors/aws/ecs/ecs.go b/detectors/aws/ecs/ecs.go index 57cc0b42ed6..284c006c4de 100644 --- a/detectors/aws/ecs/ecs.go +++ b/detectors/aws/ecs/ecs.go @@ -42,6 +42,7 @@ const ( var ( empty = resource.Empty() errCannotReadContainerName = errors.New("failed to read hostname") + errCannotParseTaskArn = errors.New("cannot parse region and account ID from the Task's ARN: the ARN does not contain at least 6 segments separated by the ':' character") 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") ) @@ -50,6 +51,8 @@ var ( type detectorUtils interface { getContainerName() (string, error) getContainerID() (string, error) + getContainerMetadataV4(ctx context.Context) (*ecsmetadata.ContainerMetadataV4, error) + getTaskMetadataV4(ctx context.Context) (*ecsmetadata.TaskMetadataV4, error) } // struct implements detectorUtils interface. @@ -97,12 +100,12 @@ func (detector *resourceDetector) Detect(ctx context.Context) (*resource.Resourc } if len(metadataURIV4) > 0 { - containerMetadata, err := ecsmetadata.GetContainerV4(ctx, &http.Client{}) + containerMetadata, err := detector.utils.getContainerMetadataV4(ctx) if err != nil { return empty, err } - taskMetadata, err := ecsmetadata.GetTaskV4(ctx, &http.Client{}) + taskMetadata, err := detector.utils.getTaskMetadataV4(ctx) if err != nil { return empty, err } @@ -125,6 +128,26 @@ func (detector *resourceDetector) Detect(ctx context.Context) (*resource.Resourc } } + arnParts := strings.Split(taskMetadata.TaskARN, ":") + // A valid ARN should have at least 6 parts. + if len(arnParts) < 6 { + return empty, errCannotParseTaskArn + } + + attributes = append( + attributes, + semconv.CloudRegion(arnParts[3]), + semconv.CloudAccountID(arnParts[4]), + ) + + availabilityZone := taskMetadata.AvailabilityZone + if len(availabilityZone) > 0 { + attributes = append( + attributes, + semconv.CloudAvailabilityZone(availabilityZone), + ) + } + logAttributes, err := detector.getLogsAttributes(containerMetadata) if err != nil { return empty, err @@ -209,6 +232,16 @@ func (detector *resourceDetector) getLogsAttributes(metadata *ecsmetadata.Contai }, nil } +// returns metadata v4 for the container. +func (ecsUtils ecsDetectorUtils) getContainerMetadataV4(ctx context.Context) (*ecsmetadata.ContainerMetadataV4, error) { + return ecsmetadata.GetContainerV4(ctx, &http.Client{}) +} + +// returns metadata v4 for the task. +func (ecsUtils ecsDetectorUtils) getTaskMetadataV4(ctx context.Context) (*ecsmetadata.TaskMetadataV4, error) { + return ecsmetadata.GetTaskV4(ctx, &http.Client{}) +} + // returns docker container ID from default c group path. func (ecsUtils ecsDetectorUtils) getContainerID() (string, error) { if runtime.GOOS != "linux" { diff --git a/detectors/aws/ecs/ecs_test.go b/detectors/aws/ecs/ecs_test.go index 0f0a29a1a34..90f51e84cb2 100644 --- a/detectors/aws/ecs/ecs_test.go +++ b/detectors/aws/ecs/ecs_test.go @@ -16,6 +16,7 @@ package ecs import ( "context" + "fmt" "testing" "go.opentelemetry.io/otel/attribute" @@ -42,6 +43,16 @@ func (detectorUtils *MockDetectorUtils) getContainerName() (string, error) { return args.String(0), args.Error(1) } +func (detectorUtils *MockDetectorUtils) getContainerMetadataV4(_ context.Context) (*metadata.ContainerMetadataV4, error) { + args := detectorUtils.Called() + return args.Get(0).(*metadata.ContainerMetadataV4), args.Error(1) +} + +func (detectorUtils *MockDetectorUtils) getTaskMetadataV4(_ context.Context) (*metadata.TaskMetadataV4, error) { + args := detectorUtils.Called() + return args.Get(0).(*metadata.TaskMetadataV4), args.Error(1) +} + // successfully returns resource when process is running on Amazon ECS environment // with no Metadata v4. func TestDetectV3(t *testing.T) { @@ -51,6 +62,8 @@ func TestDetectV3(t *testing.T) { detectorUtils.On("getContainerName").Return("container-Name", nil) detectorUtils.On("getContainerID").Return("0123456789A", nil) + detectorUtils.On("getContainerMetadataV4").Return(nil, fmt.Errorf("not supported")) + detectorUtils.On("getTaskMetadataV4").Return(nil, fmt.Errorf("not supported")) attributes := []attribute.KeyValue{ semconv.CloudProviderAWS, @@ -65,6 +78,87 @@ func TestDetectV3(t *testing.T) { assert.Equal(t, expectedResource, res, "Resource returned is incorrect") } +// successfully returns resource when process is running on Amazon ECS environment +// with Metadata v4. +func TestDetectV4(t *testing.T) { + t.Setenv(metadataV4EnvVar, "4") + + detectorUtils := new(MockDetectorUtils) + + detectorUtils.On("getContainerName").Return("container-Name", nil) + detectorUtils.On("getContainerID").Return("0123456789A", nil) + detectorUtils.On("getContainerMetadataV4").Return(&metadata.ContainerMetadataV4{ + ContainerARN: "arn:aws:ecs:us-west-2:111122223333:container/05966557-f16c-49cb-9352-24b3a0dcd0e1", + }, nil) + detectorUtils.On("getTaskMetadataV4").Return(&metadata.TaskMetadataV4{ + 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: metadata.Limits{ + CPU: 0.25, + Memory: 512, + }, + AvailabilityZone: "us-west-2a", + LaunchType: "FARGATE", + }, nil) + + attributes := []attribute.KeyValue{ + semconv.CloudProviderAWS, + semconv.CloudPlatformAWSECS, + semconv.CloudAccountID("111122223333"), + semconv.CloudRegion("us-west-2"), + semconv.CloudAvailabilityZone("us-west-2a"), + semconv.ContainerName("container-Name"), + semconv.ContainerID("0123456789A"), + semconv.AWSECSClusterARN("arn:aws:ecs:us-west-2:111122223333:cluster/default"), + semconv.AWSECSTaskARN("arn:aws:ecs:us-west-2:111122223333:task/default/e9028f8d5d8e4f258373e7b93ce9a3c3"), + semconv.AWSECSLaunchtypeKey.String("fargate"), + semconv.AWSECSTaskFamily("curltest"), + semconv.AWSECSTaskRevision("3"), + semconv.AWSECSContainerARN("arn:aws:ecs:us-west-2:111122223333:container/05966557-f16c-49cb-9352-24b3a0dcd0e1"), + } + 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 receives a bad task ARN from the Metadata v4 endpoint. +func TestDetectBadARNsv4(t *testing.T) { + t.Setenv(metadataV4EnvVar, "4") + + detectorUtils := new(MockDetectorUtils) + + detectorUtils.On("getContainerName").Return("container-Name", nil) + detectorUtils.On("getContainerID").Return("0123456789A", nil) + detectorUtils.On("getContainerMetadataV4").Return(&metadata.ContainerMetadataV4{ + ContainerARN: "container/05966557-f16c-49cb-9352-24b3a0dcd0e1", + }, nil) + detectorUtils.On("getTaskMetadataV4").Return(&metadata.TaskMetadataV4{ + Cluster: "default", + TaskARN: "default/e9028f8d5d8e4f258373e7b93ce9a3c3", + Family: "curltest", + Revision: "3", + DesiredStatus: "RUNNING", + KnownStatus: "RUNNING", + Limits: metadata.Limits{ + CPU: 0.25, + Memory: 512, + }, + AvailabilityZone: "us-west-2a", + LaunchType: "FARGATE", + }, nil) + + detector := &resourceDetector{utils: detectorUtils} + _, err := detector.Detect(context.Background()) + + assert.Equal(t, errCannotParseTaskArn, err) +} + // returns empty resource when detector cannot read container ID. func TestDetectCannotReadContainerID(t *testing.T) { t.Setenv(metadataV3EnvVar, "3") @@ -72,6 +166,8 @@ func TestDetectCannotReadContainerID(t *testing.T) { detectorUtils.On("getContainerName").Return("container-Name", nil) detectorUtils.On("getContainerID").Return("", nil) + detectorUtils.On("getContainerMetadataV4").Return(nil, fmt.Errorf("not supported")) + detectorUtils.On("getTaskMetadataV4").Return(nil, fmt.Errorf("not supported")) attributes := []attribute.KeyValue{ semconv.CloudProviderAWS, @@ -90,11 +186,12 @@ func TestDetectCannotReadContainerID(t *testing.T) { // returns empty resource when detector cannot read container Name. func TestDetectCannotReadContainerName(t *testing.T) { t.Setenv(metadataV3EnvVar, "3") - t.Setenv(metadataV4EnvVar, "4") detectorUtils := new(MockDetectorUtils) detectorUtils.On("getContainerName").Return("", errCannotReadContainerName) detectorUtils.On("getContainerID").Return("0123456789A", nil) + detectorUtils.On("getContainerMetadataV4").Return(nil, fmt.Errorf("not supported")) + detectorUtils.On("getTaskMetadataV4").Return(nil, fmt.Errorf("not supported")) detector := &resourceDetector{utils: detectorUtils} res, err := detector.Detect(context.Background()) diff --git a/detectors/aws/ecs/test/ecs_test.go b/detectors/aws/ecs/test/ecs_test.go index 44ceb38b169..fcebbe415bd 100644 --- a/detectors/aws/ecs/test/ecs_test.go +++ b/detectors/aws/ecs/test/ecs_test.go @@ -66,6 +66,9 @@ func TestDetectV4LaunchTypeEc2(t *testing.T) { attributes := []attribute.KeyValue{ semconv.CloudProviderAWS, semconv.CloudPlatformAWSECS, + semconv.CloudAccountID("111122223333"), + semconv.CloudRegion("us-west-2"), + semconv.CloudAvailabilityZone("us-west-2d"), semconv.ContainerName(hostname), // We are not running the test in an actual container, // the container id is tested with mocks of the cgroup @@ -122,6 +125,9 @@ func TestDetectV4LaunchTypeEc2BadContainerArn(t *testing.T) { attributes := []attribute.KeyValue{ semconv.CloudProviderAWS, semconv.CloudPlatformAWSECS, + semconv.CloudAccountID("111122223333"), + semconv.CloudRegion("us-west-2"), + semconv.CloudAvailabilityZone("us-west-2d"), semconv.ContainerName(hostname), // We are not running the test in an actual container, // the container id is tested with mocks of the cgroup @@ -179,6 +185,9 @@ func TestDetectV4LaunchTypeEc2BadTaskArn(t *testing.T) { semconv.CloudProviderAWS, semconv.CloudPlatformAWSECS, semconv.ContainerName(hostname), + semconv.CloudAccountID("111122223333"), + semconv.CloudRegion("us-west-2"), + semconv.CloudAvailabilityZone("us-west-2d"), // 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 @@ -235,6 +244,9 @@ func TestDetectV4LaunchTypeFargate(t *testing.T) { semconv.CloudProviderAWS, semconv.CloudPlatformAWSECS, semconv.ContainerName(hostname), + semconv.CloudAccountID("111122223333"), + semconv.CloudRegion("us-west-2"), + semconv.CloudAvailabilityZone("us-west-2a"), // 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