Skip to content

[aws detector] Additional Attributes #410

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}