Skip to content

Commit 01b39e7

Browse files
Michele Mancioppihanyuancheung
andauthored
Implement aws.ecs.* resource attributes (#2626)
* Implemented aws.ecs.* resource attributes in go.opentelemetry.io/detectors/aws/ecs * Update detectors/aws/ecs/ecs_test.go Co-authored-by: Chester Cheung <[email protected]> * Update detectors/aws/ecs/ecs_test.go Co-authored-by: Chester Cheung <[email protected]> * 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 <[email protected]>
1 parent 15ceaa2 commit 01b39e7

File tree

11 files changed

+533
-21
lines changed

11 files changed

+533
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1010

1111
### Added
1212

13+
- 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.
1314
- The `WithLogger` option to `go.opentelemetry.io/contrib/samplers/jaegerremote` to allow users to pass a `logr.Logger` and have operations logged. (#2566)
1415
- 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)
1516
- Add example use of the metrics signal to `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/example`. (#2610)

detectors/aws/ecs/ecs.go

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,15 @@ package ecs // import "go.opentelemetry.io/contrib/detectors/aws/ecs"
1717
import (
1818
"context"
1919
"errors"
20+
"fmt"
21+
"net/http"
2022
"os"
23+
"regexp"
24+
"runtime"
2125
"strings"
2226

27+
ecsmetadata "github.com/brunoscheufler/aws-ecs-metadata-go"
28+
2329
"go.opentelemetry.io/otel/attribute"
2430
"go.opentelemetry.io/otel/sdk/resource"
2531
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
@@ -35,10 +41,10 @@ const (
3541
)
3642

3743
var (
38-
empty = resource.Empty()
39-
errCannotReadContainerID = errors.New("failed to read container ID from cGroupFile")
40-
errCannotReadContainerName = errors.New("failed to read hostname")
41-
errCannotReadCGroupFile = errors.New("ECS resource detector failed to read cGroupFile")
44+
empty = resource.Empty()
45+
errCannotReadContainerName = errors.New("failed to read hostname")
46+
errCannotRetrieveLogsGroupMetadataV4 = errors.New("the ECS Metadata v4 did not return a AwsLogGroup name")
47+
errCannotRetrieveLogsStreamMetadataV4 = errors.New("the ECS Metadata v4 did not return a AwsLogStream name")
4248
)
4349

4450
// Create interface for methods needing to be mocked.
@@ -63,7 +69,9 @@ var _ resource.Detector = (*resourceDetector)(nil)
6369

6470
// NewResourceDetector returns a resource detector that will detect AWS ECS resources.
6571
func NewResourceDetector() resource.Detector {
66-
return &resourceDetector{utils: ecsDetectorUtils{}}
72+
return &resourceDetector{
73+
utils: ecsDetectorUtils{},
74+
}
6775
}
6876

6977
// Detect finds associated resources when running on ECS environment.
@@ -89,22 +97,102 @@ func (detector *resourceDetector) Detect(ctx context.Context) (*resource.Resourc
8997
semconv.ContainerIDKey.String(containerID),
9098
}
9199

100+
if len(metadataURIV4) > 0 {
101+
containerMetadata, err := ecsmetadata.GetContainerV4(ctx, &http.Client{})
102+
if err != nil {
103+
return empty, err
104+
}
105+
attributes = append(
106+
attributes,
107+
semconv.AWSECSContainerARNKey.String(containerMetadata.ContainerARN),
108+
)
109+
110+
taskMetadata, err := ecsmetadata.GetTaskV4(ctx, &http.Client{})
111+
if err != nil {
112+
return empty, err
113+
}
114+
115+
clusterArn := taskMetadata.Cluster
116+
if !strings.HasPrefix(clusterArn, "arn:") {
117+
baseArn := containerMetadata.ContainerARN[:strings.LastIndex(containerMetadata.ContainerARN, ":")]
118+
clusterArn = fmt.Sprintf("%s:cluster/%s", baseArn, clusterArn)
119+
}
120+
121+
logAttributes, err := detector.getLogsAttributes(containerMetadata)
122+
if err != nil {
123+
return empty, err
124+
}
125+
126+
if len(logAttributes) > 0 {
127+
attributes = append(attributes, logAttributes...)
128+
}
129+
130+
attributes = append(
131+
attributes,
132+
semconv.AWSECSClusterARNKey.String(clusterArn),
133+
semconv.AWSECSLaunchtypeKey.String(strings.ToLower(taskMetadata.LaunchType)),
134+
semconv.AWSECSTaskARNKey.String(taskMetadata.TaskARN),
135+
semconv.AWSECSTaskFamilyKey.String(taskMetadata.Family),
136+
semconv.AWSECSTaskRevisionKey.String(taskMetadata.Revision),
137+
)
138+
}
139+
92140
return resource.NewWithAttributes(semconv.SchemaURL, attributes...), nil
93141
}
94142

143+
func (detector *resourceDetector) getLogsAttributes(metadata *ecsmetadata.ContainerMetadataV4) ([]attribute.KeyValue, error) {
144+
if metadata.LogDriver != "awslogs" {
145+
return []attribute.KeyValue{}, nil
146+
}
147+
148+
logsOptions := metadata.LogOptions
149+
150+
if len(logsOptions.AwsLogsGroup) < 1 {
151+
return nil, errCannotRetrieveLogsGroupMetadataV4
152+
}
153+
154+
if len(logsOptions.AwsLogsStream) < 1 {
155+
return nil, errCannotRetrieveLogsStreamMetadataV4
156+
}
157+
158+
containerArn := metadata.ContainerARN
159+
logsRegion := logsOptions.AwsRegion
160+
if len(logsRegion) < 1 {
161+
r := regexp.MustCompile(`arn:aws:ecs:([^:]+):.*`)
162+
logsRegion = r.FindStringSubmatch(containerArn)[1]
163+
}
164+
165+
r := regexp.MustCompile(`arn:aws:ecs:[^:]+:([^:]+):.*`)
166+
awsAccount := r.FindStringSubmatch(containerArn)[1]
167+
168+
return []attribute.KeyValue{
169+
semconv.AWSLogGroupNamesKey.String(logsOptions.AwsLogsGroup),
170+
semconv.AWSLogGroupARNsKey.String(fmt.Sprintf("arn:aws:logs:%s:%s:log-group:%s:*", logsRegion, awsAccount, logsOptions.AwsLogsGroup)),
171+
semconv.AWSLogStreamNamesKey.String(logsOptions.AwsLogsStream),
172+
semconv.AWSLogStreamARNsKey.String(fmt.Sprintf("arn:aws:logs:%s:%s:log-group:%s:log-stream:%s", logsRegion, awsAccount, logsOptions.AwsLogsGroup, logsOptions.AwsLogsStream)),
173+
}, nil
174+
}
175+
95176
// returns docker container ID from default c group path.
96177
func (ecsUtils ecsDetectorUtils) getContainerID() (string, error) {
178+
if runtime.GOOS != "linux" {
179+
// Cgroups are used only under Linux.
180+
return "", nil
181+
}
182+
97183
fileData, err := os.ReadFile(defaultCgroupPath)
98184
if err != nil {
99-
return "", errCannotReadCGroupFile
185+
// Cgroups file not found.
186+
// For example, windows; or when running integration tests outside of a container.
187+
return "", nil
100188
}
101189
splitData := strings.Split(strings.TrimSpace(string(fileData)), "\n")
102190
for _, str := range splitData {
103191
if len(str) > containerIDLength {
104192
return str[len(str)-containerIDLength:], nil
105193
}
106194
}
107-
return "", errCannotReadContainerID
195+
return "", nil
108196
}
109197

110198
// returns host name reported by the kernel.

detectors/aws/ecs/ecs_test.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ import (
1919
"os"
2020
"testing"
2121

22-
"github.com/stretchr/testify/assert"
23-
"github.com/stretchr/testify/mock"
24-
2522
"go.opentelemetry.io/otel/attribute"
2623
"go.opentelemetry.io/otel/sdk/resource"
2724
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
25+
26+
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/mock"
2828
)
2929

3030
// Create interface for functions that need to be mocked.
@@ -42,11 +42,11 @@ func (detectorUtils *MockDetectorUtils) getContainerName() (string, error) {
4242
return args.String(0), args.Error(1)
4343
}
4444

45-
// successfully return resource when process is running on Amazon ECS environment.
46-
func TestDetect(t *testing.T) {
45+
// successfully returns resource when process is running on Amazon ECS environment
46+
// with no Metadata v4.
47+
func TestDetectV3(t *testing.T) {
4748
os.Clearenv()
4849
_ = os.Setenv(metadataV3EnvVar, "3")
49-
_ = os.Setenv(metadataV4EnvVar, "4")
5050

5151
detectorUtils := new(MockDetectorUtils)
5252

@@ -63,24 +63,30 @@ func TestDetect(t *testing.T) {
6363
detector := &resourceDetector{utils: detectorUtils}
6464
res, _ := detector.Detect(context.Background())
6565

66-
assert.Equal(t, res, expectedResource, "Resource returned is incorrect")
66+
assert.Equal(t, expectedResource, res, "Resource returned is incorrect")
6767
}
6868

6969
// returns empty resource when detector cannot read container ID.
7070
func TestDetectCannotReadContainerID(t *testing.T) {
7171
os.Clearenv()
7272
_ = os.Setenv(metadataV3EnvVar, "3")
73-
_ = os.Setenv(metadataV4EnvVar, "4")
7473
detectorUtils := new(MockDetectorUtils)
7574

7675
detectorUtils.On("getContainerName").Return("container-Name", nil)
77-
detectorUtils.On("getContainerID").Return("", errCannotReadContainerID)
76+
detectorUtils.On("getContainerID").Return("", nil)
7877

78+
attributes := []attribute.KeyValue{
79+
semconv.CloudProviderAWS,
80+
semconv.CloudPlatformAWSECS,
81+
semconv.ContainerNameKey.String("container-Name"),
82+
semconv.ContainerIDKey.String(""),
83+
}
84+
expectedResource := resource.NewWithAttributes(semconv.SchemaURL, attributes...)
7985
detector := &resourceDetector{utils: detectorUtils}
8086
res, err := detector.Detect(context.Background())
8187

82-
assert.Equal(t, errCannotReadContainerID, err)
83-
assert.Equal(t, 0, len(res.Attributes()))
88+
assert.Equal(t, nil, err)
89+
assert.Equal(t, expectedResource, res, "Resource returned is incorrect")
8490
}
8591

8692
// returns empty resource when detector cannot read container Name.

detectors/aws/ecs/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module go.opentelemetry.io/contrib/detectors/aws/ecs
33
go 1.18
44

55
require (
6+
github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220812150832-b6b31c6eeeaf
67
github.com/stretchr/testify v1.8.1
78
go.opentelemetry.io/otel v1.11.1
89
go.opentelemetry.io/otel/sdk v1.11.1

detectors/aws/ecs/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220812150832-b6b31c6eeeaf h1:WCnJxXZXx9c8gwz598wvdqmu+YTzB9wx2X1OovK3Le8=
2+
github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220812150832-b6b31c6eeeaf/go.mod h1:CeKhh8xSs3WZAc50xABMxu+FlfAAd5PNumo7NfOv7EE=
13
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
35
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

detectors/aws/ecs/test/ecs_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package ecs
16+
17+
import (
18+
"context"
19+
"net/http"
20+
"net/http/httptest"
21+
"os"
22+
"strings"
23+
"testing"
24+
25+
ecs "go.opentelemetry.io/contrib/detectors/aws/ecs"
26+
"go.opentelemetry.io/otel/attribute"
27+
"go.opentelemetry.io/otel/sdk/resource"
28+
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
29+
30+
"github.com/stretchr/testify/assert"
31+
)
32+
33+
const (
34+
metadataV4EnvVar = "ECS_CONTAINER_METADATA_URI_V4"
35+
)
36+
37+
// successfully returns resource when process is running on Amazon ECS environment
38+
// with Metadata v4 with the EC2 Launch type.
39+
func TestDetectV4LaunchTypeEc2(t *testing.T) {
40+
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
41+
if strings.HasSuffix(req.URL.String(), "/task") {
42+
content, err := os.ReadFile("metadatav4-response-task-ec2.json")
43+
if err == nil {
44+
_, err = res.Write(content)
45+
if err != nil {
46+
t.Fatal(err)
47+
}
48+
}
49+
} else {
50+
content, err := os.ReadFile("metadatav4-response-container-ec2.json")
51+
if err == nil {
52+
_, err = res.Write(content)
53+
if err != nil {
54+
t.Fatal(err)
55+
}
56+
}
57+
}
58+
}))
59+
defer testServer.Close()
60+
61+
os.Clearenv()
62+
_ = os.Setenv(metadataV4EnvVar, testServer.URL)
63+
64+
hostname, err := os.Hostname()
65+
assert.NoError(t, err, "Error")
66+
67+
attributes := []attribute.KeyValue{
68+
semconv.CloudProviderAWS,
69+
semconv.CloudPlatformAWSECS,
70+
semconv.ContainerNameKey.String(hostname),
71+
// We are not running the test in an actual container,
72+
// the container id is tested with mocks of the cgroup
73+
// file in the unit tests
74+
semconv.ContainerIDKey.String(""),
75+
semconv.AWSECSContainerARNKey.String("arn:aws:ecs:us-west-2:111122223333:container/0206b271-b33f-47ab-86c6-a0ba208a70a9"),
76+
semconv.AWSECSClusterARNKey.String("arn:aws:ecs:us-west-2:111122223333:cluster/default"),
77+
semconv.AWSECSLaunchtypeKey.String("ec2"),
78+
semconv.AWSECSTaskARNKey.String("arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c"),
79+
semconv.AWSECSTaskFamilyKey.String("curltest"),
80+
semconv.AWSECSTaskRevisionKey.String("26"),
81+
semconv.AWSLogGroupNamesKey.String("/ecs/metadata"),
82+
semconv.AWSLogGroupARNsKey.String("arn:aws:logs:us-west-2:111122223333:log-group:/ecs/metadata:*"),
83+
semconv.AWSLogStreamNamesKey.String("ecs/curl/8f03e41243824aea923aca126495f665"),
84+
semconv.AWSLogStreamARNsKey.String("arn:aws:logs:us-west-2:111122223333:log-group:/ecs/metadata:log-stream:ecs/curl/8f03e41243824aea923aca126495f665"),
85+
}
86+
expectedResource := resource.NewWithAttributes(semconv.SchemaURL, attributes...)
87+
detector := ecs.NewResourceDetector()
88+
res, err := detector.Detect(context.Background())
89+
90+
assert.Equal(t, nil, err, "Detector should not fail")
91+
assert.Equal(t, expectedResource, res, "Resource returned is incorrect")
92+
}
93+
94+
// successfully returns resource when process is running on Amazon ECS environment
95+
// with Metadata v4 with the Fargate Launch type.
96+
func TestDetectV4LaunchTypeFargate(t *testing.T) {
97+
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
98+
if strings.HasSuffix(req.URL.String(), "/task") {
99+
content, err := os.ReadFile("metadatav4-response-task-fargate.json")
100+
if err == nil {
101+
_, err = res.Write(content)
102+
if err != nil {
103+
panic(err)
104+
}
105+
}
106+
} else {
107+
content, err := os.ReadFile("metadatav4-response-container-fargate.json")
108+
if err == nil {
109+
_, err = res.Write(content)
110+
if err != nil {
111+
panic(err)
112+
}
113+
}
114+
}
115+
}))
116+
defer testServer.Close()
117+
118+
os.Clearenv()
119+
_ = os.Setenv(metadataV4EnvVar, testServer.URL)
120+
121+
hostname, err := os.Hostname()
122+
assert.NoError(t, err, "Error")
123+
124+
attributes := []attribute.KeyValue{
125+
semconv.CloudProviderAWS,
126+
semconv.CloudPlatformAWSECS,
127+
semconv.ContainerNameKey.String(hostname),
128+
// We are not running the test in an actual container,
129+
// the container id is tested with mocks of the cgroup
130+
// file in the unit tests
131+
semconv.ContainerIDKey.String(""),
132+
semconv.AWSECSContainerARNKey.String("arn:aws:ecs:us-west-2:111122223333:container/05966557-f16c-49cb-9352-24b3a0dcd0e1"),
133+
semconv.AWSECSClusterARNKey.String("arn:aws:ecs:us-west-2:111122223333:cluster/default"),
134+
semconv.AWSECSLaunchtypeKey.String("fargate"),
135+
semconv.AWSECSTaskARNKey.String("arn:aws:ecs:us-west-2:111122223333:task/default/e9028f8d5d8e4f258373e7b93ce9a3c3"),
136+
semconv.AWSECSTaskFamilyKey.String("curltest"),
137+
semconv.AWSECSTaskRevisionKey.String("3"),
138+
semconv.AWSLogGroupNamesKey.String("/ecs/containerlogs"),
139+
semconv.AWSLogGroupARNsKey.String("arn:aws:logs:us-west-2:111122223333:log-group:/ecs/containerlogs:*"),
140+
semconv.AWSLogStreamNamesKey.String("ecs/curl/cd189a933e5849daa93386466019ab50"),
141+
semconv.AWSLogStreamARNsKey.String("arn:aws:logs:us-west-2:111122223333:log-group:/ecs/containerlogs:log-stream:ecs/curl/cd189a933e5849daa93386466019ab50"),
142+
}
143+
expectedResource := resource.NewWithAttributes(semconv.SchemaURL, attributes...)
144+
detector := ecs.NewResourceDetector()
145+
res, err := detector.Detect(context.Background())
146+
147+
assert.Equal(t, nil, err, "Detector should not fail")
148+
assert.Equal(t, expectedResource, res, "Resource returned is incorrect")
149+
}

0 commit comments

Comments
 (0)