Repository: GoogleCloudPlatform/opentelemetry-operations-go
Pull request #48 (2020-06-17 19:25:24)

Allow custom display name format for spans (#45)
This allows users to, when creating the exporter, use a custom DisplayNameFormatter function to specify how to create the display name from span data. Default format remains the same. Addressing issue #45 

ymotongpoo: @stevencl1013 Can you run `make precommit`? It's failing as the following:

```
=== RUN   TestExporter_Timeout
    TestExporter_Timeout: cloudtrace_test.go:157: err.Error() = "rpc error: code = Unknown desc = context deadline exceeded"; want "rpc error: code = DeadlineExceeded desc = context deadline exceeded"
--- FAIL: TestExporter_Timeout (0.00s)
FAIL
FAIL	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace	0.029s
FAIL
make: *** [Makefile:85: test-386] Error 1
```
stevencl1013: @ymotongpoo When I ran `make precommit` on my machine, all the tests passed. I think there might be an issue with the timeout test, because it did get a `context deadline exceeded` error, but failed because it got `rpc error: code = Unknown` instead of `rpc error: code = DeadlineExceeded`. I've experienced this issue occasionally on my machine as well in the past. 

Patch:
diff --git a/exporter/trace/cloudtrace.go b/exporter/trace/cloudtrace.go
index 5a7fc52..39affa4 100644
--- a/exporter/trace/cloudtrace.go
+++ b/exporter/trace/cloudtrace.go
@@ -32,6 +32,10 @@ import (
 // Option is function type that is passed to the exporter initialization function.
 type Option func(*options)
 
+// DisplayNameFormatter is is a function that produces the display name of a span
+// given its SpanData
+type DisplayNameFormatter func(*export.SpanData) string
+
 // options contains options for configuring the exporter.
 type options struct {
 	// ProjectID is the identifier of the Stackdriver
@@ -114,6 +118,11 @@ type options struct {
 	// If it is set to zero then default value is used.
 	ReportingInterval time.Duration
 
+	// DisplayNameFormatter is a function that produces the display name of a span
+	// given its SpanData.
+	// Optional. Default format for SpanData s is "Span.{s.SpanKind}-{s.Name}"
+	DisplayNameFormatter
+
 	// MaxNumberOfWorkers sets the maximum number of go rountines that send requests
 	// to Cloud Trace. The minimum number of workers is 1.
 	MaxNumberOfWorkers int
@@ -193,6 +202,14 @@ func WithTimeout(t time.Duration) func(o *options) {
 	}
 }
 
+// WithDisplayNameFormatter sets the way span's display names will be 
+// generated from SpanData
+func WithDisplayNameFormatter(f DisplayNameFormatter) func(o *options) {
+	return func(o *options) {
+		o.DisplayNameFormatter = f
+	}
+}
+
 func (o *options) handleError(err error) {
 	if o.OnError != nil {
 		o.OnError(err)
diff --git a/exporter/trace/trace.go b/exporter/trace/trace.go
index 96271aa..300b238 100644
--- a/exporter/trace/trace.go
+++ b/exporter/trace/trace.go
@@ -110,7 +110,7 @@ func (e *traceExporter) checkBundlerError(err error) {
 
 // ExportSpan exports a SpanData to Stackdriver Trace.
 func (e *traceExporter) ExportSpan(ctx context.Context, sd *export.SpanData) {
-	protoSpan := protoFromSpanData(sd, e.projectID)
+	protoSpan := protoFromSpanData(sd, e.projectID, e.o.DisplayNameFormatter)
 	protoSize := proto.Size(protoSpan)
 	err := e.bundler.Add(&contextAndSpans{
 		ctx: ctx, 
@@ -124,7 +124,7 @@ func (e *traceExporter) ExportSpans(ctx context.Context, sds []*export.SpanData)
 	pbSpans := make([]*tracepb.Span, len(sds))
 	var protoSize int = 0
 	for i, sd := range sds {
-		pbSpans[i] = protoFromSpanData(sd, e.projectID)
+		pbSpans[i] = protoFromSpanData(sd, e.projectID, e.o.DisplayNameFormatter)
 		protoSize += proto.Size(pbSpans[i])
 	}
 	err := e.bundler.Add(&contextAndSpans{ctx, pbSpans}, protoSize)
diff --git a/exporter/trace/trace_proto.go b/exporter/trace/trace_proto.go
index d0bc9f2..13fe5ca 100644
--- a/exporter/trace/trace_proto.go
+++ b/exporter/trace/trace_proto.go
@@ -60,7 +60,18 @@ const (
 
 var userAgent = fmt.Sprintf("opentelemetry-go %s; cloudtrace-exporter %s", opentelemetry.Version(), version)
 
-func protoFromSpanData(s *export.SpanData, projectID string) *tracepb.Span {
+func generateDisplayName(s *export.SpanData, format DisplayNameFormatter) string {
+	if format != nil {
+		return format(s)
+	}
+	switch s.SpanKind {
+	// TODO(ymotongpoo): add cases for "Send" and "Recv".
+	default:
+		return fmt.Sprintf("Span.%s-%s", s.SpanKind, s.Name)
+	}
+}
+
+func protoFromSpanData(s *export.SpanData, projectID string, format DisplayNameFormatter) *tracepb.Span {
 	if s == nil {
 		return nil
 	}
@@ -68,17 +79,12 @@ func protoFromSpanData(s *export.SpanData, projectID string) *tracepb.Span {
 	traceIDString := s.SpanContext.TraceID.String()
 	spanIDString := s.SpanContext.SpanID.String()
 
-	name := s.Name
-	switch s.SpanKind {
-	// TODO(ymotongpoo): add cases for "Send" and "Recv".
-	default:
-		name = fmt.Sprintf("Span.%s-%s", s.SpanKind, name)
-	}
+	displayName := generateDisplayName(s, format)
 
 	sp := &tracepb.Span{
 		Name:                    "projects/" + projectID + "/traces/" + traceIDString + "/spans/" + spanIDString,
 		SpanId:                  spanIDString,
-		DisplayName:             trunc(name, 128),
+		DisplayName:             trunc(displayName, 128),
 		StartTime:               timestampProto(s.StartTime),
 		EndTime:                 timestampProto(s.EndTime),
 		SameProcessAsParentSpan: &wrapperspb.BoolValue{Value: !s.HasRemoteParent},


Test patch:
diff --git a/exporter/trace/cloudtrace_test.go b/exporter/trace/cloudtrace_test.go
index 6f32500..a07b64a 100644
--- a/exporter/trace/cloudtrace_test.go
+++ b/exporter/trace/cloudtrace_test.go
@@ -33,6 +33,7 @@ import (
 
 	"go.opentelemetry.io/otel/api/global"
 	sdktrace "go.opentelemetry.io/otel/sdk/trace"
+	export "go.opentelemetry.io/otel/sdk/export/trace"
 )
 
 type mockTraceServer struct {
@@ -129,6 +130,41 @@ func TestExporter_ExportSpans(t *testing.T) {
 	assert.EqualValues(t, 1, mockTrace.len())
 }
 
+func TestExporter_DisplayNameFormatter(t *testing.T) {
+	// Initial test precondition
+	mockTrace.spansUploaded = nil
+	mockTrace.delay = 0
+	
+	spanName := "span1234"
+	format := func(s *export.SpanData) string {
+		return "TEST_FORMAT" + s.Name
+	}
+
+	// Create Google Cloud Trace Exporter
+	exp, err := texporter.NewExporter(
+		texporter.WithProjectID("PROJECT_ID_NOT_REAL"),
+		texporter.WithTraceClientOptions(clientOpt),
+		texporter.WithBundleCountThreshold(1),
+		texporter.WithDisplayNameFormatter(format),
+	)
+	assert.NoError(t, err)
+
+	tp, err := sdktrace.NewProvider(
+		sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}),
+		sdktrace.WithSyncer(exp))
+	assert.NoError(t, err)
+
+	global.SetTraceProvider(tp)
+	_, span := global.TraceProvider().Tracer("test-tracer").Start(context.Background(), spanName)
+	span.End()
+	assert.True(t, span.SpanContext().IsValid())
+
+	// wait exporter to flush
+	time.Sleep(20 * time.Millisecond)
+	assert.EqualValues(t, 1, mockTrace.len())
+	assert.EqualValues(t, "TEST_FORMAT" + spanName, mockTrace.spansUploaded[0].DisplayName.Value)
+}
+
 func TestExporter_Timeout(t *testing.T) {
 	// Initial test precondition
 	mockTrace.spansUploaded = nil