Skip to content

Commit

Permalink
Implemented aws.ecs.* and aws.logs.* resource attributes
Browse files Browse the repository at this point in the history
in go.opentelemetry.io/detectors/aws/ecs for ECS Metadata v4
  • Loading branch information
Michele Mancioppi committed Sep 18, 2022
1 parent c4876c3 commit 0654cc1
Show file tree
Hide file tree
Showing 9 changed files with 488 additions and 9 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
108 changes: 103 additions & 5 deletions detectors/aws/ecs/ecs.go
Expand Up @@ -17,9 +17,14 @@ package ecs // import "go.opentelemetry.io/contrib/detectors/aws/ecs"
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"regexp"
"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"
Expand All @@ -35,10 +40,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("the ECS resource detector failed to read cGroupFile")
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.
Expand All @@ -61,9 +68,22 @@ var _ detectorUtils = (*ecsDetectorUtils)(nil)
// compile time assertion that resource detector implements the resource.Detector interface.
var _ resource.Detector = (*resourceDetector)(nil)

func getLaunchType(launchType string) (attribute.KeyValue, error) {
switch strings.ToLower(launchType) {
case "ec2":
return semconv.AWSECSLaunchtypeEC2, nil
case "fargate":
return semconv.AWSECSLaunchtypeFargate, nil
default:
return semconv.AWSECSLaunchtypeKey.String("launchType"), fmt.Errorf("launch type unrecognized: %s", launchType)
}
}

// 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.
Expand All @@ -89,9 +109,87 @@ 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...)
}

launchType, err := getLaunchType(taskMetadata.LaunchType)
if err != nil {
return empty, err
}

attributes = append(
attributes,
semconv.AWSECSClusterARNKey.String(clusterArn),
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.StringSlice([]string{logsOptions.AwsLogsGroup}),
semconv.AWSLogGroupARNsKey.StringSlice([]string{fmt.Sprintf("arn:aws:logs:%s:%s:log-group:%s", logsRegion, awsAccount, logsOptions.AwsLogsGroup)}),
semconv.AWSLogStreamNamesKey.StringSlice([]string{logsOptions.AwsLogsStream}),
semconv.AWSLogStreamARNsKey.StringSlice([]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) {
fileData, err := os.ReadFile(defaultCgroupPath)
Expand Down
117 changes: 113 additions & 4 deletions detectors/aws/ecs/ecs_test.go
Expand Up @@ -16,7 +16,10 @@ package ecs

import (
"context"
http "net/http"
"net/http/httptest"
"os"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -42,11 +45,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)

Expand All @@ -63,7 +66,113 @@ 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")
}

// 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("testdata/metadatav4-response-task-ec2.json")
assert.NoError(t, err, "failure to read metadata task")

_, err = res.Write(content)
assert.NoError(t, err, "failure to serve response")
} else {
content, err := os.ReadFile("testdata/metadatav4-response-container-ec2.json")
assert.NoError(t, err, "failure to read metadata container")

_, err = res.Write(content)
assert.NoError(t, err, "failure to serve response")
}
}))
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.AWSECSLaunchtypeEC2,
semconv.AWSECSTaskARNKey.String("arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c"),
semconv.AWSECSTaskFamilyKey.String("curltest"),
semconv.AWSECSTaskRevisionKey.String("26"),
semconv.AWSLogGroupNamesKey.StringSlice([]string{"/ecs/metadata"}),
semconv.AWSLogGroupARNsKey.StringSlice([]string{"arn:aws:logs:us-west-2:111122223333:log-group:/ecs/metadata"}),
semconv.AWSLogStreamNamesKey.StringSlice([]string{"ecs/curl/8f03e41243824aea923aca126495f665"}),
semconv.AWSLogStreamARNsKey.StringSlice([]string{"arn:aws:logs:us-west-2:111122223333:log-group:/ecs/metadata:log-stream:ecs/curl/8f03e41243824aea923aca126495f665"}),
}
expectedResource := resource.NewWithAttributes(semconv.SchemaURL, attributes...)
detector := &resourceDetector{utils: detectorUtils}
res, err := detector.Detect(context.Background())

assert.Equal(t, err, nil, "Detector should not file")
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("testdata/metadatav4-response-task-fargate.json")
assert.NoError(t, err, "failure to read metadata task")

_, err = res.Write(content)
assert.NoError(t, err, "failure to serve response")
} else {
content, err := os.ReadFile("testdata/metadatav4-response-container-fargate.json")
assert.NoError(t, err, "failure to read metadata container")

_, err = res.Write(content)
assert.NoError(t, err, "failure to serve response")
}
}))
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/05966557-f16c-49cb-9352-24b3a0dcd0e1"),
semconv.AWSECSClusterARNKey.String("arn:aws:ecs:us-west-2:111122223333:cluster/default"),
semconv.AWSECSLaunchtypeFargate,
semconv.AWSECSTaskARNKey.String("arn:aws:ecs:us-west-2:111122223333:task/default/e9028f8d5d8e4f258373e7b93ce9a3c3"),
semconv.AWSECSTaskFamilyKey.String("curltest"),
semconv.AWSECSTaskRevisionKey.String("3"),
semconv.AWSLogGroupNamesKey.StringSlice([]string{"/ecs/containerlogs"}),
semconv.AWSLogGroupARNsKey.StringSlice([]string{"arn:aws:logs:us-west-2:111122223333:log-group:/ecs/containerlogs"}),
semconv.AWSLogStreamNamesKey.StringSlice([]string{"ecs/curl/cd189a933e5849daa93386466019ab50"}),
semconv.AWSLogStreamARNsKey.StringSlice([]string{"arn:aws:logs:us-west-2:111122223333:log-group:/ecs/containerlogs:log-stream:ecs/curl/cd189a933e5849daa93386466019ab50"}),
}
expectedResource := resource.NewWithAttributes(semconv.SchemaURL, attributes...)
detector := &resourceDetector{utils: detectorUtils}
res, err := detector.Detect(context.Background())

assert.Equal(t, err, nil, "Detector should not file")
assert.Equal(t, expectedResource, res, "Resource returned is incorrect")
}

// returns empty resource when detector cannot read container ID.
Expand Down
1 change: 1 addition & 0 deletions detectors/aws/ecs/go.mod
Expand Up @@ -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-20220812150832-b6b31c6eeeaf
github.com/stretchr/testify v1.8.0
go.opentelemetry.io/otel v1.10.0
go.opentelemetry.io/otel/sdk v1.10.0
Expand Down
2 changes: 2 additions & 0 deletions 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=
Expand Down
44 changes: 44 additions & 0 deletions detectors/aws/ecs/testdata/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"
}
]
}

0 comments on commit 0654cc1

Please sign in to comment.