Repository: GoogleCloudPlatform/opentelemetry-operations-go
Pull request #180 (2021-07-02 15:03:19)

Set span-kind field in trace_proto.go


Patch:
diff --git a/exporter/trace/trace_proto.go b/exporter/trace/trace_proto.go
index a9bddbf..b563888 100644
--- a/exporter/trace/trace_proto.go
+++ b/exporter/trace/trace_proto.go
@@ -26,6 +26,7 @@ import (
 	"go.opentelemetry.io/otel/attribute"
 	"go.opentelemetry.io/otel/codes"
 	sdktrace "go.opentelemetry.io/otel/sdk/trace"
+	"go.opentelemetry.io/otel/trace"
 
 	timestamppb "github.com/golang/protobuf/ptypes/timestamp"
 	wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
@@ -39,6 +40,7 @@ const (
 	// TODO(ymotongpoo): uncomment this after gRPC trace get supported.
 	// maxMessageEventsPerSpan    = 128
 	maxAttributeStringValue = 256
+	maxNumLinks             = 128
 	agentLabel              = "g.co/agent"
 
 	// Attributes recorded on the span for the requests.
@@ -110,6 +112,7 @@ func protoFromReadOnlySpan(s sdktrace.ReadOnlySpan, defaultTraceAttributes []att
 		StartTime:               timestampProto(s.StartTime()),
 		EndTime:                 timestampProto(s.EndTime()),
 		SameProcessAsParentSpan: &wrapperspb.BoolValue{Value: !s.Parent().IsRemote()},
+		SpanKind:                convertSpanKind(s.SpanKind()),
 	}
 	if s.Parent().SpanID() != s.SpanContext().SpanID() && s.Parent().SpanID().IsValid() {
 		sp.ParentSpanId = s.Parent().SpanID().String()
@@ -178,11 +181,40 @@ func protoFromReadOnlySpan(s sdktrace.ReadOnlySpan, defaultTraceAttributes []att
 		sp.TimeEvents.DroppedAnnotationsCount = clip32(droppedAnnotationsCount)
 	}
 
-	// TODO(ymotongpoo): add implementations for Links
+	sp.Links = linksProtoFromLinks(s.Links())
 
 	return sp
 }
 
+// Converts OTel span links to Cloud Trace links proto in order. If there are
+// more than maxNumLinks links, the first maxNumLinks will be taken and the rest
+// dropped.
+func linksProtoFromLinks(links []trace.Link) *tracepb.Span_Links {
+	numLinks := len(links)
+	if numLinks == 0 {
+		return nil
+	}
+
+	linksPb := &tracepb.Span_Links{}
+	numLinksToKeep := numLinks
+	if numLinksToKeep > maxNumLinks {
+		numLinksToKeep = maxNumLinks
+	}
+
+	for _, link := range links[:numLinksToKeep] {
+		linkPb := &tracepb.Span_Link{
+			TraceId: link.SpanContext.TraceID().String(),
+			SpanId:  link.SpanContext.SpanID().String(),
+			Type:    tracepb.Span_Link_TYPE_UNSPECIFIED,
+		}
+		copyAttributes(&linkPb.Attributes, link.Attributes)
+		linksPb.Link = append(linksPb.Link, linkPb)
+	}
+	linksPb.DroppedLinksCount = clip32(numLinks - numLinksToKeep)
+
+	return linksPb
+}
+
 // timestampProto creates a timestamp proto for a time.Time.
 func timestampProto(t time.Time) *timestamppb.Timestamp {
 	return &timestamppb.Timestamp{
@@ -292,3 +324,23 @@ func clip32(x int) int32 {
 	}
 	return int32(x)
 }
+
+func convertSpanKind(kind trace.SpanKind) tracepb.Span_SpanKind {
+	switch kind {
+	case trace.SpanKindUnspecified, trace.SpanKindInternal:
+		// SpanKindUnspecified is an unspecified SpanKind and is not a
+		// valid SpanKind. SpanKindUnspecified should be replaced with
+		// SpanKindInternal if it is received.
+		return tracepb.Span_INTERNAL
+	case trace.SpanKindServer:
+		return tracepb.Span_SERVER
+	case trace.SpanKindClient:
+		return tracepb.Span_CLIENT
+	case trace.SpanKindProducer:
+		return tracepb.Span_PRODUCER
+	case trace.SpanKindConsumer:
+		return tracepb.Span_CONSUMER
+	default:
+		return tracepb.Span_INTERNAL
+	}
+}


Test patch:
diff --git a/exporter/trace/cloudtrace_test.go b/exporter/trace/cloudtrace_test.go
index 01c8163..017b44b 100644
--- a/exporter/trace/cloudtrace_test.go
+++ b/exporter/trace/cloudtrace_test.go
@@ -25,6 +25,7 @@ import (
 	"go.opentelemetry.io/otel"
 	"go.opentelemetry.io/otel/codes"
 	sdktrace "go.opentelemetry.io/otel/sdk/trace"
+	"go.opentelemetry.io/otel/trace"
 
 	"github.com/googleinterns/cloud-operations-api-mock/cloudmock"
 	"github.com/stretchr/testify/assert"
@@ -60,7 +61,11 @@ func TestExporter_ExportSpan(t *testing.T) {
 	span.End()
 	assert.True(t, span.SpanContext().IsValid())
 
-	_, span = otel.Tracer("test-tracer").Start(context.Background(), "test-span-with-error-status")
+	_, span = otel.Tracer("test-tracer").Start(
+		context.Background(),
+		"test-span-with-error-status",
+		trace.WithLinks(trace.Link{SpanContext: genSpanContext()}),
+	)
 	span.SetStatus(codes.Error, "Error Message")
 	span.End()
 
@@ -73,6 +78,8 @@ func TestExporter_ExportSpan(t *testing.T) {
 	assert.EqualValues(t, codepb.Code_UNKNOWN, mock.GetSpan(1).GetStatus().Code)
 	assert.EqualValues(t, "Error Message", mock.GetSpan(1).GetStatus().Message)
 	assert.EqualValues(t, "TEST_VALUE", mock.GetSpan(0).Attributes.AttributeMap["TEST_ATTRIBUTE"].GetStringValue().Value)
+	assert.Nil(t, mock.GetSpan(0).Links)
+	assert.Len(t, mock.GetSpan(1).Links.Link, 1)
 }
 
 func TestExporter_DisplayNameNoFormatter(t *testing.T) {
diff --git a/exporter/trace/trace_proto_test.go b/exporter/trace/trace_proto_test.go
new file mode 100644
index 0000000..8273a00
--- /dev/null
+++ b/exporter/trace/trace_proto_test.go
@@ -0,0 +1,81 @@
+// Copyright 2021 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
+//
+//      https://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"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"go.opentelemetry.io/otel/attribute"
+	sdktrace "go.opentelemetry.io/otel/sdk/trace"
+	"go.opentelemetry.io/otel/trace"
+	tracepb "google.golang.org/genproto/googleapis/devtools/cloudtrace/v2"
+)
+
+func genSpanContext() trace.SpanContext {
+	tracer := sdktrace.NewTracerProvider().Tracer("")
+	_, span := tracer.Start(context.Background(), "")
+	return span.SpanContext()
+}
+
+func TestTraceProto_linksProtoFromLinks(t *testing.T) {
+	t.Run("Should be nil when no links", func(t *testing.T) {
+		assert.Nil(t, linksProtoFromLinks([]trace.Link{}))
+	})
+
+	t.Run("Can convert one link", func(t *testing.T) {
+		spanContext := genSpanContext()
+		link := trace.Link{
+			SpanContext: spanContext,
+			Attributes: []attribute.KeyValue{
+				attribute.String("hello", "world"),
+			},
+		}
+		linksPb := linksProtoFromLinks([]trace.Link{link})
+
+		assert.NotNil(t, linksPb)
+		assert.EqualValues(t, linksPb.DroppedLinksCount, 0)
+		assert.Len(t, linksPb.Link, 1)
+		assert.Equal(t, spanContext.TraceID().String(), linksPb.Link[0].TraceId)
+		assert.Equal(t, spanContext.SpanID().String(), linksPb.Link[0].SpanId)
+		assert.Equal(t, tracepb.Span_Link_TYPE_UNSPECIFIED, linksPb.Link[0].Type)
+		assert.Len(t, linksPb.Link[0].Attributes.AttributeMap, 1)
+		assert.Contains(t, linksPb.Link[0].Attributes.AttributeMap, "hello")
+		assert.Equal(
+			t,
+			"world",
+			linksPb.Link[0].Attributes.AttributeMap["hello"].GetStringValue().Value,
+		)
+	})
+
+	t.Run("Drops links when there are more than 128", func(t *testing.T) {
+		var links []trace.Link
+		for i := 0; i < 148; i++ {
+			links = append(
+				links,
+				trace.Link{
+					SpanContext: genSpanContext(),
+					Attributes: []attribute.KeyValue{
+						attribute.String("hello", "world"),
+					},
+				})
+		}
+		linksPb := linksProtoFromLinks(links)
+		assert.NotNil(t, linksPb)
+		assert.EqualValues(t, linksPb.DroppedLinksCount, 20)
+		assert.Len(t, linksPb.Link, maxNumLinks)
+	})
+}