diff --git a/.chloggen/eks_cluster_name.yaml b/.chloggen/eks_cluster_name.yaml new file mode 100755 index 000000000000..949bcde02c54 --- /dev/null +++ b/.chloggen/eks_cluster_name.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: enhancement + +# 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: Add k8s cluster name detection when running in EKS + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [26794] + +# (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: [user] diff --git a/processor/resourcedetectionprocessor/README.md b/processor/resourcedetectionprocessor/README.md index d5500d70b587..87ff503f3d49 100644 --- a/processor/resourcedetectionprocessor/README.md +++ b/processor/resourcedetectionprocessor/README.md @@ -318,6 +318,10 @@ processors: * cloud.provider ("aws") * cloud.platform ("aws_eks") + * k8s.cluster.name + +Note: The kubernetes cluster name is only available when running on EC2 instances, and requires permission to run the `EC2:DescribeInstances` [action](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html). +If you see an error with the message `context deadline exceeded`, please increase the timeout setting in your config. Example: @@ -325,7 +329,7 @@ Example: processors: resourcedetection/eks: detectors: [env, eks] - timeout: 2s + timeout: 15s override: false ``` diff --git a/processor/resourcedetectionprocessor/internal/aws/eks/detector.go b/processor/resourcedetectionprocessor/internal/aws/eks/detector.go index 5727669c7f6f..c287921e73ae 100644 --- a/processor/resourcedetectionprocessor/internal/aws/eks/detector.go +++ b/processor/resourcedetectionprocessor/internal/aws/eks/detector.go @@ -7,7 +7,12 @@ import ( "context" "fmt" "os" + "strings" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/ec2metadata" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/processor" conventions "go.opentelemetry.io/collector/semconv/v1.6.1" @@ -28,10 +33,16 @@ const ( kubernetesServiceHostEnvVar = "KUBERNETES_SERVICE_HOST" authConfigmapNS = "kube-system" authConfigmapName = "aws-auth" + + clusterNameAwsEksTag = "aws:eks:cluster-name" + clusterNameEksTag = "eks:cluster-name" + kubernetesClusterNameTag = "kubernetes.io/cluster/" ) type detectorUtils interface { getConfigMap(ctx context.Context, namespace string, name string) (map[string]string, error) + getClusterName(ctx context.Context, logger *zap.Logger) string + getClusterNameTagFromReservations([]*ec2.Reservation) string } type eksDetectorUtils struct { @@ -43,6 +54,7 @@ type detector struct { utils detectorUtils logger *zap.Logger err error + ra metadata.ResourceAttributesConfig rb *metadata.ResourceBuilder } @@ -54,10 +66,12 @@ var _ detectorUtils = (*eksDetectorUtils)(nil) func NewDetector(set processor.CreateSettings, dcfg internal.DetectorConfig) (internal.Detector, error) { cfg := dcfg.(Config) utils, err := newK8sDetectorUtils() + return &detector{ utils: utils, logger: set.Logger, err: err, + ra: cfg.ResourceAttributes, rb: metadata.NewResourceBuilder(cfg.ResourceAttributes), }, nil } @@ -74,6 +88,11 @@ func (d *detector) Detect(ctx context.Context) (resource pcommon.Resource, schem d.rb.SetCloudProvider(conventions.AttributeCloudProviderAWS) d.rb.SetCloudPlatform(conventions.AttributeCloudPlatformAWSEKS) + if d.ra.K8sClusterName.Enabled { + clusterName := d.utils.getClusterName(ctx, d.logger) + d.rb.SetK8sClusterName(clusterName) + } + return d.rb.Emit(), conventions.SchemaURL, nil } @@ -114,3 +133,64 @@ func (e eksDetectorUtils) getConfigMap(ctx context.Context, namespace string, na } return cm.Data, nil } + +func (e eksDetectorUtils) getClusterName(ctx context.Context, logger *zap.Logger) string { + defaultErrorMessage := "Unable to get EKS cluster name" + sess, err := session.NewSession() + if err != nil { + logger.Warn(defaultErrorMessage, zap.Error(err)) + return "" + } + + ec2Svc := ec2metadata.New(sess) + region, err := ec2Svc.Region() + if err != nil { + logger.Warn(defaultErrorMessage, zap.Error(err)) + return "" + } + + svc := ec2.New(sess, aws.NewConfig().WithRegion(region)) + instanceIdentityDocument, err := ec2Svc.GetInstanceIdentityDocumentWithContext(ctx) + if err != nil { + logger.Warn(defaultErrorMessage, zap.Error(err)) + return "" + } + + instances, err := svc.DescribeInstances(&ec2.DescribeInstancesInput{ + InstanceIds: []*string{ + aws.String(instanceIdentityDocument.InstanceID), + }, + }) + if err != nil { + logger.Warn(defaultErrorMessage, zap.Error(err)) + return "" + } + + clusterName := e.getClusterNameTagFromReservations(instances.Reservations) + if len(clusterName) == 0 { + logger.Warn("Failed to detect EKS cluster name. No tag for cluster name found on EC2 instance") + return "" + } + + return clusterName +} + +func (e eksDetectorUtils) getClusterNameTagFromReservations(reservations []*ec2.Reservation) string { + for _, reservation := range reservations { + for _, instance := range reservation.Instances { + for _, tag := range instance.Tags { + if tag.Key == nil { + continue + } + + if *tag.Key == clusterNameAwsEksTag || *tag.Key == clusterNameEksTag { + return *tag.Value + } else if strings.HasPrefix(*tag.Key, kubernetesClusterNameTag) { + return strings.TrimPrefix(*tag.Key, kubernetesClusterNameTag) + } + } + } + } + + return "" +} diff --git a/processor/resourcedetectionprocessor/internal/aws/eks/detector_test.go b/processor/resourcedetectionprocessor/internal/aws/eks/detector_test.go index daf8a9bedbda..0b85120932e2 100644 --- a/processor/resourcedetectionprocessor/internal/aws/eks/detector_test.go +++ b/processor/resourcedetectionprocessor/internal/aws/eks/detector_test.go @@ -7,15 +7,21 @@ import ( "context" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/processor/processortest" + conventions "go.opentelemetry.io/collector/semconv/v1.6.1" "go.uber.org/zap" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata" ) +const ( + clusterName = "my-cluster" +) + type MockDetectorUtils struct { mock.Mock } @@ -25,6 +31,15 @@ func (detectorUtils *MockDetectorUtils) getConfigMap(_ context.Context, namespac return args.Get(0).(map[string]string), args.Error(1) } +func (detectorUtils *MockDetectorUtils) getClusterName(_ context.Context, _ *zap.Logger) string { + var reservations []*ec2.Reservation + return detectorUtils.getClusterNameTagFromReservations(reservations) +} + +func (detectorUtils *MockDetectorUtils) getClusterNameTagFromReservations(_ []*ec2.Reservation) string { + return clusterName +} + func TestNewDetector(t *testing.T) { dcfg := CreateDefaultConfig() detector, err := NewDetector(processortest.NewNopCreateSettings(), dcfg) @@ -38,9 +53,9 @@ func TestEKS(t *testing.T) { ctx := context.Background() t.Setenv("KUBERNETES_SERVICE_HOST", "localhost") - detectorUtils.On("getConfigMap", authConfigmapNS, authConfigmapName).Return(map[string]string{"cluster.name": "my-cluster"}, nil) + detectorUtils.On("getConfigMap", authConfigmapNS, authConfigmapName).Return(map[string]string{conventions.AttributeK8SClusterName: clusterName}, nil) // Call EKS Resource detector to detect resources - eksResourceDetector := &detector{utils: detectorUtils, err: nil, rb: metadata.NewResourceBuilder(metadata.DefaultResourceAttributesConfig())} + eksResourceDetector := &detector{utils: detectorUtils, err: nil, ra: metadata.DefaultResourceAttributesConfig(), rb: metadata.NewResourceBuilder(metadata.DefaultResourceAttributesConfig())} res, _, err := eksResourceDetector.Detect(ctx) require.NoError(t, err) diff --git a/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_config.go b/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_config.go index 47fbd55571ca..f21c9ae76a1d 100644 --- a/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_config.go +++ b/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_config.go @@ -25,8 +25,9 @@ func (rac *ResourceAttributeConfig) Unmarshal(parser *confmap.Conf) error { // ResourceAttributesConfig provides config for resourcedetectionprocessor/eks resource attributes. type ResourceAttributesConfig struct { - CloudPlatform ResourceAttributeConfig `mapstructure:"cloud.platform"` - CloudProvider ResourceAttributeConfig `mapstructure:"cloud.provider"` + CloudPlatform ResourceAttributeConfig `mapstructure:"cloud.platform"` + CloudProvider ResourceAttributeConfig `mapstructure:"cloud.provider"` + K8sClusterName ResourceAttributeConfig `mapstructure:"k8s.cluster.name"` } func DefaultResourceAttributesConfig() ResourceAttributesConfig { @@ -37,5 +38,8 @@ func DefaultResourceAttributesConfig() ResourceAttributesConfig { CloudProvider: ResourceAttributeConfig{ Enabled: true, }, + K8sClusterName: ResourceAttributeConfig{ + Enabled: false, + }, } } diff --git a/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_config_test.go b/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_config_test.go index 9ce16e7f0d6a..fa542527d5fb 100644 --- a/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_config_test.go +++ b/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_config_test.go @@ -25,15 +25,17 @@ func TestResourceAttributesConfig(t *testing.T) { { name: "all_set", want: ResourceAttributesConfig{ - CloudPlatform: ResourceAttributeConfig{Enabled: true}, - CloudProvider: ResourceAttributeConfig{Enabled: true}, + CloudPlatform: ResourceAttributeConfig{Enabled: true}, + CloudProvider: ResourceAttributeConfig{Enabled: true}, + K8sClusterName: ResourceAttributeConfig{Enabled: true}, }, }, { name: "none_set", want: ResourceAttributesConfig{ - CloudPlatform: ResourceAttributeConfig{Enabled: false}, - CloudProvider: ResourceAttributeConfig{Enabled: false}, + CloudPlatform: ResourceAttributeConfig{Enabled: false}, + CloudProvider: ResourceAttributeConfig{Enabled: false}, + K8sClusterName: ResourceAttributeConfig{Enabled: false}, }, }, } diff --git a/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_resource.go b/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_resource.go index aff8c18f53ad..b4286d831a6b 100644 --- a/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_resource.go +++ b/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_resource.go @@ -35,6 +35,13 @@ func (rb *ResourceBuilder) SetCloudProvider(val string) { } } +// SetK8sClusterName sets provided value as "k8s.cluster.name" attribute. +func (rb *ResourceBuilder) SetK8sClusterName(val string) { + if rb.config.K8sClusterName.Enabled { + rb.res.Attributes().PutStr("k8s.cluster.name", val) + } +} + // Emit returns the built resource and resets the internal builder state. func (rb *ResourceBuilder) Emit() pcommon.Resource { r := rb.res diff --git a/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_resource_test.go b/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_resource_test.go index 40fc980e81bc..a467659755bc 100644 --- a/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_resource_test.go +++ b/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/generated_resource_test.go @@ -15,6 +15,7 @@ func TestResourceBuilder(t *testing.T) { rb := NewResourceBuilder(cfg) rb.SetCloudPlatform("cloud.platform-val") rb.SetCloudProvider("cloud.provider-val") + rb.SetK8sClusterName("k8s.cluster.name-val") res := rb.Emit() assert.Equal(t, 0, rb.Emit().Attributes().Len()) // Second call should return empty Resource @@ -23,7 +24,7 @@ func TestResourceBuilder(t *testing.T) { case "default": assert.Equal(t, 2, res.Attributes().Len()) case "all_set": - assert.Equal(t, 2, res.Attributes().Len()) + assert.Equal(t, 3, res.Attributes().Len()) case "none_set": assert.Equal(t, 0, res.Attributes().Len()) return @@ -41,6 +42,11 @@ func TestResourceBuilder(t *testing.T) { if ok { assert.EqualValues(t, "cloud.provider-val", val.Str()) } + val, ok = res.Attributes().Get("k8s.cluster.name") + assert.Equal(t, test == "all_set", ok) + if ok { + assert.EqualValues(t, "k8s.cluster.name-val", val.Str()) + } }) } } diff --git a/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/testdata/config.yaml b/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/testdata/config.yaml index d00b63470c51..1b7d4c7eda41 100644 --- a/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/testdata/config.yaml +++ b/processor/resourcedetectionprocessor/internal/aws/eks/internal/metadata/testdata/config.yaml @@ -5,9 +5,13 @@ all_set: enabled: true cloud.provider: enabled: true + k8s.cluster.name: + enabled: true none_set: resource_attributes: cloud.platform: enabled: false cloud.provider: enabled: false + k8s.cluster.name: + enabled: false diff --git a/processor/resourcedetectionprocessor/internal/aws/eks/metadata.yaml b/processor/resourcedetectionprocessor/internal/aws/eks/metadata.yaml index e544e7c1f552..9911e7164b53 100644 --- a/processor/resourcedetectionprocessor/internal/aws/eks/metadata.yaml +++ b/processor/resourcedetectionprocessor/internal/aws/eks/metadata.yaml @@ -10,4 +10,8 @@ resource_attributes: cloud.platform: description: The cloud.platform type: string - enabled: true \ No newline at end of file + enabled: true + k8s.cluster.name: + description: The EKS cluster name. This attribute is currently only available when running on EC2 instances, and requires permission to run the EC2:DescribeInstances action. + type: string + enabled: false \ No newline at end of file