// Copyright 2018 Google LLC // // 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 trace import ( "context" "errors" "net/http" "sort" "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/attribute" otcodes "go.opentelemetry.io/otel/codes" sdktrace "go.opentelemetry.io/otel/sdk/trace" "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 := IsOpenTelemetryTracingEnabled() SetOpenTelemetryTracingEnabledField(false) te := testutil.NewTestExporter() t.Cleanup(func() { SetOpenTelemetryTracingEnabledField(old) te.Unregister() }) ctx := context.Background() ctx = StartSpan(ctx, "test-span") TracePrintf(ctx, annotationData(), "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 := IsOpenTelemetryTracingEnabled() SetOpenTelemetryTracingEnabledField(true) ctx := context.Background() te := testutil.NewOpenTelemetryTestExporter() t.Cleanup(func() { SetOpenTelemetryTracingEnabledField(old) te.Unregister(ctx) }) ctx = StartSpan(ctx, "test-span") TracePrintf(ctx, annotationData(), "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 := 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 := 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 := []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"), } got := spans[0].Events[0].Attributes // Sorting is required since the TracePrintf parameter is a map. sort.Slice(got, func(i, j int) bool { return got[i].Key < got[j].Key }) if !cmp.Equal(got, want, ignoreEventFields, ignoreValueFields) { t.Errorf("got %v, want %v", got, want) } wantEvent := 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], wantEvent, ignoreEventFields, ignoreValueFields) { t.Errorf("got %v, want %v", spans[0].Events[1], want) } } func TestToStatus(t *testing.T) { for _, testcase := range []struct { input error want octrace.Status }{ { errors.New("some random error"), octrace.Status{Code: int32(code.Code_UNKNOWN), Message: "some random error"}, }, { &googleapi.Error{Code: http.StatusConflict, Message: "some specific googleapi http error"}, octrace.Status{Code: int32(code.Code_ALREADY_EXISTS), Message: "some specific googleapi http error"}, }, { status.Error(codes.DataLoss, "some specific grpc error"), octrace.Status{Code: int32(code.Code_DATA_LOSS), Message: "some specific grpc error"}, }, } { got := toStatus(testcase.input) if r := testutil.Diff(got, testcase.want); r != "" { t.Errorf("got -, want +:\n%s", r) } } } 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", }, } { // Wrap supported types in apierror.APIError as GAPIC clients // do, but fall back to the unwrapped error if not supported. // https://github.com/googleapis/gax-go/blob/v2.12.0/v2/invoke.go#L95 var err error err, ok := apierror.FromError(testcase.input) if !ok { err = testcase.input } got := toOpenTelemetryStatusDescription(err) 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 annotationData() 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 }