Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add detector for Cloud Run service. #455

Merged
merged 18 commits into from
Dec 10, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- The AWS detector now adds the cloud zone, host image ID, host type, and host name to the returned `Resource`. (#410)
- Add Amazon ECS Resource Detector for AWS X-Ray. (#466)
- Add propagator for AWS X-Ray (#462)

- A new `gcp.CloudRun` detector for detecting resource from a Cloud Run instance. (#455)
yegle marked this conversation as resolved.
Show resolved Hide resolved

### Changed

- Add semantic version to `Tracer` / `Meter` created by instrumentation packages `otelsaram`, `otelrestful`, `otelmongo`, `otelhttp` and `otelhttptrace`. (#412)
Expand Down
124 changes: 124 additions & 0 deletions detectors/gcp/cloud-run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// 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"
"fmt"
"os"

"cloud.google.com/go/compute/metadata"

"go.opentelemetry.io/otel/label"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/semconv"
)

const serviceNamespace = "cloud-run-managed"

// The minimal list of metadata.Client methods we use. Use an interface so we
// can replace it with a fake implementation in the unit test.
type metadataClient interface {
ProjectID() (string, error)
Get(string) (string, error)
InstanceID() (string, error)
}

// CloudRun collects resource information of Cloud Run instance.
type CloudRun struct {
mc metadataClient
onGCE func() bool
getenv func(string) string
}

// compile time assertion that CloudRun implements the resource.Detector
// interface.
var _ resource.Detector = (*CloudRun)(nil)

// NewCloudRun creates a CloudRun detector.
func NewCloudRun() *CloudRun {
yegle marked this conversation as resolved.
Show resolved Hide resolved
return &CloudRun{
mc: metadata.NewClient(nil),
onGCE: metadata.OnGCE,
getenv: os.Getenv,
}
}

// for test only
func (c *CloudRun) setupForTest(mc metadataClient, ongce func() bool, getenv func(string) string) {
yegle marked this conversation as resolved.
Show resolved Hide resolved
c.mc = mc
c.onGCE = ongce
c.getenv = getenv
}

// Detect detects associated resources when running on Cloud Run hosts.
// NOTE: the service.namespace label is currently hardcoded to be
// "cloud-run-managed". This may change in the future, please do not rely on
// this behavior yet.
func (c *CloudRun) Detect(ctx context.Context) (*resource.Resource, error) {
// .OnGCE is actually testing whether the metadata server is available.
// Metadata server is supported on Cloud Run.
if !c.onGCE() {
return nil, nil
}

labels := []label.KeyValue{
semconv.CloudProviderGCP,
}

var errInfo []string

if projectID, err := c.mc.ProjectID(); hasProblem(err) {
errInfo = append(errInfo, err.Error())
} else if projectID != "" {
labels = append(labels, semconv.CloudAccountIDKey.String(projectID))
}

if region, err := c.mc.Get("instance/region"); hasProblem(err) {
errInfo = append(errInfo, err.Error())
} else if region != "" {
labels = append(labels, semconv.CloudRegionKey.String(region))
}

if instanceID, err := c.mc.InstanceID(); hasProblem(err) {
errInfo = append(errInfo, err.Error())
} else if instanceID != "" {
labels = append(labels, semconv.ServiceInstanceIDKey.String(instanceID))
}

// Part of Cloud Run container runtime contract.
// See https://cloud.google.com/run/docs/reference/container-contract
// The same K_SERVICE value ultimately maps to both `namespace` and
// `job` label of `generic_task` metric type.
if service := c.getenv("K_SERVICE"); service == "" {
errInfo = append(errInfo, "envvar K_SERVICE contains empty string.")
} else {
labels = append(labels,
semconv.ServiceNamespaceKey.String(serviceNamespace),
semconv.ServiceNameKey.String(service),
)
}
resource, err := resource.New(ctx, resource.WithAttributes(labels...))
if err != nil {
errInfo = append(errInfo, err.Error())
}

var aggregatedErr error
if len(errInfo) > 0 {
aggregatedErr = fmt.Errorf("detecting Cloud Run resources: %s", errInfo)
}

return resource, aggregatedErr
}
170 changes: 170 additions & 0 deletions detectors/gcp/cloud-run_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// 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"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"

"go.opentelemetry.io/otel/label"
"go.opentelemetry.io/otel/sdk/resource"
)

var (
notOnGCE = func() bool { return false }
onGCE = func() bool { return true }
)

func getenv(m map[string]string) func(string) string {
return func(s string) string {
if m == nil {
return ""
}
return m[s]
}
}

type client struct {
m map[string]string
}

func (c *client) Get(s string) (string, error) {
got, ok := c.m[s]
if !ok {
return "", fmt.Errorf("%q do not exist", s)
} else if got == "" {
return "", fmt.Errorf("%q is empty", s)
}
return got, nil
}

func (c *client) InstanceID() (string, error) {
return c.Get("instance/id")
}

func (c *client) ProjectID() (string, error) {
return c.Get("project/project-id")
}

var _ metadataClient = (*client)(nil)

func TestCloudRunDetectorNotOnGCE(t *testing.T) {
ctx := context.Background()
c := NewCloudRun()
c.setupForTest(nil, notOnGCE, getenv(nil))

if res, err := c.Detect(ctx); res != nil || err != nil {
t.Errorf("Expect c.Detect(ctx) to return (nil, nil), got (%v, %v)", res, err)
}
}

func TestCloudRunDetectorExpectSuccess(t *testing.T) {
ctx := context.Background()

metadata := map[string]string{
"project/project-id": "foo",
"instance/id": "bar",
"instance/region": "utopia",
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
}
envvars := map[string]string{
"K_SERVICE": "x-service",
}
want, err := resource.New(
ctx,
resource.WithAttributes(
label.String("cloud.account.id", "foo"),
label.String("cloud.provider", "gcp"),
label.String("cloud.region", "utopia"),
label.String("service.instance.id", "bar"),
label.String("service.name", "x-service"),
label.String("service.namespace", "cloud-run-managed"),
),
)
if err != nil {
t.Fatalf("failed to create a resource: %v", err)
}
c := NewCloudRun()
c.setupForTest(&client{m: metadata}, onGCE, getenv(envvars))

if res, err := c.Detect(ctx); err != nil {
t.Fatalf("got unexpected failure: %v", err)
} else if diff := cmp.Diff(want, res); diff != "" {
t.Errorf("detected resource differ from expected (-want, +got)\n%s", diff)
}
}

func TestCloudRunDetectorExpectFail(t *testing.T) {
ctx := context.Background()

tests := []struct {
name string
metadata map[string]string
envvars map[string]string
}{
{
name: "Missing ProjectID",
metadata: map[string]string{
"instance/id": "bar",
"instance/region": "utopia",
},
envvars: map[string]string{
"K_SERVICE": "x-service",
},
},
{
name: "Missing InstanceID",
metadata: map[string]string{
"project/project-id": "foo",
"instance/region": "utopia",
},
envvars: map[string]string{
"K_SERVICE": "x-service",
},
},
{
name: "Missing Region",
metadata: map[string]string{
"project/project-id": "foo",
"instance/id": "bar",
},
envvars: map[string]string{
"K_SERVICE": "x-service",
},
},
{
name: "Missing K_SERVICE envvar",
metadata: map[string]string{
"project/project-id": "foo",
"instance/id": "bar",
"instance/region": "utopia",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := NewCloudRun()
c.setupForTest(&client{m: test.metadata}, onGCE, getenv(test.envvars))

if res, err := c.Detect(ctx); err == nil {
t.Errorf("Expect c.Detect(ctx) to return error, got nil (resource: %v)", res)
} else {
t.Logf("err: %v", err)
}
})
}
}
1 change: 1 addition & 0 deletions detectors/gcp/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.14

require (
cloud.google.com/go v0.72.0
github.com/google/go-cmp v0.5.3
go.opentelemetry.io/otel v0.14.0
go.opentelemetry.io/otel/sdk v0.14.0
)