diff --git a/go.mod b/go.mod index 0b774e3d3793..658c68e64856 100644 --- a/go.mod +++ b/go.mod @@ -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.11.0 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 google.golang.org/api v0.128.0 @@ -23,11 +25,14 @@ require ( cloud.google.com/go/compute v1.23.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/iam v1.1.1 // 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.1 // 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/sync v0.3.0 // indirect diff --git a/go.sum b/go.sum index 9010c05c9f31..fd9dc7caa146 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= @@ -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= @@ -206,6 +220,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= diff --git a/internal/trace/trace.go b/internal/trace/trace.go index c201d343e989..afe28441c7f7 100644 --- a/internal/trace/trace.go +++ b/internal/trace/trace.go @@ -16,35 +16,74 @@ package trace import ( "context" + "errors" "fmt" + "os" + "sort" + "strings" "go.opencensus.io/trace" - "golang.org/x/xerrors" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + ottrace "go.opentelemetry.io/otel/trace" "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" +) + +var ( + // TODO(chrisdsmith): What is the correct name for the OpenTelemetry tracer? + OpenTelemetryTracerName string = "cloud.google.com/go/internal/trace" +) + +func IsOpenCensusTracingEnabled() bool { + return !IsOpenTelemetryTracingEnabled() +} + +func IsOpenTelemetryTracingEnabled() bool { + env := strings.TrimSpace(os.Getenv(telemetryPlatformTracing)) + return strings.EqualFold(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(OpenTelemetryTracerName).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) + if err != nil { + span.SetStatus(codes.Error, toOpenTelemetryStatusDescription(err)) + span.RecordError(err) + } + 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 -// OpenCensus status. +// toStatus converts an error to an equivalent OpenCensus status. func toStatus(err error) trace.Status { var err2 *googleapi.Error - if ok := xerrors.As(err, &err2); ok { + if ok := errors.As(err, &err2); ok { return trace.Status{Code: httpStatusCodeToOCCode(err2.Code), Message: err2.Message} } else if s, ok := status.FromError(err); ok { return trace.Status{Code: int32(s.Code()), Message: s.Message()} @@ -53,6 +92,18 @@ func toStatus(err error) trace.Status { } } +// toOpenTelemetryStatus converts an error to an equivalent OpenTelemetry status description. +func toOpenTelemetryStatusDescription(err error) string { + var err2 *googleapi.Error + if ok := errors.As(err, &err2); ok { + return err2.Message + } else if s, ok := status.FromError(err); ok { + return s.Message() + } else { + return err.Error() + } +} + // TODO(deklerk): switch to using OpenCensus function when it becomes available. // Reference: https://github.com/googleapis/googleapis/blob/26b634d2724ac5dd30ae0b0cbfb01f07f2e4050e/google/rpc/code.proto func httpStatusCodeToOCCode(httpStatusCode int) int32 { @@ -86,10 +137,25 @@ func httpStatusCodeToOCCode(httpStatusCode int) int32 { } } +// TracePrintf retrieves the current OpenCensus or OpenTelemetry span from context, then: +// * calls Span.Annotatef if OpenCensus is enabled; or +// * calls Span.AddEvent if OpenTelemetry is enabled. +// // TODO: (odeke-em): perhaps just pass around spans due to the cost // incurred from using trace.FromContext(ctx) yet we could avoid // throwing away the work done by ctx, span := trace.StartSpan. func TracePrintf(ctx context.Context, attrMap map[string]interface{}, format string, args ...interface{}) { + if IsOpenTelemetryTracingEnabled() { + attrs := otAttrs(attrMap) + ottrace.SpanFromContext(ctx).AddEvent(fmt.Sprintf(format, args...), ottrace.WithAttributes(attrs...)) + } else { + attrs := ocAttrs(attrMap) + trace.FromContext(ctx).Annotatef(attrs, format, args...) + } +} + +// ocAttrs converts a generic map to OpenCensus attributes. +func ocAttrs(attrMap map[string]interface{}) []trace.Attribute { var attrs []trace.Attribute for k, v := range attrMap { var a trace.Attribute @@ -107,5 +173,34 @@ func TracePrintf(ctx context.Context, attrMap map[string]interface{}, format str } attrs = append(attrs, a) } - trace.FromContext(ctx).Annotatef(attrs, format, args...) + return attrs +} + +// otAttrs converts a generic map to OpenTelemetry attributes. +func otAttrs(attrMap map[string]interface{}) []attribute.KeyValue { + var attrs []attribute.KeyValue + // Sort the input map by its keys for predictable order in tests. + keys := make([]string, 0, len(attrMap)) + for k := range attrMap { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + v := attrMap[k] + var a attribute.KeyValue + switch v := v.(type) { + case string: + a = attribute.Key(k).String(v) + case bool: + a = attribute.Key(k).Bool(v) + case int: + a = attribute.Key(k).Int(v) + case int64: + a = attribute.Key(k).Int64(v) + default: + a = attribute.Key(k).String(fmt.Sprintf("%#v", v)) + } + attrs = append(attrs, a) + } + return attrs } diff --git a/internal/trace/trace_test.go b/internal/trace/trace_test.go index 2af9fb8be781..1ae43e06c26d 100644 --- a/internal/trace/trace_test.go +++ b/internal/trace/trace_test.go @@ -15,18 +15,154 @@ package trace import ( + "context" "errors" "net/http" + "os" "testing" "cloud.google.com/go/internal/testutil" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/googleapis/gax-go/v2/apierror" octrace "go.opencensus.io/trace" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + otcodes "go.opentelemetry.io/otel/codes" + 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" ) +var ( + ignoreEventFields = cmpopts.IgnoreFields(sdktrace.Event{}, "Time") + ignoreValueFields = cmpopts.IgnoreFields(attribute.Value{}, "vtype", "numeric", "stringly", "slice") +) + +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-span") + + 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 got, want := spans[0].Name, "test-span"; got != want { + t.Fatalf("got %s, want %s", got, want) + } + if want := int32(3); spans[0].Status.Code != want { + t.Errorf("got %v, want %v", spans[0].Status.Code, 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)) + } + got := spans[0].Annotations[0].Attributes + want := make(map[string]interface{}) + want["my_bool"] = true + want["my_float"] = "0.9" + want["my_int"] = int64(123) + want["my_int64"] = int64(456) + want["my_string"] = "my string" + opt := cmpopts.SortMaps(func(a, b int) bool { + return a < b + }) + if !cmp.Equal(got, want, opt) { + t.Errorf("got(-), want(+),: \n%s", cmp.Diff(got, want, opt)) + } +} + +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-span") + + TracePrintf(ctx, attrMap(), "Add my annotations") + + err := &googleapi.Error{Code: http.StatusBadRequest, Message: "INVALID ARGUMENT"} + EndSpan(ctx, err) + + if IsOpenCensusTracingEnabled() { + t.Errorf("got true, want false") + } + if !IsOpenTelemetryTracingEnabled() { + t.Errorf("got false, want true") + } + spans := exporter.GetSpans() + if len(spans) != 1 { + t.Fatalf("got %d, want 1", len(spans)) + } + if got, want := spans[0].Name, "test-span"; got != want { + t.Fatalf("got %s, want %s", got, want) + } + if want := otcodes.Error; spans[0].Status.Code != want { + t.Errorf("got %v, want %v", spans[0].Status.Code, want) + } + if want := "INVALID ARGUMENT"; spans[0].Status.Description != want { + t.Errorf("got %v, want %v", spans[0].Status.Description, want) + } + + want := sdktrace.Event{ + Name: "Add my annotations", + // KeyValues are sorted by key, since this Event was created by TracePrintf. + Attributes: []attribute.KeyValue{ + attribute.Key("my_bool").Bool(true), + attribute.Key("my_float").String("0.9"), + attribute.Key("my_int").Int(123), + attribute.Key("my_int64").Int64(int64(456)), + attribute.Key("my_string").String("my string"), + }, + } + if !cmp.Equal(spans[0].Events[0], want, ignoreEventFields, ignoreValueFields) { + t.Errorf("got %v, want %v", spans[0].Events[0], want) + } + want = sdktrace.Event{ + Name: "exception", + Attributes: []attribute.KeyValue{ + // KeyValues are NOT sorted by key, but the sort is deterministic, + // since this Event was created by Span.RecordError. + attribute.Key("exception.type").String("*googleapi.Error"), + attribute.Key("exception.message").String("googleapi: Error 400: INVALID ARGUMENT"), + }, + } + if !cmp.Equal(spans[0].Events[1], want, ignoreEventFields, ignoreValueFields) { + t.Errorf("got %v, want %v", spans[0].Events[1], want) + } +} + func TestToStatus(t *testing.T) { for _, testcase := range []struct { input error @@ -51,3 +187,110 @@ func TestToStatus(t *testing.T) { } } } + +func TestToOpenTelemetryStatusDescription(t *testing.T) { + for _, testcase := range []struct { + input error + want string + }{ + { + errors.New("some random error"), + "some random error", + }, + { + &googleapi.Error{Code: http.StatusConflict, Message: "some specific googleapi http error"}, + "some specific googleapi http error", + }, + { + status.Error(codes.DataLoss, "some specific grpc error"), + "some specific grpc error", + }, + } { + got := toOpenTelemetryStatusDescription(testcase.input) + if got != testcase.want { + t.Errorf("got %s, want %s", got, testcase.want) + } + } +} + +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) + } + } +} + +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 +}