diff --git a/CHANGELOG.md b/CHANGELOG.md index df0f0cb593b..1d8452d845b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - DynamoDB spans created with the `go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws` package will now have the appropriate database attributes added for the operation being performed. These attributes are detected automatically, but it is also now possible to provide a custom function to set attributes using `WithAttributeSetter`. (#1582) +- Add resource detector for GCP cloud function. ### Fixed diff --git a/detectors/gcp/cloud-function.go b/detectors/gcp/cloud-function.go new file mode 100644 index 00000000000..cd6436eb790 --- /dev/null +++ b/detectors/gcp/cloud-function.go @@ -0,0 +1,71 @@ +// 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 ( + "context" + "os" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.7.0" +) + +const ( + gcpFunctionNameKey = "K_SERVICE" +) + +// NewCloudFunction will return a GCP Cloud Function resource detector. +func NewCloudFunction() resource.Detector { + return &cloudFunction{ + cloudRun: NewCloudRun(), + } +} + +// cloudFunction collects resource information of GCP Cloud Function +type cloudFunction struct { + cloudRun *CloudRun +} + +// Detect detects associated resources when running in GCP Cloud Function. +func (f *cloudFunction) Detect(ctx context.Context) (*resource.Resource, error) { + functionName, ok := f.googleCloudFunctionName() + if !ok { + return nil, nil + } + + projectID, err := f.cloudRun.mc.ProjectID() + if err != nil { + return nil, err + } + region, err := f.cloudRun.cloudRegion(ctx) + if err != nil { + return nil, err + } + + attributes := []attribute.KeyValue{ + semconv.CloudProviderGCP, + semconv.CloudPlatformGCPCloudFunctions, + semconv.FaaSNameKey.String(functionName), + semconv.CloudAccountIDKey.String(projectID), + semconv.CloudRegionKey.String(region), + } + return resource.NewWithAttributes(semconv.SchemaURL, attributes...), nil + +} + +func (f *cloudFunction) googleCloudFunctionName() (string, bool) { + return os.LookupEnv(gcpFunctionNameKey) +} diff --git a/detectors/gcp/cloud-function_test.go b/detectors/gcp/cloud-function_test.go new file mode 100644 index 00000000000..e3d439fc707 --- /dev/null +++ b/detectors/gcp/cloud-function_test.go @@ -0,0 +1,175 @@ +// 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 ( + "context" + "errors" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" +) + +var ( + errTest = errors.New("testError") +) + +const ( + projectIDValue = "some-projectID" + regionValue = "some-region" + functionName = "sample-function" +) + +type metaDataClientImpl struct { + projectID func() (string, error) + get func(string) (string, error) + instanceID func() (string, error) +} + +func (mock *metaDataClientImpl) ProjectID() (string, error) { + if mock.projectID != nil { + return mock.projectID() + } + return "", nil +} + +func (mock *metaDataClientImpl) Get(key string) (string, error) { + if mock.get != nil { + return mock.get(key) + } + return "", nil +} + +func (mock *metaDataClientImpl) InstanceID() (string, error) { + if mock.instanceID != nil { + return mock.instanceID() + } + return "", nil +} + +type want struct { + res *resource.Resource + err error +} + +func TestCloudFunctionDetect(t *testing.T) { + oldValue, ok := os.LookupEnv(gcpFunctionNameKey) + if !ok { + err := os.Setenv(gcpFunctionNameKey, functionName) + if err != nil { + t.Error("unable to set environment variable ", err) + } + } + defer func() { + if !ok { + os.Unsetenv(gcpFunctionNameKey) + } else { + os.Setenv(gcpFunctionNameKey, oldValue) + } + }() + tests := []struct { + name string + cr *CloudRun + expected want + }{ + { + name: "error in reading ProjectID", + cr: &CloudRun{ + mc: &metaDataClientImpl{ + projectID: func() (string, error) { + return "", errTest + }, + }, + }, + expected: want{ + res: nil, + err: errTest, + }, + }, + { + name: "error in reading region", + cr: &CloudRun{ + mc: &metaDataClientImpl{ + get: func(key string) (string, error) { + return "", errTest + }, + }, + }, + expected: want{ + res: nil, + err: errTest, + }, + }, + { + name: "success", + cr: &CloudRun{ + mc: &metaDataClientImpl{ + projectID: func() (string, error) { + return projectIDValue, nil + }, + get: func(key string) (string, error) { + return regionValue, nil + }, + }, + }, + expected: want{ + res: resource.NewSchemaless([]attribute.KeyValue{ + semconv.CloudProviderGCP, + semconv.CloudPlatformGCPCloudFunctions, + semconv.FaaSNameKey.String(functionName), + semconv.CloudAccountIDKey.String(projectIDValue), + semconv.CloudRegionKey.String(regionValue), + }...), + err: nil, + }, + }, + } + + for _, test := range tests { + detector := cloudFunction{ + cloudRun: test.cr, + } + res, err := detector.Detect(context.Background()) + if err != test.expected.err { + t.Fatalf("got unexpected failure: %v", err) + } else if diff := cmp.Diff(test.expected.res, res); diff != "" { + t.Errorf("detected resource differ from expected (-want, +got)\n%s", diff) + } + } +} + +func TestNotOnCloudFunction(t *testing.T) { + oldValue, ok := os.LookupEnv(gcpFunctionNameKey) + if ok { + os.Unsetenv(gcpFunctionNameKey) + } + defer func() { + if ok { + os.Setenv(gcpFunctionNameKey, oldValue) + } + }() + detector := NewCloudFunction() + res, err := detector.Detect(context.Background()) + if err != nil { + t.Errorf("expected cloud function detector to return error as nil, but returned %v", err) + } else if res != nil { + t.Errorf("expected cloud function detector to return resource as nil, but returned %v", res) + } +}