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=