Skip to content

Commit

Permalink
Implement aws.ecs.* resource attributes (#2626)
Browse files Browse the repository at this point in the history
* Implemented aws.ecs.* resource attributes in go.opentelemetry.io/detectors/aws/ecs

* Update detectors/aws/ecs/ecs_test.go

Co-authored-by: Chester Cheung <cheung.zhy.csu@gmail.com>

* Update detectors/aws/ecs/ecs_test.go

Co-authored-by: Chester Cheung <cheung.zhy.csu@gmail.com>

* fix: lower-case the value of aws.ecs.launchtype

* Add aws.logs.* support, remove spurious /aws prefix from log group

* Add tests for V4 on Fargate launch type

* Rebase

* Fix integration tests, fix behavior on Windows

After a surreal session of debugging, it turns out that httptest.NewServer fails on Windows by failing to bind the server socket unless the GoLang package name contains "test". To deal with that, I split the tests between "integration tests" needing a HTTP server into "ecs/test", and the other in "ecs".

In order to work around the need of being resilient to the lack of /proc/self/cgroup (which happens in our tests but, above all, when running containers on Windows), the ECVS detector is now more lenient to not finding the container id.

Co-authored-by: Chester Cheung <cheung.zhy.csu@gmail.com>
  • Loading branch information
Michele Mancioppi and hanyuancheung committed Nov 23, 2022
1 parent 15ceaa2 commit 01b39e7
Show file tree
Hide file tree
Showing 11 changed files with 533 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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)
Expand Down
102 changes: 95 additions & 7 deletions detectors/aws/ecs/ecs.go
Expand Up @@ -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"
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -89,22 +97,102 @@ 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 {
if len(str) > containerIDLength {
return str[len(str)-containerIDLength:], nil
}
}
return "", errCannotReadContainerID
return "", nil
}

// returns host name reported by the kernel.
Expand Down
28 changes: 17 additions & 11 deletions detectors/aws/ecs/ecs_test.go
Expand Up @@ -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.
Expand All @@ -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)

Expand All @@ -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.
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.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
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
149 changes: 149 additions & 0 deletions 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")
}

0 comments on commit 01b39e7

Please sign in to comment.