Repository: open-telemetry/opentelemetry-go-contrib
Pull request #2310 (2022-05-23 19:42:35)

Unify and improve GCP resource detection, second attempt
This PR supersedes https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/pull/383 and https://github.com/open-telemetry/opentelemetry-go-contrib/pull/2223.  It was also discussed at the May 5th sig meeting.

It implements resource detection for GCE, GKE, GAE, Cloud Run, and Cloud Functions using a single detector.  The underlying logic is [implemented](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/tree/main/detectors/gcp) (and [integration tested](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/e2cce7b04a10906fca4fd38b95ba824a085d61e2/e2e-test-server/endtoendserver/server.go#L153) in GCP environments) in [GoogleCloudPlatform/opentelemetry-operations-go](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go).  The underlying library provides a function, CloudPlatform, which detects which platform it is on, and then one function for each attribute.  The reason for the split between "library" and detector is so that the library functions can be integration tested in real GCP environments, which is difficult to do in upstream repositories.

Unfortunately, the current resource detector was already marked stable, so we won't be able to remove existing types, etc.   I've added deprecation warnings to them to encourage users to move to the new implementation.

When users switch to the new detector, there are a few breaking changes:

* No longer supports detecting k8s.pod.name, container.name, k8s.namespace.name when running on k8s. These are expected to be set outside of this resource detector as described in the README.
* No longer sets service.namespace to "cloud-run-managed" on Cloud Run.
* Sets faas.name instead of service.name on Cloud Run.
* Sets faas.id instead of service.instance.id on Cloud Run.

Additions:

* Adds detector for Google App Engine
* Correctly sets cloud.platform on GKE and Cloud Run
* Detects cloud.availability_zone/region when running on GKE with workload identity enabled (the GCE metadata server is not accessible in that scenario)
* Detects faas.version on cloud run and cloud functions.

codecov[bot]: # [Codecov](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2310?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) Report
> Merging [#2310](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2310?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) (ae4136a) into [main](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/commit/2e08c1fabf73d1926ad5d25e34179bbc018126eb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) (2e08c1f) will **increase** coverage by `0.0%`.
> The diff coverage is `73.8%`.

> :exclamation: Current head ae4136a differs from pull request most recent head 30f2157. Consider uploading reports for the commit 30f2157 to get more accurate results

[![Impacted file tree graph](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2310/graphs/tree.svg?width=650&height=150&src=pr&token=P6F3W9WA7Q&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry)](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2310?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry)

```diff
@@          Coverage Diff          @@
##            main   #2310   +/-   ##
=====================================
  Coverage   73.6%   73.6%           
=====================================
  Files        141     142    +1     
  Lines       6396    6480   +84     
=====================================
+ Hits        4712    4774   +62     
- Misses      1542    1559   +17     
- Partials     142     147    +5     
```


| [Impacted Files](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2310?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) | Coverage Δ | |
|---|---|---|
| [detectors/gcp/cloud-function.go](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2310/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-ZGV0ZWN0b3JzL2djcC9jbG91ZC1mdW5jdGlvbi5nbw==) | `100.0% <ø> (ø)` | |
| [detectors/gcp/cloud-run.go](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2310/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-ZGV0ZWN0b3JzL2djcC9jbG91ZC1ydW4uZ28=) | `100.0% <ø> (ø)` | |
| [detectors/gcp/gce.go](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2310/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-ZGV0ZWN0b3JzL2djcC9nY2UuZ28=) | `8.7% <ø> (ø)` | |
| [detectors/gcp/gke.go](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2310/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-ZGV0ZWN0b3JzL2djcC9na2UuZ28=) | `0.0% <ø> (ø)` | |
| [detectors/gcp/detector.go](https://codecov.io/gh/open-telemetry/opentelemetry-go-contrib/pull/2310/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-ZGV0ZWN0b3JzL2djcC9kZXRlY3Rvci5nbw==) | `73.8% <73.8%> (ø)` | |


Patch:
diff --git a/detectors/gcp/README.md b/detectors/gcp/README.md
new file mode 100644
index 000000000..df8ce1e62
--- /dev/null
+++ b/detectors/gcp/README.md
@@ -0,0 +1,57 @@
+# GCP Resource detector
+
+The GCP resource detector supports detecting resources on:
+
+ * Google Compute Engine (GCE)
+ * Google Kubernetes Engine (GKE)
+ * Google App Engine (GAE)
+ * Cloud Run
+ * Cloud Functions
+
+## Usage
+
+```golang
+ctx := context.Background()
+// Detect your resources
+res, err := resource.New(ctx,
+    // Use the GCP resource detector!
+    resource.WithDetectors(gcp.NewDetector()),
+    // Keep the default detectors
+    resource.WithTelemetrySDK(),
+    // Add your own custom attributes to identify your application
+    resource.WithAttributes(
+        semconv.ServiceNameKey.String("my-application"),
+        semconv.ServiceNamespaceKey.String("my-company-frontend-team"),
+    ),
+)
+if err != nil {
+    // Handle err
+}
+// Use the resource in your tracerprovider (or meterprovider)
+tp := trace.NewTracerProvider(
+    // ... other options
+    trace.WithResource(res),
+)
+```
+
+## Setting Kubernetes attributes
+
+Previous iterations of GCP resource detection attempted to detect
+`container.name`, `k8s.pod.name` and `k8s.namespace.name`.  When using this detector,
+you should use this in your Pod Spec to set these using
+`OTEL_RESOURCE_ATTRIBUTES`:
+
+```yaml
+env:
+- name: POD_NAME
+  valueFrom:
+    fieldRef:
+      fieldPath: metadata.name
+- name: NAMESPACE_NAME
+  valueFrom:
+    fieldRef:
+      fieldPath: metadata.namespace
+- name: CONTAINER_NAME
+  value: my-container-name
+- name: OTEL_RESOURCE_ATTRIBUTES
+  value: k8s.pod.name=$(POD_NAME),k8s.namespace.name=$(NAMESPACE_NAME),k8s.container.name=$(CONTAINER_NAME)
\ No newline at end of file
diff --git a/detectors/gcp/cloud-function.go b/detectors/gcp/cloud-function.go
index 73752f4cb..b99a01c33 100644
--- a/detectors/gcp/cloud-function.go
+++ b/detectors/gcp/cloud-function.go
@@ -28,6 +28,7 @@ const (
 )
 
 // NewCloudFunction will return a GCP Cloud Function resource detector.
+// Deprecated: Use gcp.NewDetector() instead, which sets the same resource attributes.
 func NewCloudFunction() resource.Detector {
 	return &cloudFunction{
 		cloudRun: NewCloudRun(),
diff --git a/detectors/gcp/cloud-run.go b/detectors/gcp/cloud-run.go
index e616c1ae3..dfd7cfdfd 100644
--- a/detectors/gcp/cloud-run.go
+++ b/detectors/gcp/cloud-run.go
@@ -38,6 +38,7 @@ type metadataClient interface {
 }
 
 // CloudRun collects resource information of Cloud Run instance.
+// Deprecated: Use gcp.NewDetector() instead. Note that it sets faas.* resource attributes instead of service.* attributes.
 type CloudRun struct {
 	mc     metadataClient
 	onGCE  func() bool
@@ -49,6 +50,7 @@ type CloudRun struct {
 var _ resource.Detector = (*CloudRun)(nil)
 
 // NewCloudRun creates a CloudRun detector.
+// Deprecated: Use gcp.NewDetector() instead. Note that it sets faas.* resource attributes instead of service.* attributes.
 func NewCloudRun() *CloudRun {
 	return &CloudRun{
 		mc:     metadata.NewClient(nil),
diff --git a/detectors/gcp/detector.go b/detectors/gcp/detector.go
new file mode 100644
index 000000000..8a26d6d07
--- /dev/null
+++ b/detectors/gcp/detector.go
@@ -0,0 +1,135 @@
+// 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 "go.opentelemetry.io/contrib/detectors/gcp"
+
+import (
+	"context"
+	"fmt"
+
+	"cloud.google.com/go/compute/metadata"
+	"github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp"
+
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/sdk/resource"
+	semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
+)
+
+// NewDetector returns a resource detector which detects resource attributes on:
+// * Google Compute Engine (GCE).
+// * Google Kubernetes Engine (GKE).
+// * Google App Engine (GAE).
+// * Cloud Run.
+// * Cloud Functions.
+func NewDetector() resource.Detector {
+	return &detector{detector: gcp.NewDetector()}
+}
+
+type detector struct {
+	detector gcpDetector
+}
+
+// Detect detects associated resources when running on GCE, GKE, GAE,
+// Cloud Run, and Cloud functions.
+func (d *detector) Detect(ctx context.Context) (*resource.Resource, error) {
+	if !metadata.OnGCE() {
+		return nil, nil
+	}
+	b := &resourceBuilder{}
+	b.attrs = append(b.attrs, semconv.CloudProviderGCP)
+	b.add(semconv.CloudAccountIDKey, d.detector.ProjectID)
+
+	switch d.detector.CloudPlatform() {
+	case gcp.GKE:
+		b.attrs = append(b.attrs, semconv.CloudPlatformGCPKubernetesEngine)
+		b.addZoneOrRegion(d.detector.GKEAvailabilityZoneOrRegion)
+		b.add(semconv.K8SClusterNameKey, d.detector.GKEClusterName)
+		b.add(semconv.HostIDKey, d.detector.GKEHostID)
+	case gcp.CloudRun:
+		b.attrs = append(b.attrs, semconv.CloudPlatformGCPCloudRun)
+		b.add(semconv.FaaSNameKey, d.detector.FaaSName)
+		b.add(semconv.FaaSVersionKey, d.detector.FaaSVersion)
+		b.add(semconv.FaaSIDKey, d.detector.FaaSID)
+		b.add(semconv.CloudRegionKey, d.detector.FaaSCloudRegion)
+	case gcp.CloudFunctions:
+		b.attrs = append(b.attrs, semconv.CloudPlatformGCPCloudFunctions)
+		b.add(semconv.FaaSNameKey, d.detector.FaaSName)
+		b.add(semconv.FaaSVersionKey, d.detector.FaaSVersion)
+		b.add(semconv.FaaSIDKey, d.detector.FaaSID)
+		b.add(semconv.CloudRegionKey, d.detector.FaaSCloudRegion)
+	case gcp.AppEngine:
+		b.attrs = append(b.attrs, semconv.CloudPlatformGCPAppEngine)
+		b.addZoneAndRegion(d.detector.AppEngineAvailabilityZoneAndRegion)
+		b.add(semconv.FaaSNameKey, d.detector.AppEngineServiceName)
+		b.add(semconv.FaaSVersionKey, d.detector.AppEngineServiceVersion)
+		b.add(semconv.FaaSIDKey, d.detector.AppEngineServiceInstance)
+	case gcp.GCE:
+		b.attrs = append(b.attrs, semconv.CloudPlatformGCPComputeEngine)
+		b.addZoneAndRegion(d.detector.GCEAvailabilityZoneAndRegion)
+		b.add(semconv.HostTypeKey, d.detector.GCEHostType)
+		b.add(semconv.HostIDKey, d.detector.GCEHostID)
+		b.add(semconv.HostNameKey, d.detector.GCEHostName)
+	default:
+		// We don't support this platform yet, so just return with what we have
+	}
+	return b.build()
+}
+
+// resourceBuilder simplifies constructing resources using GCP detection
+// library functions.
+type resourceBuilder struct {
+	errs  []error
+	attrs []attribute.KeyValue
+}
+
+func (r *resourceBuilder) add(key attribute.Key, detect func() (string, error)) {
+	if v, err := detect(); err == nil {
+		r.attrs = append(r.attrs, key.String(v))
+	} else {
+		r.errs = append(r.errs, err)
+	}
+}
+
+// zoneAndRegion functions are expected to return zone, region, err.
+func (r *resourceBuilder) addZoneAndRegion(detect func() (string, string, error)) {
+	if zone, region, err := detect(); err == nil {
+		r.attrs = append(r.attrs, semconv.CloudAvailabilityZoneKey.String(zone))
+		r.attrs = append(r.attrs, semconv.CloudRegionKey.String(region))
+	} else {
+		r.errs = append(r.errs, err)
+	}
+}
+
+func (r *resourceBuilder) addZoneOrRegion(detect func() (string, gcp.LocationType, error)) {
+	if v, locType, err := detect(); err == nil {
+		switch locType {
+		case gcp.Zone:
+			r.attrs = append(r.attrs, semconv.CloudAvailabilityZoneKey.String(v))
+		case gcp.Region:
+			r.attrs = append(r.attrs, semconv.CloudRegionKey.String(v))
+		default:
+			r.errs = append(r.errs, fmt.Errorf("location must be zone or region. Got %v", locType))
+		}
+	} else {
+		r.errs = append(r.errs, err)
+	}
+}
+
+func (r *resourceBuilder) build() (*resource.Resource, error) {
+	var err error
+	if len(r.errs) > 0 {
+		err = fmt.Errorf("%w: %s", resource.ErrPartialResource, r.errs)
+	}
+	return resource.NewWithAttributes(semconv.SchemaURL, r.attrs...), err
+}
diff --git a/detectors/gcp/gce.go b/detectors/gcp/gce.go
index 4a2562949..ab3688c0e 100644
--- a/detectors/gcp/gce.go
+++ b/detectors/gcp/gce.go
@@ -28,6 +28,7 @@ import (
 )
 
 // GCE collects resource information of GCE computing instances.
+// Deprecated: Use gcp.NewDetector() instead, which sets the same resource attributes on GCE.
 type GCE struct{}
 
 // compile time assertion that GCE implements the resource.Detector interface.
diff --git a/detectors/gcp/gke.go b/detectors/gcp/gke.go
index 321d66148..23146ab19 100644
--- a/detectors/gcp/gke.go
+++ b/detectors/gcp/gke.go
@@ -27,6 +27,8 @@ import (
 )
 
 // GKE collects resource information of GKE computing instances.
+// Deprecated: Use gcp.NewDetector() instead, which does NOT detect container, pod, and namespace attributes.
+// Set those using name using the OTEL_RESOURCE_ATTRIBUTES env var instead.
 type GKE struct{}
 
 // compile time assertion that GKE implements the resource.Detector interface.
diff --git a/detectors/gcp/go.mod b/detectors/gcp/go.mod
index 869b23cdc..561fecd55 100644
--- a/detectors/gcp/go.mod
+++ b/detectors/gcp/go.mod
@@ -4,14 +4,19 @@ go 1.17
 
 require (
 	cloud.google.com/go/compute v1.6.1
+	github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v0.31.0
 	github.com/google/go-cmp v0.5.8
+	github.com/stretchr/testify v1.7.1
 	go.opentelemetry.io/otel v1.7.0
 	go.opentelemetry.io/otel/sdk v1.7.0
 )
 
 require (
+	github.com/davecgh/go-spew v1.1.0 // indirect
 	github.com/go-logr/logr v1.2.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
 	go.opentelemetry.io/otel/trace v1.7.0 // indirect
 	golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
+	gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
 )
diff --git a/detectors/gcp/go.sum b/detectors/gcp/go.sum
index 2214f7cf9..82f1f1834 100644
--- a/detectors/gcp/go.sum
+++ b/detectors/gcp/go.sum
@@ -54,6 +54,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v0.31.0 h1:EnFLvwFv8onlnjYFSrQ4EMSm6uWRBZHOItR/Tz0Jnqk=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v0.31.0/go.mod h1:s7Gpwj0tk7XnVCm4BQEmx/mbS36SuTCY/vMB2SNxe8o=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -173,8 +175,10 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -601,6 +605,7 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/detectors/gcp/types.go b/detectors/gcp/types.go
new file mode 100644
index 000000000..1dbe2dad4
--- /dev/null
+++ b/detectors/gcp/types.go
@@ -0,0 +1,39 @@
+// 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 "go.opentelemetry.io/contrib/detectors/gcp"
+
+import "github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp"
+
+// gcpDetector can detect attributes of GCP environments.
+type gcpDetector interface {
+	ProjectID() (string, error)
+	CloudPlatform() gcp.Platform
+	GKEAvailabilityZoneOrRegion() (string, gcp.LocationType, error)
+	GKEClusterName() (string, error)
+	GKEHostID() (string, error)
+	GKEHostName() (string, error)
+	FaaSName() (string, error)
+	FaaSVersion() (string, error)
+	FaaSID() (string, error)
+	FaaSCloudRegion() (string, error)
+	AppEngineAvailabilityZoneAndRegion() (string, string, error)
+	AppEngineServiceName() (string, error)
+	AppEngineServiceVersion() (string, error)
+	AppEngineServiceInstance() (string, error)
+	GCEAvailabilityZoneAndRegion() (string, string, error)
+	GCEHostType() (string, error)
+	GCEHostID() (string, error)
+	GCEHostName() (string, error)
+}


Test patch:
diff --git a/detectors/gcp/detector_test.go b/detectors/gcp/detector_test.go
new file mode 100644
index 000000000..2676e10e5
--- /dev/null
+++ b/detectors/gcp/detector_test.go
@@ -0,0 +1,345 @@
+// 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 "go.opentelemetry.io/contrib/detectors/gcp"
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp"
+	"github.com/stretchr/testify/assert"
+
+	"go.opentelemetry.io/otel/sdk/resource"
+	semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
+)
+
+func TestDetect(t *testing.T) {
+	// Set this before all tests to ensure metadata.onGCE() returns true
+	err := os.Setenv("GCE_METADATA_HOST", "169.254.169.254")
+	assert.NoError(t, err)
+
+	for _, tc := range []struct {
+		desc             string
+		detector         resource.Detector
+		expectErr        bool
+		expectedResource *resource.Resource
+	}{
+		{
+			desc: "zonal GKE cluster",
+			detector: &detector{detector: &fakeGCPDetector{
+				projectID:           "my-project",
+				cloudPlatform:       gcp.GKE,
+				gkeHostID:           "1472385723456792345",
+				gkeClusterName:      "my-cluster",
+				gkeAvailabilityZone: "us-central1-c",
+			}},
+			expectedResource: resource.NewWithAttributes(semconv.SchemaURL,
+				semconv.CloudProviderGCP,
+				semconv.CloudAccountIDKey.String("my-project"),
+				semconv.CloudPlatformGCPKubernetesEngine,
+				semconv.K8SClusterNameKey.String("my-cluster"),
+				semconv.CloudAvailabilityZoneKey.String("us-central1-c"),
+				semconv.HostIDKey.String("1472385723456792345"),
+			),
+		},
+		{
+			desc: "regional GKE cluster",
+			detector: &detector{detector: &fakeGCPDetector{
+				projectID:      "my-project",
+				cloudPlatform:  gcp.GKE,
+				gkeHostID:      "1472385723456792345",
+				gkeClusterName: "my-cluster",
+				gkeRegion:      "us-central1",
+			}},
+			expectedResource: resource.NewWithAttributes(semconv.SchemaURL,
+				semconv.CloudProviderGCP,
+				semconv.CloudAccountIDKey.String("my-project"),
+				semconv.CloudPlatformGCPKubernetesEngine,
+				semconv.K8SClusterNameKey.String("my-cluster"),
+				semconv.CloudRegionKey.String("us-central1"),
+				semconv.HostIDKey.String("1472385723456792345"),
+			),
+		},
+		{
+			desc: "GCE",
+			detector: &detector{detector: &fakeGCPDetector{
+				projectID:           "my-project",
+				cloudPlatform:       gcp.GCE,
+				gceHostID:           "1472385723456792345",
+				gceHostName:         "my-gke-node-1234",
+				gceHostType:         "n1-standard1",
+				gceAvailabilityZone: "us-central1-c",
+				gceRegion:           "us-central1",
+			}},
+			expectedResource: resource.NewWithAttributes(semconv.SchemaURL,
+				semconv.CloudProviderGCP,
+				semconv.CloudAccountIDKey.String("my-project"),
+				semconv.CloudPlatformGCPComputeEngine,
+				semconv.HostIDKey.String("1472385723456792345"),
+				semconv.HostNameKey.String("my-gke-node-1234"),
+				semconv.HostTypeKey.String("n1-standard1"),
+				semconv.CloudRegionKey.String("us-central1"),
+				semconv.CloudAvailabilityZoneKey.String("us-central1-c"),
+			),
+		},
+		{
+			desc: "Cloud Run",
+			detector: &detector{detector: &fakeGCPDetector{
+				projectID:       "my-project",
+				cloudPlatform:   gcp.CloudRun,
+				faaSID:          "1472385723456792345",
+				faaSCloudRegion: "us-central1",
+				faaSName:        "my-service",
+				faaSVersion:     "123456",
+			}},
+			expectedResource: resource.NewWithAttributes(semconv.SchemaURL,
+				semconv.CloudProviderGCP,
+				semconv.CloudAccountIDKey.String("my-project"),
+				semconv.CloudPlatformGCPCloudRun,
+				semconv.CloudRegionKey.String("us-central1"),
+				semconv.FaaSNameKey.String("my-service"),
+				semconv.FaaSVersionKey.String("123456"),
+				semconv.FaaSIDKey.String("1472385723456792345"),
+			),
+		},
+		{
+			desc: "Cloud Functions",
+			detector: &detector{detector: &fakeGCPDetector{
+				projectID:       "my-project",
+				cloudPlatform:   gcp.CloudFunctions,
+				faaSID:          "1472385723456792345",
+				faaSCloudRegion: "us-central1",
+				faaSName:        "my-service",
+				faaSVersion:     "123456",
+			}},
+			expectedResource: resource.NewWithAttributes(semconv.SchemaURL,
+				semconv.CloudProviderGCP,
+				semconv.CloudAccountIDKey.String("my-project"),
+				semconv.CloudPlatformGCPCloudFunctions,
+				semconv.CloudRegionKey.String("us-central1"),
+				semconv.FaaSNameKey.String("my-service"),
+				semconv.FaaSVersionKey.String("123456"),
+				semconv.FaaSIDKey.String("1472385723456792345"),
+			),
+		},
+		{
+			desc: "App Engine",
+			detector: &detector{detector: &fakeGCPDetector{
+				projectID:                 "my-project",
+				cloudPlatform:             gcp.AppEngine,
+				appEngineServiceInstance:  "1472385723456792345",
+				appEngineAvailabilityZone: "us-central1-c",
+				appEngineRegion:           "us-central1",
+				appEngineServiceName:      "my-service",
+				appEngineServiceVersion:   "123456",
+			}},
+			expectedResource: resource.NewWithAttributes(semconv.SchemaURL,
+				semconv.CloudProviderGCP,
+				semconv.CloudAccountIDKey.String("my-project"),
+				semconv.CloudPlatformGCPAppEngine,
+				semconv.CloudRegionKey.String("us-central1"),
+				semconv.CloudAvailabilityZoneKey.String("us-central1-c"),
+				semconv.FaaSNameKey.String("my-service"),
+				semconv.FaaSVersionKey.String("123456"),
+				semconv.FaaSIDKey.String("1472385723456792345"),
+			),
+		},
+		{
+			desc: "Unknown Platform",
+			detector: &detector{detector: &fakeGCPDetector{
+				projectID:     "my-project",
+				cloudPlatform: gcp.UnknownPlatform,
+			}},
+			expectedResource: resource.NewWithAttributes(semconv.SchemaURL,
+				semconv.CloudProviderGCP,
+				semconv.CloudAccountIDKey.String("my-project"),
+			),
+		},
+		{
+			desc: "error",
+			detector: &detector{detector: &fakeGCPDetector{
+				err: fmt.Errorf("failed to get metadata"),
+			}},
+			expectErr: true,
+			expectedResource: resource.NewWithAttributes(semconv.SchemaURL,
+				semconv.CloudProviderGCP,
+			),
+		},
+	} {
+		t.Run(tc.desc, func(t *testing.T) {
+			res, err := tc.detector.Detect(context.TODO())
+			if tc.expectErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+			assert.Equal(t, tc.expectedResource, res, "Resource object returned is incorrect")
+		})
+	}
+}
+
+// fakeGCPDetector implements gcpDetector and uses fake values.
+type fakeGCPDetector struct {
+	err                       error
+	projectID                 string
+	cloudPlatform             gcp.Platform
+	gkeAvailabilityZone       string
+	gkeRegion                 string
+	gkeClusterName            string
+	gkeHostID                 string
+	gkeHostName               string
+	faaSName                  string
+	faaSVersion               string
+	faaSID                    string
+	faaSCloudRegion           string
+	appEngineAvailabilityZone string
+	appEngineRegion           string
+	appEngineServiceName      string
+	appEngineServiceVersion   string
+	appEngineServiceInstance  string
+	gceAvailabilityZone       string
+	gceRegion                 string
+	gceHostType               string
+	gceHostID                 string
+	gceHostName               string
+}
+
+func (f *fakeGCPDetector) ProjectID() (string, error) {
+	if f.err != nil {
+		return "", f.err
+	}
+	return f.projectID, nil
+}
+
+func (f *fakeGCPDetector) CloudPlatform() gcp.Platform {
+	return f.cloudPlatform
+}
+
+func (f *fakeGCPDetector) GKEAvailabilityZoneOrRegion() (string, gcp.LocationType, error) {
+	if f.err != nil {
+		return "", gcp.UndefinedLocation, f.err
+	}
+	if f.gkeAvailabilityZone != "" {
+		return f.gkeAvailabilityZone, gcp.Zone, nil
+	}
+	return f.gkeRegion, gcp.Region, nil
+}
+
+func (f *fakeGCPDetector) GKEClusterName() (string, error) {
+	if f.err != nil {
+		return "", f.err
+	}
+	return f.gkeClusterName, nil
+}
+
+func (f *fakeGCPDetector) GKEHostID() (string, error) {
+	if f.err != nil {
+		return "", f.err
+	}
+	return f.gkeHostID, nil
+}
+
+func (f *fakeGCPDetector) GKEHostName() (string, error) {
+	if f.err != nil {
+		return "", f.err
+	}
+	return f.gkeHostName, nil
+}
+
+func (f *fakeGCPDetector) FaaSName() (string, error) {
+	if f.err != nil {
+		return "", f.err
+	}
+	return f.faaSName, nil
+}
+
+func (f *fakeGCPDetector) FaaSVersion() (string, error) {
+	if f.err != nil {
+		return "", f.err
+	}
+	return f.faaSVersion, nil
+}
+
+func (f *fakeGCPDetector) FaaSID() (string, error) {
+	if f.err != nil {
+		return "", f.err
+	}
+	return f.faaSID, nil
+}
+
+func (f *fakeGCPDetector) FaaSCloudRegion() (string, error) {
+	if f.err != nil {
+		return "", f.err
+	}
+	return f.faaSCloudRegion, nil
+}
+
+func (f *fakeGCPDetector) AppEngineAvailabilityZoneAndRegion() (string, string, error) {
+	if f.err != nil {
+		return "", "", f.err
+	}
+	return f.appEngineAvailabilityZone, f.appEngineRegion, nil
+}
+
+func (f *fakeGCPDetector) AppEngineServiceName() (string, error) {
+	if f.err != nil {
+		return "", f.err
+	}
+	return f.appEngineServiceName, nil
+}
+
+func (f *fakeGCPDetector) AppEngineServiceVersion() (string, error) {
+	if f.err != nil {
+		return "", f.err
+	}
+	return f.appEngineServiceVersion, nil
+}
+
+func (f *fakeGCPDetector) AppEngineServiceInstance() (string, error) {
+	if f.err != nil {
+		return "", f.err
+	}
+	return f.appEngineServiceInstance, nil
+}
+
+func (f *fakeGCPDetector) GCEAvailabilityZoneAndRegion() (string, string, error) {
+	if f.err != nil {
+		return "", "", f.err
+	}
+	return f.gceAvailabilityZone, f.gceRegion, nil
+}
+
+func (f *fakeGCPDetector) GCEHostType() (string, error) {
+	if f.err != nil {
+		return "", f.err
+	}
+	return f.gceHostType, nil
+}
+
+func (f *fakeGCPDetector) GCEHostID() (string, error) {
+	if f.err != nil {
+		return "", f.err
+	}
+	return f.gceHostID, nil
+}
+
+func (f *fakeGCPDetector) GCEHostName() (string, error) {
+	if f.err != nil {
+		return "", f.err
+	}
+	return f.gceHostName, nil
+}