event/otel: adapter for OpenTelemetry tracing

The parent ID of the event is unused; the context is used to convey
information instead.

Change-Id: I03da0a6636182b3532d6f8139cfa2ee51106c3b0
Reviewed-on: https://go-review.googlesource.com/c/exp/+/320090
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/event/otel/trace.go b/event/otel/trace.go
new file mode 100644
index 0000000..bc75376
--- /dev/null
+++ b/event/otel/trace.go
@@ -0,0 +1,53 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package otel
+
+import (
+	"context"
+
+	"go.opentelemetry.io/otel/trace"
+	"golang.org/x/exp/event"
+)
+
+type TraceHandler struct {
+	tracer trace.Tracer
+}
+
+var _ event.TraceHandler = (*TraceHandler)(nil)
+
+func NewTraceHandler(t trace.Tracer) *TraceHandler {
+	return &TraceHandler{tracer: t}
+}
+
+type spanKey struct{}
+
+func (t *TraceHandler) Start(ctx context.Context, e *event.Event) context.Context {
+	opts := labelsToSpanOptions(e.Labels)
+	octx, span := t.tracer.Start(ctx, e.Message, opts...)
+	return context.WithValue(octx, spanKey{}, span)
+}
+
+func (t *TraceHandler) End(ctx context.Context, e *event.Event) {
+	span, ok := ctx.Value(spanKey{}).(trace.Span)
+	if !ok {
+		panic("End called on context with no span")
+	}
+	span.End()
+}
+
+func labelsToSpanOptions(ls []event.Label) []trace.SpanOption {
+	var opts []trace.SpanOption
+	for _, l := range ls {
+		switch l.Name {
+		case "link":
+			opts = append(opts, trace.WithLinks(l.Value.Interface().(trace.Link)))
+		case "newRoot":
+			opts = append(opts, trace.WithNewRoot())
+		case "spanKind":
+			opts = append(opts, trace.WithSpanKind(l.Value.Interface().(trace.SpanKind)))
+		}
+	}
+	return opts
+}
diff --git a/event/otel/trace_test.go b/event/otel/trace_test.go
new file mode 100644
index 0000000..65310de
--- /dev/null
+++ b/event/otel/trace_test.go
@@ -0,0 +1,157 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package otel
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"testing"
+
+	sdktrace "go.opentelemetry.io/otel/sdk/trace"
+	"go.opentelemetry.io/otel/trace"
+	"golang.org/x/exp/event"
+)
+
+func TestTrace(t *testing.T) {
+	// Verify that otel and event traces work well together.
+	// This test uses a single, fixed span tree (see makeTraceSpec).
+	// Each test case varies which of the individual spans are
+	// created directly from an otel tracer, and which are created
+	// using the event package.
+
+	want := "root (f (g h) p (q r))"
+
+	for i, tfunc := range []func(int) bool{
+		func(int) bool { return true },
+		func(int) bool { return false },
+		func(i int) bool { return i%2 == 0 },
+		func(i int) bool { return i%2 == 1 },
+		func(i int) bool { return i%3 == 0 },
+		func(i int) bool { return i%3 == 1 },
+	} {
+		ctx, tr, shutdown := setupOtel()
+		// There are 7 spans, so we create a 7-element slice.
+		// tfunc determines, for each index, whether it holds
+		// an otel tracer or nil.
+		tracers := make([]trace.Tracer, 7)
+		for i := 0; i < len(tracers); i++ {
+			if tfunc(i) {
+				tracers[i] = tr
+			}
+		}
+		s := makeTraceSpec(tracers)
+		s.apply(ctx)
+		got := shutdown()
+		if got != want {
+			t.Errorf("#%d: got %v, want %v", i, got, want)
+		}
+	}
+}
+
+func makeTraceSpec(tracers []trace.Tracer) *traceSpec {
+	return &traceSpec{
+		name:   "root",
+		tracer: tracers[0],
+		children: []*traceSpec{
+			{
+				name:   "f",
+				tracer: tracers[1],
+				children: []*traceSpec{
+					{name: "g", tracer: tracers[2]},
+					{name: "h", tracer: tracers[3]},
+				},
+			},
+			{
+				name:   "p",
+				tracer: tracers[4],
+				children: []*traceSpec{
+					{name: "q", tracer: tracers[5]},
+					{name: "r", tracer: tracers[6]},
+				},
+			},
+		},
+	}
+}
+
+type traceSpec struct {
+	name     string
+	tracer   trace.Tracer // nil for event
+	children []*traceSpec
+}
+
+// apply builds spans for the traceSpec and all its children,
+// If the traceSpec has a non-nil tracer, it is used to create the span.
+// Otherwise, event.Trace.Start is used.
+func (s *traceSpec) apply(ctx context.Context) {
+	if s.tracer != nil {
+		var span trace.Span
+		ctx, span = s.tracer.Start(ctx, s.name)
+		defer span.End()
+	} else {
+		var end func()
+		ctx, end = event.To(ctx).Start(s.name)
+		defer end()
+	}
+	for _, c := range s.children {
+		c.apply(ctx)
+	}
+}
+
+func setupOtel() (context.Context, trace.Tracer, func() string) {
+	ctx := context.Background()
+	e := newTestExporter()
+	bsp := sdktrace.NewSimpleSpanProcessor(e)
+	stp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(bsp))
+	tracer := stp.Tracer("")
+
+	ee := event.NewExporter(NewTraceHandler(tracer))
+	ctx = event.WithExporter(ctx, ee)
+	return ctx, tracer, func() string { stp.Shutdown(ctx); return e.got }
+}
+
+// testExporter is an otel exporter for traces
+type testExporter struct {
+	m   map[trace.SpanID][]*sdktrace.SpanSnapshot // key is parent SpanID
+	got string
+}
+
+var _ sdktrace.SpanExporter = (*testExporter)(nil)
+
+func newTestExporter() *testExporter {
+	return &testExporter{m: map[trace.SpanID][]*sdktrace.SpanSnapshot{}}
+}
+
+func (e *testExporter) ExportSpans(ctx context.Context, ss []*sdktrace.SpanSnapshot) error {
+	for _, s := range ss {
+		sid := s.Parent.SpanID()
+		e.m[sid] = append(e.m[sid], s)
+	}
+	return nil
+}
+
+func (e *testExporter) Shutdown(ctx context.Context) error {
+	root := e.m[trace.SpanID{}][0]
+	var buf bytes.Buffer
+	e.print(&buf, root)
+	e.got = buf.String()
+	return nil
+}
+
+func (e *testExporter) print(w io.Writer, ss *sdktrace.SpanSnapshot) {
+	fmt.Fprintf(w, "%s", ss.Name)
+	children := e.m[ss.SpanContext.SpanID()]
+	if len(children) > 0 {
+		fmt.Fprint(w, " (")
+		for i, ss := range children {
+			if i != 0 {
+				fmt.Fprint(w, " ")
+			}
+			e.print(w, ss)
+		}
+		fmt.Fprint(w, ")")
+	}
+}
diff --git a/go.mod b/go.mod
index 79ff9d2..e32b3f4 100644
--- a/go.mod
+++ b/go.mod
@@ -12,6 +12,10 @@
 	github.com/google/go-cmp v0.5.5
 	github.com/rs/zerolog v1.21.0
 	github.com/sirupsen/logrus v1.8.1
+	go.opentelemetry.io/otel v0.20.0 // indirect
+	go.opentelemetry.io/otel/metric v0.20.0 // indirect
+	go.opentelemetry.io/otel/sdk v0.20.0
+	go.opentelemetry.io/otel/trace v0.20.0
 	go.uber.org/atomic v1.7.0 // indirect
 	go.uber.org/multierr v1.6.0 // indirect
 	go.uber.org/zap v1.16.0
diff --git a/go.sum b/go.sum
index cdfe453..5d70f5f 100644
--- a/go.sum
+++ b/go.sum
@@ -243,8 +243,9 @@
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
@@ -255,6 +256,16 @@
 go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
 go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opentelemetry.io/otel v0.20.0 h1:eaP0Fqu7SXHwvjiqDq83zImeehOHX8doTvU9AwXON8g=
+go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
+go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8=
+go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=
+go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw=
+go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=
+go.opentelemetry.io/otel/sdk v0.20.0 h1:JsxtGXd06J8jrnya7fdI/U/MR6yXA5DtbZy+qoHQlr8=
+go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc=
+go.opentelemetry.io/otel/trace v0.20.0 h1:1DL6EXUdcg95gukhuRRvLDO/4X5THh/5dIV52lqtnbw=
+go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
@@ -403,6 +414,8 @@
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=