diff --git a/processor/resourcedetectionprocessor/README.md b/processor/resourcedetectionprocessor/README.md index 03b08add2587..b743a227704e 100644 --- a/processor/resourcedetectionprocessor/README.md +++ b/processor/resourcedetectionprocessor/README.md @@ -29,6 +29,12 @@ to read resource information from the [GCE metadata server](https://cloud.google * host.image.id * host.type +* GKE: Google Kubernetes Engine + + * cloud.provider ("gcp") + * cloud.infrastructure_service ("gcp_gke") + * k8s.cluster.name (name of the GKE cluster) + * AWS EC2: Uses [AWS SDK for Go](https://docs.aws.amazon.com/sdk-for-go/api/aws/ec2metadata/) to read resource information from the [EC2 instance metadata API](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html) to retrieve the following resource attributes: * cloud.provider ("aws") @@ -94,7 +100,7 @@ ec2: ## Configuration ```yaml -# a list of resource detectors to run, valid options are: "env", "system", "gce", "ec2", "ecs", "elastic_beanstalk", "azure" +# a list of resource detectors to run, valid options are: "env", "system", "gce", "gke", "ec2", "ecs", "elastic_beanstalk", "azure" detectors: [ ] # determines if existing resource attributes should be overridden or preserved, defaults to true override: diff --git a/processor/resourcedetectionprocessor/factory.go b/processor/resourcedetectionprocessor/factory.go index a4b64d37ec9c..5aa3a4c12a62 100644 --- a/processor/resourcedetectionprocessor/factory.go +++ b/processor/resourcedetectionprocessor/factory.go @@ -32,6 +32,7 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/azure" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/env" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/gcp/gce" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/gcp/gke" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/system" ) @@ -54,13 +55,14 @@ type factory struct { // NewFactory creates a new factory for ResourceDetection processor. func NewFactory() component.ProcessorFactory { resourceProviderFactory := internal.NewProviderFactory(map[internal.DetectorType]internal.DetectorFactory{ - env.TypeStr: env.NewDetector, - system.TypeStr: system.NewDetector, - gce.TypeStr: gce.NewDetector, + azure.TypeStr: azure.NewDetector, ec2.TypeStr: ec2.NewDetector, ecs.TypeStr: ecs.NewDetector, elasticbeanstalk.TypeStr: elasticbeanstalk.NewDetector, - azure.TypeStr: azure.NewDetector, + env.TypeStr: env.NewDetector, + gce.TypeStr: gce.NewDetector, + gke.TypeStr: gke.NewDetector, + system.TypeStr: system.NewDetector, }) f := &factory{ diff --git a/processor/resourcedetectionprocessor/internal/gcp/gce/gce.go b/processor/resourcedetectionprocessor/internal/gcp/gce/gce.go index e114f0ea676d..d30b5fa1606b 100644 --- a/processor/resourcedetectionprocessor/internal/gcp/gce/gce.go +++ b/processor/resourcedetectionprocessor/internal/gcp/gce/gce.go @@ -25,6 +25,7 @@ import ( "go.opentelemetry.io/collector/translator/conventions" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/gcp" ) const ( @@ -34,11 +35,11 @@ const ( var _ internal.Detector = (*Detector)(nil) type Detector struct { - metadata gceMetadata + metadata gcp.Metadata } func NewDetector(component.ProcessorCreateParams, internal.DetectorConfig) (internal.Detector, error) { - return &Detector{metadata: &gceMetadataImpl{}}, nil + return &Detector{metadata: &gcp.MetadataImpl{}}, nil } func (d *Detector) Detect(context.Context) (pdata.Resource, error) { diff --git a/processor/resourcedetectionprocessor/internal/gcp/gce/gce_test.go b/processor/resourcedetectionprocessor/internal/gcp/gce/gce_test.go index d12cb6e9d190..727cc4b2967d 100644 --- a/processor/resourcedetectionprocessor/internal/gcp/gce/gce_test.go +++ b/processor/resourcedetectionprocessor/internal/gcp/gce/gce_test.go @@ -20,53 +20,15 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/translator/conventions" "go.uber.org/zap" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/gcp" ) -type mockMetadata struct { - mock.Mock -} - -func (m *mockMetadata) OnGCE() bool { - return m.MethodCalled("OnGCE").Bool(0) -} - -func (m *mockMetadata) ProjectID() (string, error) { - args := m.MethodCalled("ProjectID") - return args.String(0), args.Error(1) -} - -func (m *mockMetadata) Zone() (string, error) { - args := m.MethodCalled("Zone") - return args.String(0), args.Error(1) -} - -func (m *mockMetadata) Hostname() (string, error) { - args := m.MethodCalled("Hostname") - return args.String(0), args.Error(1) -} - -func (m *mockMetadata) InstanceID() (string, error) { - args := m.MethodCalled("InstanceID") - return args.String(0), args.Error(1) -} - -func (m *mockMetadata) InstanceName() (string, error) { - args := m.MethodCalled("InstanceName") - return args.String(0), args.Error(1) -} - -func (m *mockMetadata) Get(suffix string) (string, error) { - args := m.MethodCalled("Get") - return args.String(0), args.Error(1) -} - func TestNewDetector(t *testing.T) { d, err := NewDetector(component.ProcessorCreateParams{Logger: zap.NewNop()}, nil) assert.NotNil(t, d) @@ -74,14 +36,14 @@ func TestNewDetector(t *testing.T) { } func TestDetectTrue(t *testing.T) { - md := &mockMetadata{} + md := &gcp.MockMetadata{} md.On("OnGCE").Return(true) md.On("ProjectID").Return("1", nil) md.On("Zone").Return("zone", nil) md.On("Hostname").Return("hostname", nil) md.On("InstanceID").Return("2", nil) md.On("InstanceName").Return("name", nil) - md.On("Get").Return("machine-type", nil) + md.On("Get", "instance/machine-type").Return("machine-type", nil) detector := &Detector{metadata: md} res, err := detector.Detect(context.Background()) @@ -105,7 +67,7 @@ func TestDetectTrue(t *testing.T) { } func TestDetectFalse(t *testing.T) { - md := &mockMetadata{} + md := &gcp.MockMetadata{} md.On("OnGCE").Return(false) detector := &Detector{metadata: md} @@ -116,14 +78,14 @@ func TestDetectFalse(t *testing.T) { } func TestDetectError(t *testing.T) { - md := &mockMetadata{} + md := &gcp.MockMetadata{} md.On("OnGCE").Return(true) md.On("ProjectID").Return("", errors.New("err1")) md.On("Zone").Return("", errors.New("err2")) md.On("Hostname").Return("", errors.New("err3")) md.On("InstanceID").Return("", errors.New("err4")) md.On("InstanceName").Return("", errors.New("err5")) - md.On("Get").Return("", errors.New("err6")) + md.On("Get", "instance/machine-type").Return("", errors.New("err6")) detector := &Detector{metadata: md} res, err := detector.Detect(context.Background()) diff --git a/processor/resourcedetectionprocessor/internal/gcp/gke/gke.go b/processor/resourcedetectionprocessor/internal/gcp/gke/gke.go new file mode 100644 index 000000000000..d6210c7afe1e --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/gcp/gke/gke.go @@ -0,0 +1,78 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gke + +import ( + "context" + "os" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/consumer/pdata" + "go.opentelemetry.io/collector/translator/conventions" + "go.uber.org/zap" + + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/gcp" +) + +const ( + // TypeStr is type of detector. + TypeStr = "gke" + + // GCE metadata attribute containing the GKE cluster name. + clusterNameAttribute = "cluster-name" + + // Environment variable that is set when running on Kubernetes. + kubernetesServiceHostEnvVar = "KUBERNETES_SERVICE_HOST" +) + +var _ internal.Detector = (*Detector)(nil) + +type Detector struct { + log *zap.Logger + metadata gcp.Metadata +} + +func NewDetector(params component.ProcessorCreateParams, _ internal.DetectorConfig) (internal.Detector, error) { + return &Detector{log: params.Logger, metadata: &gcp.MetadataImpl{}}, nil +} + +// Detect detects associated resources when running in GKE environment. +func (gke *Detector) Detect(ctx context.Context) (pdata.Resource, error) { + res := pdata.NewResource() + + // Check if on GCP. + if !gke.metadata.OnGCE() { + return res, nil + } + + attr := res.Attributes() + attr.InsertString(conventions.AttributeCloudProvider, conventions.AttributeCloudProviderGCP) + + // Check if running on k8s. + if os.Getenv(kubernetesServiceHostEnvVar) == "" { + return res, nil + } + + attr.InsertString(conventions.AttributeCloudInfrastructureService, conventions.AttributeCloudProviderGCPGKE) + + if clusterName, err := gke.metadata.InstanceAttributeValue(clusterNameAttribute); err != nil { + gke.log.Warn("Unable to determine GKE cluster name", zap.Error(err)) + } else if clusterName != "" { + attr.InsertString(conventions.AttributeK8sCluster, clusterName) + } + + return res, nil +} diff --git a/processor/resourcedetectionprocessor/internal/gcp/gke/gke_test.go b/processor/resourcedetectionprocessor/internal/gcp/gke/gke_test.go new file mode 100644 index 000000000000..3757dd2e0f1a --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/gcp/gke/gke_test.go @@ -0,0 +1,119 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gke + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.uber.org/zap" + + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/gcp" +) + +func TestNotGCE(t *testing.T) { + metadata := &gcp.MockMetadata{} + detector := &Detector{ + log: zap.NewNop(), + metadata: metadata, + } + + metadata.On("OnGCE").Return(false) + res, err := detector.Detect(context.Background()) + require.NoError(t, err) + assert.Equal(t, 0, res.Attributes().Len()) + + metadata.AssertExpectations(t) +} + +func TestDetectWithoutCluster(t *testing.T) { + metadata := &gcp.MockMetadata{} + detector := &Detector{ + log: zap.NewNop(), + metadata: metadata, + } + + metadata.On("OnGCE").Return(true) + metadata.On("InstanceAttributeValue", "cluster-name").Return("", errors.New("no cluster")) + + require.NoError(t, os.Setenv("KUBERNETES_SERVICE_HOST", "localhost")) + + res, err := detector.Detect(context.Background()) + require.NoError(t, err) + + assert.Equal(t, map[string]interface{}{ + "cloud.provider": "gcp", + "cloud.infrastructure_service": "gcp_gke", + }, internal.AttributesToMap(res.Attributes())) + + metadata.AssertExpectations(t) +} + +func TestDetectWithoutK8s(t *testing.T) { + metadata := &gcp.MockMetadata{} + detector := &Detector{ + log: zap.NewNop(), + metadata: metadata, + } + + metadata.On("OnGCE").Return(true) + + require.NoError(t, os.Unsetenv("KUBERNETES_SERVICE_HOST")) + + res, err := detector.Detect(context.Background()) + require.NoError(t, err) + + assert.Equal(t, map[string]interface{}{ + "cloud.provider": "gcp", + }, internal.AttributesToMap(res.Attributes())) + + metadata.AssertExpectations(t) +} + +func TestDetector_Detect(t *testing.T) { + metadata := &gcp.MockMetadata{} + detector := &Detector{ + log: zap.NewNop(), + metadata: metadata, + } + + metadata.On("OnGCE").Return(true) + metadata.On("InstanceAttributeValue", "cluster-name").Return("cluster-a", nil) + + require.NoError(t, os.Setenv("KUBERNETES_SERVICE_HOST", "localhost")) + + res, err := detector.Detect(context.Background()) + require.NoError(t, err) + + assert.Equal(t, map[string]interface{}{ + "cloud.provider": "gcp", + "cloud.infrastructure_service": "gcp_gke", + "k8s.cluster.name": "cluster-a", + }, internal.AttributesToMap(res.Attributes())) + + metadata.AssertExpectations(t) +} + +func TestNewDetector(t *testing.T) { + detector, err := NewDetector(component.ProcessorCreateParams{Logger: zap.NewNop()}, nil) + assert.NoError(t, err) + assert.NotNil(t, detector) +} diff --git a/processor/resourcedetectionprocessor/internal/gcp/gce/metadata.go b/processor/resourcedetectionprocessor/internal/gcp/metadata.go similarity index 62% rename from processor/resourcedetectionprocessor/internal/gcp/gce/metadata.go rename to processor/resourcedetectionprocessor/internal/gcp/metadata.go index be5198615eef..167965259c24 100644 --- a/processor/resourcedetectionprocessor/internal/gcp/gce/metadata.go +++ b/processor/resourcedetectionprocessor/internal/gcp/metadata.go @@ -12,46 +12,53 @@ // See the License for the specific language governing permissions and // limitations under the License. -package gce +package gcp import "cloud.google.com/go/compute/metadata" -type gceMetadata interface { +type Metadata interface { OnGCE() bool ProjectID() (string, error) Zone() (string, error) Hostname() (string, error) + InstanceAttributeValue(attr string) (string, error) InstanceID() (string, error) InstanceName() (string, error) Get(suffix string) (string, error) } -type gceMetadataImpl struct{} +type MetadataImpl struct{} -func (m *gceMetadataImpl) OnGCE() bool { +var _ Metadata = (*MetadataImpl)(nil) + +func (m *MetadataImpl) OnGCE() bool { return metadata.OnGCE() } -func (m *gceMetadataImpl) ProjectID() (string, error) { +func (m *MetadataImpl) ProjectID() (string, error) { return metadata.ProjectID() } -func (m *gceMetadataImpl) Zone() (string, error) { +func (m *MetadataImpl) Zone() (string, error) { return metadata.Zone() } -func (m *gceMetadataImpl) Hostname() (string, error) { +func (m *MetadataImpl) Hostname() (string, error) { return metadata.Hostname() } -func (m *gceMetadataImpl) InstanceID() (string, error) { +func (m *MetadataImpl) InstanceAttributeValue(attr string) (string, error) { + return metadata.InstanceAttributeValue(attr) +} + +func (m *MetadataImpl) InstanceID() (string, error) { return metadata.InstanceID() } -func (m *gceMetadataImpl) InstanceName() (string, error) { +func (m *MetadataImpl) InstanceName() (string, error) { return metadata.InstanceName() } -func (m *gceMetadataImpl) Get(suffix string) (string, error) { +func (m *MetadataImpl) Get(suffix string) (string, error) { return metadata.Get(suffix) } diff --git a/processor/resourcedetectionprocessor/internal/gcp/gce/metadata_test.go b/processor/resourcedetectionprocessor/internal/gcp/metadata_test.go similarity index 91% rename from processor/resourcedetectionprocessor/internal/gcp/gce/metadata_test.go rename to processor/resourcedetectionprocessor/internal/gcp/metadata_test.go index fb7660288c5f..fb8e2209974c 100644 --- a/processor/resourcedetectionprocessor/internal/gcp/gce/metadata_test.go +++ b/processor/resourcedetectionprocessor/internal/gcp/metadata_test.go @@ -12,18 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -package gce +package gcp import ( "testing" ) func TestGCEMetadata(t *testing.T) { - metadata := &gceMetadataImpl{} + metadata := &MetadataImpl{} metadata.OnGCE() metadata.ProjectID() metadata.Zone() metadata.Hostname() + metadata.InstanceAttributeValue("") metadata.InstanceID() metadata.InstanceName() metadata.Get("") diff --git a/processor/resourcedetectionprocessor/internal/gcp/mockmetadata.go b/processor/resourcedetectionprocessor/internal/gcp/mockmetadata.go new file mode 100644 index 000000000000..a029df76c485 --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/gcp/mockmetadata.go @@ -0,0 +1,64 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gcp + +import ( + "github.com/stretchr/testify/mock" +) + +var _ Metadata = (*MockMetadata)(nil) + +type MockMetadata struct { + mock.Mock +} + +func (m *MockMetadata) OnGCE() bool { + return m.MethodCalled("OnGCE").Bool(0) +} + +func (m *MockMetadata) ProjectID() (string, error) { + args := m.MethodCalled("ProjectID") + return args.String(0), args.Error(1) +} + +func (m *MockMetadata) Zone() (string, error) { + args := m.MethodCalled("Zone") + return args.String(0), args.Error(1) +} + +func (m *MockMetadata) Hostname() (string, error) { + args := m.MethodCalled("Hostname") + return args.String(0), args.Error(1) +} + +func (m *MockMetadata) InstanceAttributeValue(attr string) (string, error) { + args := m.MethodCalled("InstanceAttributeValue", attr) + return args.String(0), args.Error(1) +} + +func (m *MockMetadata) InstanceID() (string, error) { + args := m.MethodCalled("InstanceID") + return args.String(0), args.Error(1) +} + +func (m *MockMetadata) InstanceName() (string, error) { + args := m.MethodCalled("InstanceName") + return args.String(0), args.Error(1) +} + +func (m *MockMetadata) Get(suffix string) (string, error) { + args := m.MethodCalled("Get", suffix) + return args.String(0), args.Error(1) +} diff --git a/processor/resourcedetectionprocessor/internal/gcp/mockmetadata_test.go b/processor/resourcedetectionprocessor/internal/gcp/mockmetadata_test.go new file mode 100644 index 000000000000..7a1fc28238a0 --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/gcp/mockmetadata_test.go @@ -0,0 +1,42 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gcp + +import ( + "testing" +) + +func TestMockGCEMetadata(t *testing.T) { + metadata := &MockMetadata{} + metadata.On("OnGCE").Return(false) + metadata.On("ProjectID").Return("", nil) + metadata.On("Zone").Return("", nil) + metadata.On("Hostname").Return("", nil) + metadata.On("InstanceAttributeValue", "").Return("", nil) + metadata.On("InstanceID").Return("", nil) + metadata.On("InstanceName").Return("", nil) + metadata.On("Get", "").Return("", nil) + + metadata.OnGCE() + metadata.ProjectID() + metadata.Zone() + metadata.Hostname() + metadata.InstanceAttributeValue("") + metadata.InstanceID() + metadata.InstanceName() + metadata.Get("") + + metadata.AssertExpectations(t) +}