diff --git a/.chloggen/update-ec2-scraper.yaml b/.chloggen/update-ec2-scraper.yaml new file mode 100755 index 000000000000..d5a2208beb6d --- /dev/null +++ b/.chloggen/update-ec2-scraper.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: 'bug_fix' + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: resourcedetectionprocessor + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Update to ec2 scraper so that core attributes are not dropped if describeTags returns an error (likely due to permissions) + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [30672] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/processor/resourcedetectionprocessor/internal/aws/ec2/ec2.go b/processor/resourcedetectionprocessor/internal/aws/ec2/ec2.go index 34ae8022b4fb..969ed76020ca 100644 --- a/processor/resourcedetectionprocessor/internal/aws/ec2/ec2.go +++ b/processor/resourcedetectionprocessor/internal/aws/ec2/ec2.go @@ -31,11 +31,29 @@ const ( var _ internal.Detector = (*Detector)(nil) +type ec2ifaceBuilder interface { + buildClient(region string, client *http.Client) (ec2iface.EC2API, error) +} + +type ec2ClientBuilder struct{} + +func (e *ec2ClientBuilder) buildClient(region string, client *http.Client) (ec2iface.EC2API, error) { + sess, err := session.NewSession(&aws.Config{ + Region: aws.String(region), + HTTPClient: client}, + ) + if err != nil { + return nil, err + } + return ec2.New(sess), nil +} + type Detector struct { metadataProvider ec2provider.Provider tagKeyRegexes []*regexp.Regexp logger *zap.Logger rb *metadata.ResourceBuilder + ec2ClientBuilder ec2ifaceBuilder } func NewDetector(set processor.CreateSettings, dcfg internal.DetectorConfig) (internal.Detector, error) { @@ -54,6 +72,7 @@ func NewDetector(set processor.CreateSettings, dcfg internal.DetectorConfig) (in tagKeyRegexes: tagKeyRegexes, logger: set.Logger, rb: metadata.NewResourceBuilder(cfg.ResourceAttributes), + ec2ClientBuilder: &ec2ClientBuilder{}, }, nil } @@ -85,16 +104,21 @@ func (d *Detector) Detect(ctx context.Context) (resource pcommon.Resource, schem res := d.rb.Emit() if len(d.tagKeyRegexes) != 0 { - client := getClientConfig(ctx, d.logger) - tags, err := connectAndFetchEc2Tags(meta.Region, meta.InstanceID, d.tagKeyRegexes, client) + httpClient := getClientConfig(ctx, d.logger) + ec2Client, err := d.ec2ClientBuilder.buildClient(meta.Region, httpClient) if err != nil { - return res, "", fmt.Errorf("failed fetching ec2 instance tags: %w", err) + d.logger.Warn("failed to build ec2 client", zap.Error(err)) + return res, conventions.SchemaURL, nil } - for key, val := range tags { - res.Attributes().PutStr(tagPrefix+key, val) + tags, err := fetchEC2Tags(ec2Client, meta.InstanceID, d.tagKeyRegexes) + if err != nil { + d.logger.Warn("failed fetching ec2 instance tags", zap.Error(err)) + } else { + for key, val := range tags { + res.Attributes().PutStr(tagPrefix+key, val) + } } } - return res, conventions.SchemaURL, nil } @@ -107,19 +131,6 @@ func getClientConfig(ctx context.Context, logger *zap.Logger) *http.Client { return client } -func connectAndFetchEc2Tags(region string, instanceID string, tagKeyRegexes []*regexp.Regexp, client *http.Client) (map[string]string, error) { - sess, err := session.NewSession(&aws.Config{ - Region: aws.String(region), - HTTPClient: client}, - ) - if err != nil { - return nil, err - } - e := ec2.New(sess) - - return fetchEC2Tags(e, instanceID, tagKeyRegexes) -} - func fetchEC2Tags(svc ec2iface.EC2API, instanceID string, tagKeyRegexes []*regexp.Regexp) (map[string]string, error) { ec2Tags, err := svc.DescribeTags(&ec2.DescribeTagsInput{ Filters: []*ec2.Filter{{ diff --git a/processor/resourcedetectionprocessor/internal/aws/ec2/ec2_test.go b/processor/resourcedetectionprocessor/internal/aws/ec2/ec2_test.go index b57ca85a2814..17916ef75356 100644 --- a/processor/resourcedetectionprocessor/internal/aws/ec2/ec2_test.go +++ b/processor/resourcedetectionprocessor/internal/aws/ec2/ec2_test.go @@ -6,6 +6,7 @@ package ec2 import ( "context" "errors" + "net/http" "regexp" "testing" @@ -36,6 +37,18 @@ type mockMetadata struct { var _ ec2provider.Provider = (*mockMetadata)(nil) +type mockClientBuilder struct{} + +func (e *mockClientBuilder) buildClient(_ string, _ *http.Client) (ec2iface.EC2API, error) { + return &mockEC2Client{}, nil +} + +type mockClientBuilderError struct{} + +func (e *mockClientBuilderError) buildClient(_ string, _ *http.Client) (ec2iface.EC2API, error) { + return &mockEC2ClientError{}, nil +} + func (mm mockMetadata) InstanceID(_ context.Context) (string, error) { if !mm.isAvailable { return "", errUnavailable @@ -97,6 +110,41 @@ func TestNewDetector(t *testing.T) { } } +// Define a mock client to mock connecting to an EC2 instance +type mockEC2ClientError struct { + ec2iface.EC2API +} + +// override the DescribeTags function to mock the output from an actual EC2 instance +func (m *mockEC2ClientError) DescribeTags(_ *ec2.DescribeTagsInput) (*ec2.DescribeTagsOutput, error) { + return nil, errors.New("Error fetching tags") +} + +type mockEC2Client struct { + ec2iface.EC2API +} + +// override the DescribeTags function to mock the output from an actual EC2 instance +func (m *mockEC2Client) DescribeTags(input *ec2.DescribeTagsInput) (*ec2.DescribeTagsOutput, error) { + if *input.Filters[0].Values[0] == "error" { + return nil, errors.New("error") + } + + tag1 := "tag1" + tag2 := "tag2" + resource1 := "resource1" + val1 := "val1" + val2 := "val2" + resourceType := "type" + + return &ec2.DescribeTagsOutput{ + Tags: []*ec2.TagDescription{ + {Key: &tag1, ResourceId: &resource1, ResourceType: &resourceType, Value: &val1}, + {Key: &tag2, ResourceId: &resource1, ResourceType: &resourceType, Value: &val2}, + }, + }, nil +} + func TestDetector_Detect(t *testing.T) { type fields struct { metadataProvider ec2provider.Provider @@ -105,11 +153,13 @@ func TestDetector_Detect(t *testing.T) { ctx context.Context } tests := []struct { - name string - fields fields - args args - want pcommon.Resource - wantErr bool + name string + fields fields + tagKeyRegexes []*regexp.Regexp + args args + want pcommon.Resource + wantErr bool + tagsProvider ec2ifaceBuilder }{ { name: "success", @@ -139,6 +189,69 @@ func TestDetector_Detect(t *testing.T) { attr.PutStr("host.name", "example-hostname") return res }()}, + { + name: "success with tags", + fields: fields{metadataProvider: &mockMetadata{ + retIDDoc: ec2metadata.EC2InstanceIdentityDocument{ + Region: "us-west-2", + AccountID: "account1234", + AvailabilityZone: "us-west-2a", + InstanceID: "i-abcd1234", + ImageID: "abcdef", + InstanceType: "c4.xlarge", + }, + retHostname: "example-hostname", + isAvailable: true}}, + tagKeyRegexes: []*regexp.Regexp{regexp.MustCompile("^tag1$"), regexp.MustCompile("^tag2$")}, + args: args{ctx: context.Background()}, + want: func() pcommon.Resource { + res := pcommon.NewResource() + attr := res.Attributes() + attr.PutStr("cloud.account.id", "account1234") + attr.PutStr("cloud.provider", "aws") + attr.PutStr("cloud.platform", "aws_ec2") + attr.PutStr("cloud.region", "us-west-2") + attr.PutStr("cloud.availability_zone", "us-west-2a") + attr.PutStr("host.id", "i-abcd1234") + attr.PutStr("host.image.id", "abcdef") + attr.PutStr("host.type", "c4.xlarge") + attr.PutStr("host.name", "example-hostname") + attr.PutStr("ec2.tag.tag1", "val1") + attr.PutStr("ec2.tag.tag2", "val2") + return res + }(), + tagsProvider: &mockClientBuilder{}, + }, + { + name: "success without tags returned from describeTags", + fields: fields{metadataProvider: &mockMetadata{ + retIDDoc: ec2metadata.EC2InstanceIdentityDocument{ + Region: "us-west-2", + AccountID: "account1234", + AvailabilityZone: "us-west-2a", + InstanceID: "i-abcd1234", + ImageID: "abcdef", + InstanceType: "c4.xlarge", + }, + retHostname: "example-hostname", + isAvailable: true}}, + args: args{ctx: context.Background()}, + want: func() pcommon.Resource { + res := pcommon.NewResource() + attr := res.Attributes() + attr.PutStr("cloud.account.id", "account1234") + attr.PutStr("cloud.provider", "aws") + attr.PutStr("cloud.platform", "aws_ec2") + attr.PutStr("cloud.region", "us-west-2") + attr.PutStr("cloud.availability_zone", "us-west-2a") + attr.PutStr("host.id", "i-abcd1234") + attr.PutStr("host.image.id", "abcdef") + attr.PutStr("host.type", "c4.xlarge") + attr.PutStr("host.name", "example-hostname") + return res + }(), + tagsProvider: &mockClientBuilderError{}, + }, { name: "endpoint not available", fields: fields{metadataProvider: &mockMetadata{ @@ -177,6 +290,8 @@ func TestDetector_Detect(t *testing.T) { metadataProvider: tt.fields.metadataProvider, logger: zap.NewNop(), rb: metadata.NewResourceBuilder(metadata.DefaultResourceAttributesConfig()), + tagKeyRegexes: tt.tagKeyRegexes, + ec2ClientBuilder: tt.tagsProvider, } got, _, err := d.Detect(tt.args.ctx) @@ -191,32 +306,6 @@ func TestDetector_Detect(t *testing.T) { } } -// Define a mock client to mock connecting to an EC2 instance -type mockEC2Client struct { - ec2iface.EC2API -} - -// override the DescribeTags function to mock the output from an actual EC2 instance -func (m *mockEC2Client) DescribeTags(input *ec2.DescribeTagsInput) (*ec2.DescribeTagsOutput, error) { - if *input.Filters[0].Values[0] == "error" { - return nil, errors.New("error") - } - - tag1 := "tag1" - tag2 := "tag2" - resource1 := "resource1" - val1 := "val1" - val2 := "val2" - resourceType := "type" - - return &ec2.DescribeTagsOutput{ - Tags: []*ec2.TagDescription{ - {Key: &tag1, ResourceId: &resource1, ResourceType: &resourceType, Value: &val1}, - {Key: &tag2, ResourceId: &resource1, ResourceType: &resourceType, Value: &val2}, - }, - }, nil -} - func TestEC2Tags(t *testing.T) { tests := []struct { name string