Skip to content

Commit 13ae395

Browse files
dackroydMrAlias
andauthored
[aws detector] Additional Attributes (#410)
Adding extra host/cloud attributes from the semantic conventions that can be provided by AWS instance metadata: * `host.image.id` * `host.name` * `host.type` * `cloud.zone` Metadata (currently only `hostname`) failures will still allow the resource to be returned, along with a wrapped ErrPartialResource (per the Detector interface design) Co-authored-by: Tyler Yahn <[email protected]>
1 parent a3f208a commit 13ae395

File tree

2 files changed

+144
-21
lines changed

2 files changed

+144
-21
lines changed

detectors/aws/aws.go

+43-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ package aws
1616

1717
import (
1818
"context"
19+
"fmt"
20+
"net/http"
1921

22+
"github.com/aws/aws-sdk-go/aws/awserr"
2023
"github.com/aws/aws-sdk-go/aws/ec2metadata"
2124
"github.com/aws/aws-sdk-go/aws/session"
2225

@@ -33,6 +36,7 @@ type AWS struct {
3336
type client interface {
3437
Available() bool
3538
GetInstanceIdentityDocument() (ec2metadata.EC2InstanceIdentityDocument, error)
39+
GetMetadata(p string) (string, error)
3640
}
3741

3842
// compile time assertion that AWS implements the resource.Detector interface.
@@ -57,11 +61,23 @@ func (aws *AWS) Detect(ctx context.Context) (*resource.Resource, error) {
5761
labels := []label.KeyValue{
5862
semconv.CloudProviderAWS,
5963
semconv.CloudRegionKey.String(doc.Region),
64+
semconv.CloudZoneKey.String(doc.AvailabilityZone),
6065
semconv.CloudAccountIDKey.String(doc.AccountID),
6166
semconv.HostIDKey.String(doc.InstanceID),
67+
semconv.HostImageIDKey.String(doc.ImageID),
68+
semconv.HostTypeKey.String(doc.InstanceType),
6269
}
6370

64-
return resource.New(labels...), nil
71+
m := &metadata{client: client}
72+
m.add(semconv.HostNameKey, "hostname")
73+
74+
labels = append(labels, m.labels...)
75+
76+
if len(m.errs) > 0 {
77+
err = fmt.Errorf("%w: %s", resource.ErrPartialResource, m.errs)
78+
}
79+
80+
return resource.New(labels...), err
6581
}
6682

6783
func (aws *AWS) client() (client, error) {
@@ -76,3 +92,29 @@ func (aws *AWS) client() (client, error) {
7692

7793
return ec2metadata.New(s), nil
7894
}
95+
96+
type metadata struct {
97+
client client
98+
errs []error
99+
labels []label.KeyValue
100+
}
101+
102+
func (m *metadata) add(k label.Key, n string) {
103+
v, err := m.client.GetMetadata(n)
104+
if err == nil {
105+
m.labels = append(m.labels, k.String(v))
106+
return
107+
}
108+
109+
rf, ok := err.(awserr.RequestFailure)
110+
if !ok {
111+
m.errs = append(m.errs, fmt.Errorf("%q: %w", n, err))
112+
return
113+
}
114+
115+
if rf.StatusCode() == http.StatusNotFound {
116+
return
117+
}
118+
119+
m.errs = append(m.errs, fmt.Errorf("%q: %d %s", n, rf.StatusCode(), rf.Code()))
120+
}

detectors/aws/aws_test.go

+101-20
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@ package aws
1717
import (
1818
"context"
1919
"errors"
20+
"net/http"
2021
"testing"
2122
"time"
2223

24+
"github.com/aws/aws-sdk-go/aws/awserr"
2325
"github.com/aws/aws-sdk-go/aws/ec2metadata"
2426
"github.com/stretchr/testify/assert"
2527
"github.com/stretchr/testify/require"
2628

29+
"go.opentelemetry.io/otel/label"
2730
"go.opentelemetry.io/otel/sdk/resource"
2831
"go.opentelemetry.io/otel/semconv"
2932
)
@@ -35,9 +38,39 @@ func TestAWS_Detect(t *testing.T) {
3538

3639
type want struct {
3740
Error string
41+
Partial bool
3842
Resource *resource.Resource
3943
}
4044

45+
usWestInst := func() (ec2metadata.EC2InstanceIdentityDocument, error) {
46+
// Example from https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
47+
doc := ec2metadata.EC2InstanceIdentityDocument{
48+
MarketplaceProductCodes: []string{"1abc2defghijklm3nopqrs4tu"},
49+
AvailabilityZone: "us-west-2b",
50+
PrivateIP: "10.158.112.84",
51+
Version: "2017-09-30",
52+
Region: "us-west-2",
53+
InstanceID: "i-1234567890abcdef0",
54+
InstanceType: "t2.micro",
55+
AccountID: "123456789012",
56+
PendingTime: time.Date(2016, time.November, 19, 16, 32, 11, 0, time.UTC),
57+
ImageID: "ami-5fb8c835",
58+
Architecture: "x86_64",
59+
}
60+
61+
return doc, nil
62+
}
63+
64+
usWestIDLabels := []label.KeyValue{
65+
semconv.CloudProviderAWS,
66+
semconv.CloudRegionKey.String("us-west-2"),
67+
semconv.CloudZoneKey.String("us-west-2b"),
68+
semconv.CloudAccountIDKey.String("123456789012"),
69+
semconv.HostIDKey.String("i-1234567890abcdef0"),
70+
semconv.HostImageIDKey.String("ami-5fb8c835"),
71+
semconv.HostTypeKey.String("t2.micro"),
72+
}
73+
4174
testTable := map[string]struct {
4275
Fields fields
4376
Want want
@@ -53,32 +86,63 @@ func TestAWS_Detect(t *testing.T) {
5386
},
5487
Want: want{Error: "id not available"},
5588
},
56-
"Instance ID Available": {
89+
"Hostname Not Found": {
5790
Fields: fields{
58-
Client: &clientMock{available: true, idDoc: func() (ec2metadata.EC2InstanceIdentityDocument, error) {
59-
// Example from https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
60-
doc := ec2metadata.EC2InstanceIdentityDocument{
61-
MarketplaceProductCodes: []string{"1abc2defghijklm3nopqrs4tu"},
62-
AvailabilityZone: "us-west-2b",
63-
PrivateIP: "10.158.112.84",
64-
Version: "2017-09-30",
65-
Region: "us-west-2",
66-
InstanceID: "i-1234567890abcdef0",
67-
InstanceType: "t2.micro",
68-
AccountID: "123456789012",
69-
PendingTime: time.Date(2016, time.November, 19, 16, 32, 11, 0, time.UTC),
70-
ImageID: "ami-5fb8c835",
71-
Architecture: "x86_64",
72-
}
73-
74-
return doc, nil
75-
}},
91+
Client: &clientMock{available: true, idDoc: usWestInst, metadata: map[string]meta{}},
92+
},
93+
Want: want{Resource: resource.New(usWestIDLabels...)},
94+
},
95+
"Hostname Response Error": {
96+
Fields: fields{
97+
Client: &clientMock{
98+
available: true,
99+
idDoc: usWestInst,
100+
metadata: map[string]meta{
101+
"hostname": {err: awserr.NewRequestFailure(awserr.New("EC2MetadataError", "failed to make EC2Metadata request", errors.New("response error")), http.StatusInternalServerError, "test-request")},
102+
},
103+
},
104+
},
105+
Want: want{
106+
Error: `partial resource: ["hostname": 500 EC2MetadataError]`,
107+
Partial: true,
108+
Resource: resource.New(usWestIDLabels...),
109+
},
110+
},
111+
"Hostname General Error": {
112+
Fields: fields{
113+
Client: &clientMock{
114+
available: true,
115+
idDoc: usWestInst,
116+
metadata: map[string]meta{
117+
"hostname": {err: errors.New("unknown error")},
118+
},
119+
},
120+
},
121+
Want: want{
122+
Error: `partial resource: ["hostname": unknown error]`,
123+
Partial: true,
124+
Resource: resource.New(usWestIDLabels...),
125+
},
126+
},
127+
"All Available": {
128+
Fields: fields{
129+
Client: &clientMock{
130+
available: true,
131+
idDoc: usWestInst,
132+
metadata: map[string]meta{
133+
"hostname": {value: "ip-12-34-56-78.us-west-2.compute.internal"},
134+
},
135+
},
76136
},
77137
Want: want{Resource: resource.New(
78138
semconv.CloudProviderAWS,
79139
semconv.CloudRegionKey.String("us-west-2"),
140+
semconv.CloudZoneKey.String("us-west-2b"),
80141
semconv.CloudAccountIDKey.String("123456789012"),
81142
semconv.HostIDKey.String("i-1234567890abcdef0"),
143+
semconv.HostImageIDKey.String("ami-5fb8c835"),
144+
semconv.HostNameKey.String("ip-12-34-56-78.us-west-2.compute.internal"),
145+
semconv.HostTypeKey.String("t2.micro"),
82146
)},
83147
},
84148
}
@@ -93,20 +157,28 @@ func TestAWS_Detect(t *testing.T) {
93157

94158
r, err := aws.Detect(context.Background())
95159

160+
assert.Equal(t, tt.Want.Resource, r, "Resource")
161+
96162
if tt.Want.Error != "" {
97163
require.EqualError(t, err, tt.Want.Error, "Error")
164+
assert.Equal(t, tt.Want.Partial, errors.Is(err, resource.ErrPartialResource), "Partial Resource")
98165
return
99166
}
100167

101168
require.NoError(t, err, "Error")
102-
assert.Equal(t, tt.Want.Resource, r, "Resource")
103169
})
104170
}
105171
}
106172

107173
type clientMock struct {
108174
available bool
109175
idDoc func() (ec2metadata.EC2InstanceIdentityDocument, error)
176+
metadata map[string]meta
177+
}
178+
179+
type meta struct {
180+
err error
181+
value string
110182
}
111183

112184
func (c *clientMock) Available() bool {
@@ -116,3 +188,12 @@ func (c *clientMock) Available() bool {
116188
func (c *clientMock) GetInstanceIdentityDocument() (ec2metadata.EC2InstanceIdentityDocument, error) {
117189
return c.idDoc()
118190
}
191+
192+
func (c *clientMock) GetMetadata(p string) (string, error) {
193+
v, ok := c.metadata[p]
194+
if !ok {
195+
return "", awserr.NewRequestFailure(awserr.New("EC2MetadataError", "failed to make EC2Metadata request", errors.New("response error")), http.StatusNotFound, "test-request")
196+
}
197+
198+
return v.value, v.err
199+
}

0 commit comments

Comments
 (0)