Skip to content

Commit

Permalink
implement cloud.{account.id,availability_zone,region} in AWS ECS dete…
Browse files Browse the repository at this point in the history
…ctor (#4860)

* implement cloud.{account.id,availability_zone,region} in AWS ECS detector

* fix lint

* add changelog entry

* fix itests

* make log resilient against bad task ARNs

* report errors if account or region lookup fails

* reduce code duplication

* Update ecs.go

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>

* improve errCannotParseTaskArn error message

* add test coverage for bad ARNs across the board

* split attributes in changelog

* fix formatting changelog

---------

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
  • Loading branch information
mmanciop and MrAlias committed Feb 6, 2024
1 parent 144c933 commit de64a47
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
37 changes: 35 additions & 2 deletions detectors/aws/ecs/ecs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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" {
Expand Down
99 changes: 98 additions & 1 deletion detectors/aws/ecs/ecs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package ecs

import (
"context"
"fmt"
"testing"

"go.opentelemetry.io/otel/attribute"
Expand All @@ -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) {
Expand All @@ -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,
Expand All @@ -65,13 +78,96 @@ 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")
detectorUtils := new(MockDetectorUtils)

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,
Expand All @@ -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())
Expand Down
12 changes: 12 additions & 0 deletions detectors/aws/ecs/test/ecs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit de64a47

Please sign in to comment.