diff --git a/exporter/trace/cloudtrace.go b/exporter/trace/cloudtrace.go index 5a7fc5294..39affa426 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/cloudtrace_test.go b/exporter/trace/cloudtrace_test.go index 6f325008e..a07b64adb 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 diff --git a/exporter/trace/trace.go b/exporter/trace/trace.go index 96271aaa5..300b2387e 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 d0bc9f228..13fe5ca77 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},