blob: 2e03d2a89df08c1c00079dfaaf43161acd3b17f6 [file] [log] [blame]
// 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
}