Skip to content

Commit

Permalink
feat(internal/trace): add OpenTelemetry support
Browse files Browse the repository at this point in the history
Add GOOGLE_API_GO_EXPERIMENTAL_TELEMETRY_PLATFORM_TRACING env var flag
for opt-in OpenTelemetry tracing.

refs: #2205
  • Loading branch information
quartzmo committed Oct 23, 2023
1 parent f8ba0b9 commit 1b7d79b
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 5 deletions.
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ require (
github.com/google/martian/v3 v3.3.2
github.com/googleapis/gax-go/v2 v2.12.0
go.opencensus.io v0.24.0
go.opentelemetry.io/otel v1.19.0
go.opentelemetry.io/otel/trace v1.19.0
golang.org/x/oauth2 v0.8.0
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2
google.golang.org/api v0.128.0
Expand All @@ -23,11 +25,14 @@ require (
cloud.google.com/go/compute v1.19.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v0.13.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/s2a-go v0.1.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.4 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
Expand Down
15 changes: 15 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
Expand All @@ -29,6 +30,11 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
Expand Down Expand Up @@ -74,6 +80,7 @@ github.com/googleapis/enterprise-certificate-proxy v0.2.4/go.mod h1:AwSRAtLfXpU5
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
Expand All @@ -85,9 +92,16 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE=
go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8=
go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg=
go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
Expand Down Expand Up @@ -205,6 +219,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
40 changes: 35 additions & 5 deletions internal/trace/trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,57 @@ package trace
import (
"context"
"fmt"
"os"
"strings"

"go.opencensus.io/trace"
"go.opentelemetry.io/otel"
ottrace "go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"google.golang.org/api/googleapi"
"google.golang.org/genproto/googleapis/rpc/code"
"google.golang.org/grpc/status"
)

const (
telemetryPlatformTracing = "GOOGLE_API_GO_EXPERIMENTAL_TELEMETRY_PLATFORM_TRACING"
telemetryPlatformTracingOpenCensus = "opencensus"
telemetryPlatformTracingOpenTelemetry = "opentelemetry"
)

func IsOpenCensusTracingEnabled() bool {
env := strings.TrimSpace(os.Getenv(telemetryPlatformTracing))
return env == "" || strings.ToLower(env) == telemetryPlatformTracingOpenCensus
}

func IsOpenTelemetryTracingEnabled() bool {
env := strings.TrimSpace(os.Getenv(telemetryPlatformTracing))
return strings.ToLower(env) == telemetryPlatformTracingOpenTelemetry
}

// StartSpan adds a span to the trace with the given name.
func StartSpan(ctx context.Context, name string) context.Context {
ctx, _ = trace.StartSpan(ctx, name)
if IsOpenTelemetryTracingEnabled() {
ctx, _ = otel.GetTracerProvider().Tracer("cloud.google.com/go/internal/trace").Start(ctx, name)
} else {
ctx, _ = trace.StartSpan(ctx, name)
}
return ctx
}

// EndSpan ends a span with the given error.
func EndSpan(ctx context.Context, err error) {
span := trace.FromContext(ctx)
if err != nil {
span.SetStatus(toStatus(err))
if IsOpenTelemetryTracingEnabled() {
span := ottrace.SpanFromContext(ctx)
// TODO(chrisdsmith): Handle errors
span.End()
} else {
span := trace.FromContext(ctx)
if err != nil {
span.SetStatus(toStatus(err))
}
span.End()
}
span.End()
}

// toStatus interrogates an error and converts it to an appropriate
Expand Down
179 changes: 179 additions & 0 deletions internal/trace/trace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,125 @@
package trace

import (
"context"
"errors"
"net/http"
"os"
"testing"

"cloud.google.com/go/internal/testutil"
"github.com/googleapis/gax-go/v2/apierror"
octrace "go.opencensus.io/trace"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"google.golang.org/api/googleapi"
"google.golang.org/genproto/googleapis/rpc/code"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

func TestStartSpan_OpenCensus(t *testing.T) {
old := os.Getenv(telemetryPlatformTracing)
defer os.Setenv(telemetryPlatformTracing, old)
os.Setenv(telemetryPlatformTracing, "any value")
te := testutil.NewTestExporter()
defer te.Unregister()

ctx := context.Background()
ctx = StartSpan(ctx, "test")

attrMap := attrMap()
TracePrintf(ctx, attrMap, "Add my annotations")

err := &googleapi.Error{Code: http.StatusBadRequest, Message: "INVALID ARGUMENT"}
EndSpan(ctx, err)

if !IsOpenCensusTracingEnabled() {
t.Errorf("got false, want true")
}
if IsOpenTelemetryTracingEnabled() {
t.Errorf("got true, want false")
}
spans := te.Spans
if len(spans) != 1 {
t.Fatalf("got %d, want 1", len(spans))
}
if want := int32(3); spans[0].Status.Code != want {
t.Errorf("got %v, want %v", spans[0].Status.Message, want)
}
if want := "INVALID ARGUMENT"; spans[0].Status.Message != want {
t.Errorf("got %v, want %v", spans[0].Status.Message, want)
}
if len(spans[0].Annotations) != 1 {
t.Fatalf("got %d, want 1", len(spans[0].Annotations))
}
attrs := spans[0].Annotations[0].Attributes
if got, want := attrs["my_string"], "my string"; got != want {
t.Errorf("got %s, want %s", got, want)
}
if got, want := attrs["my_bool"], true; got != want {
t.Errorf("got %s, want %v", got, want)
}
if got, want := attrs["my_int"], int64(123); got != want {
t.Errorf("got %s, want %d", got, want)
}
if got, want := attrs["my_int64"], int64(456); got != want {
t.Errorf("got %s, want %d", got, want)
}
if got, want := attrs["my_float"], "0.9"; got != want {
t.Errorf("got %s, want %v", got, want)
}
}

func attrMap() map[string]interface{} {
attrMap := make(map[string]interface{})
attrMap["my_string"] = "my string"
attrMap["my_bool"] = true
attrMap["my_int"] = 123
attrMap["my_int64"] = int64(456)
attrMap["my_float"] = 0.9
return attrMap
}

func TestStartSpan_OpenTelemetry(t *testing.T) {
old := os.Getenv(telemetryPlatformTracing)
defer os.Setenv(telemetryPlatformTracing, old)
os.Setenv(telemetryPlatformTracing, "opentelemetry")

exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(
sdktrace.WithSyncer(exporter),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
)
defer tp.Shutdown(context.Background())
otel.SetTracerProvider(tp)

ctx := context.Background()
ctx = StartSpan(ctx, "test")
var err error // nil error
EndSpan(ctx, err)

if IsOpenCensusTracingEnabled() {
t.Errorf("got true, want false")
}
if !IsOpenTelemetryTracingEnabled() {
t.Errorf("got false, want true")
}
if len(exporter.GetSpans()) == 0 {
t.Fatalf("Wanted OpenTelemetry span to be created, got 0")
}
}

func TestTracePrintf_OpenCensus(t *testing.T) {
old := os.Getenv(telemetryPlatformTracing)
defer os.Setenv(telemetryPlatformTracing, old)
os.Setenv(telemetryPlatformTracing, "")
te := testutil.NewTestExporter()
defer te.Unregister()

}

func TestToStatus(t *testing.T) {
for _, testcase := range []struct {
input error
Expand All @@ -51,3 +158,75 @@ func TestToStatus(t *testing.T) {
}
}
}

func TestToStatus_APIError(t *testing.T) {
for _, testcase := range []struct {
input error
want octrace.Status
}{
{
// Apparently nonsensical error, but this is supported by the implementation.
&googleapi.Error{Code: 200, Message: "OK"},
octrace.Status{Code: int32(code.Code_OK), Message: "OK"},
},
{
&googleapi.Error{Code: 499, Message: "error 499"},
octrace.Status{Code: int32(code.Code_CANCELLED), Message: "error 499"},
},
{
&googleapi.Error{Code: http.StatusInternalServerError, Message: "error 500"},
octrace.Status{Code: int32(code.Code_UNKNOWN), Message: "error 500"},
},
{
&googleapi.Error{Code: http.StatusBadRequest, Message: "error 400"},
octrace.Status{Code: int32(code.Code_INVALID_ARGUMENT), Message: "error 400"},
},
{
&googleapi.Error{Code: http.StatusGatewayTimeout, Message: "error 504"},
octrace.Status{Code: int32(code.Code_DEADLINE_EXCEEDED), Message: "error 504"},
},
{
&googleapi.Error{Code: http.StatusNotFound, Message: "error 404"},
octrace.Status{Code: int32(code.Code_NOT_FOUND), Message: "error 404"},
},
{
&googleapi.Error{Code: http.StatusConflict, Message: "error 409"},
octrace.Status{Code: int32(code.Code_ALREADY_EXISTS), Message: "error 409"},
},
{
&googleapi.Error{Code: http.StatusForbidden, Message: "error 403"},
octrace.Status{Code: int32(code.Code_PERMISSION_DENIED), Message: "error 403"},
},
{
&googleapi.Error{Code: http.StatusUnauthorized, Message: "error 401"},
octrace.Status{Code: int32(code.Code_UNAUTHENTICATED), Message: "error 401"},
},
{
&googleapi.Error{Code: http.StatusTooManyRequests, Message: "error 429"},
octrace.Status{Code: int32(code.Code_RESOURCE_EXHAUSTED), Message: "error 429"},
},
{
&googleapi.Error{Code: http.StatusNotImplemented, Message: "error 501"},
octrace.Status{Code: int32(code.Code_UNIMPLEMENTED), Message: "error 501"},
},
{
&googleapi.Error{Code: http.StatusServiceUnavailable, Message: "error 503"},
octrace.Status{Code: int32(code.Code_UNAVAILABLE), Message: "error 503"},
},
{
&googleapi.Error{Code: http.StatusMovedPermanently, Message: "error 301"},
octrace.Status{Code: int32(code.Code_UNKNOWN), Message: "error 301"},
},
} {
// Wrap googleapi.Error in apierror.APIError as GAPIC clients do.
// https://github.com/googleapis/gax-go/blob/v2.12.0/v2/invoke.go#L95
err, ok := apierror.FromError(testcase.input)
if !ok {
t.Fatalf("apierror.FromError failed to parse %v", testcase.input)
}
got := toStatus(err)
if r := testutil.Diff(got, testcase.want); r != "" {
t.Errorf("got -, want +:\n%s", r)
}
}
}

0 comments on commit 1b7d79b

Please sign in to comment.