Skip to content

Commit

Permalink
[aws detector] Additional Attributes (#410)
Browse files Browse the repository at this point in the history
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 <MrAlias@users.noreply.github.com>
  • Loading branch information
dackroyd and MrAlias authored Oct 30, 2020
1 parent a3f208a commit 13ae395
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 21 deletions.
44 changes: 43 additions & 1 deletion detectors/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ package aws

import (
"context"
"fmt"
"net/http"

"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"

Expand All @@ -33,6 +36,7 @@ type AWS struct {
type client interface {
Available() bool
GetInstanceIdentityDocument() (ec2metadata.EC2InstanceIdentityDocument, error)
GetMetadata(p string) (string, error)
}

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

return resource.New(labels...), nil
m := &metadata{client: client}
m.add(semconv.HostNameKey, "hostname")

labels = append(labels, m.labels...)

if len(m.errs) > 0 {
err = fmt.Errorf("%w: %s", resource.ErrPartialResource, m.errs)
}

return resource.New(labels...), err
}

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

return ec2metadata.New(s), nil
}

type metadata struct {
client client
errs []error
labels []label.KeyValue
}

func (m *metadata) add(k label.Key, n string) {
v, err := m.client.GetMetadata(n)
if err == nil {
m.labels = append(m.labels, k.String(v))
return
}

rf, ok := err.(awserr.RequestFailure)
if !ok {
m.errs = append(m.errs, fmt.Errorf("%q: %w", n, err))
return
}

if rf.StatusCode() == http.StatusNotFound {
return
}

m.errs = append(m.errs, fmt.Errorf("%q: %d %s", n, rf.StatusCode(), rf.Code()))
}
121 changes: 101 additions & 20 deletions detectors/aws/aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ package aws
import (
"context"
"errors"
"net/http"
"testing"
"time"

"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

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

type want struct {
Error string
Partial bool
Resource *resource.Resource
}

usWestInst := func() (ec2metadata.EC2InstanceIdentityDocument, error) {
// Example from https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
doc := ec2metadata.EC2InstanceIdentityDocument{
MarketplaceProductCodes: []string{"1abc2defghijklm3nopqrs4tu"},
AvailabilityZone: "us-west-2b",
PrivateIP: "10.158.112.84",
Version: "2017-09-30",
Region: "us-west-2",
InstanceID: "i-1234567890abcdef0",
InstanceType: "t2.micro",
AccountID: "123456789012",
PendingTime: time.Date(2016, time.November, 19, 16, 32, 11, 0, time.UTC),
ImageID: "ami-5fb8c835",
Architecture: "x86_64",
}

return doc, nil
}

usWestIDLabels := []label.KeyValue{
semconv.CloudProviderAWS,
semconv.CloudRegionKey.String("us-west-2"),
semconv.CloudZoneKey.String("us-west-2b"),
semconv.CloudAccountIDKey.String("123456789012"),
semconv.HostIDKey.String("i-1234567890abcdef0"),
semconv.HostImageIDKey.String("ami-5fb8c835"),
semconv.HostTypeKey.String("t2.micro"),
}

testTable := map[string]struct {
Fields fields
Want want
Expand All @@ -53,32 +86,63 @@ func TestAWS_Detect(t *testing.T) {
},
Want: want{Error: "id not available"},
},
"Instance ID Available": {
"Hostname Not Found": {
Fields: fields{
Client: &clientMock{available: true, idDoc: func() (ec2metadata.EC2InstanceIdentityDocument, error) {
// Example from https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
doc := ec2metadata.EC2InstanceIdentityDocument{
MarketplaceProductCodes: []string{"1abc2defghijklm3nopqrs4tu"},
AvailabilityZone: "us-west-2b",
PrivateIP: "10.158.112.84",
Version: "2017-09-30",
Region: "us-west-2",
InstanceID: "i-1234567890abcdef0",
InstanceType: "t2.micro",
AccountID: "123456789012",
PendingTime: time.Date(2016, time.November, 19, 16, 32, 11, 0, time.UTC),
ImageID: "ami-5fb8c835",
Architecture: "x86_64",
}

return doc, nil
}},
Client: &clientMock{available: true, idDoc: usWestInst, metadata: map[string]meta{}},
},
Want: want{Resource: resource.New(usWestIDLabels...)},
},
"Hostname Response Error": {
Fields: fields{
Client: &clientMock{
available: true,
idDoc: usWestInst,
metadata: map[string]meta{
"hostname": {err: awserr.NewRequestFailure(awserr.New("EC2MetadataError", "failed to make EC2Metadata request", errors.New("response error")), http.StatusInternalServerError, "test-request")},
},
},
},
Want: want{
Error: `partial resource: ["hostname": 500 EC2MetadataError]`,
Partial: true,
Resource: resource.New(usWestIDLabels...),
},
},
"Hostname General Error": {
Fields: fields{
Client: &clientMock{
available: true,
idDoc: usWestInst,
metadata: map[string]meta{
"hostname": {err: errors.New("unknown error")},
},
},
},
Want: want{
Error: `partial resource: ["hostname": unknown error]`,
Partial: true,
Resource: resource.New(usWestIDLabels...),
},
},
"All Available": {
Fields: fields{
Client: &clientMock{
available: true,
idDoc: usWestInst,
metadata: map[string]meta{
"hostname": {value: "ip-12-34-56-78.us-west-2.compute.internal"},
},
},
},
Want: want{Resource: resource.New(
semconv.CloudProviderAWS,
semconv.CloudRegionKey.String("us-west-2"),
semconv.CloudZoneKey.String("us-west-2b"),
semconv.CloudAccountIDKey.String("123456789012"),
semconv.HostIDKey.String("i-1234567890abcdef0"),
semconv.HostImageIDKey.String("ami-5fb8c835"),
semconv.HostNameKey.String("ip-12-34-56-78.us-west-2.compute.internal"),
semconv.HostTypeKey.String("t2.micro"),
)},
},
}
Expand All @@ -93,20 +157,28 @@ func TestAWS_Detect(t *testing.T) {

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

assert.Equal(t, tt.Want.Resource, r, "Resource")

if tt.Want.Error != "" {
require.EqualError(t, err, tt.Want.Error, "Error")
assert.Equal(t, tt.Want.Partial, errors.Is(err, resource.ErrPartialResource), "Partial Resource")
return
}

require.NoError(t, err, "Error")
assert.Equal(t, tt.Want.Resource, r, "Resource")
})
}
}

type clientMock struct {
available bool
idDoc func() (ec2metadata.EC2InstanceIdentityDocument, error)
metadata map[string]meta
}

type meta struct {
err error
value string
}

func (c *clientMock) Available() bool {
Expand All @@ -116,3 +188,12 @@ func (c *clientMock) Available() bool {
func (c *clientMock) GetInstanceIdentityDocument() (ec2metadata.EC2InstanceIdentityDocument, error) {
return c.idDoc()
}

func (c *clientMock) GetMetadata(p string) (string, error) {
v, ok := c.metadata[p]
if !ok {
return "", awserr.NewRequestFailure(awserr.New("EC2MetadataError", "failed to make EC2Metadata request", errors.New("response error")), http.StatusNotFound, "test-request")
}

return v.value, v.err
}

0 comments on commit 13ae395

Please sign in to comment.