diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bfd3779ff74..099d9b30675a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - A `TextMapPropagator` and associated `TextMapCarrier` are added to the `go.opentelemetry.io/otel/oteltest` package to test TextMap type propagators and their use. (#1259) - `SpanContextFromContext` returns `SpanContext` from context. (#1255) - Add an opencensus to opentelemetry tracing bridge. (#1305) +- Add an opencensus binary propagation implementation. (#1334) ### Changed diff --git a/bridge/opencensus/binary/binary.go b/bridge/opencensus/binary/binary.go new file mode 100644 index 000000000000..819b88dd4b38 --- /dev/null +++ b/bridge/opencensus/binary/binary.go @@ -0,0 +1,86 @@ +// Copyright The OpenTelemetry Authors +// +// 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 +// +// http://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 binary // import "go.opentelemetry.io/otel/bridge/opencensus/binary" + +import ( + "context" + + ocpropagation "go.opencensus.io/trace/propagation" + + "go.opentelemetry.io/otel/bridge/opencensus/utils" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +type key uint + +const binaryKey key = 0 + +// traceContextKey is the same as opencensus: +// https://github.com/census-instrumentation/opencensus-go/blob/3fb168f674736c026e623310bfccb0691e6dec8a/plugin/ocgrpc/trace_common.go#L30 +const binaryHeader = "grpc-trace-bin" + +// Binary is an OpenTelemetry implementation of the OpenCensus grpc binary format. +// Binary propagation was temporarily removed from opentelemetry. See +// https://github.com/open-telemetry/opentelemetry-specification/issues/437 +type Binary struct{} + +var _ propagation.TextMapPropagator = Binary{} + +// Inject injects context into the TextMapCarrier +func (b Binary) Inject(ctx context.Context, carrier propagation.TextMapCarrier) { + binaryContext := ctx.Value(binaryKey) + if state, ok := binaryContext.(string); binaryContext != nil && ok { + carrier.Set(binaryHeader, state) + } + + sc := trace.SpanContextFromContext(ctx) + if !sc.IsValid() { + return + } + h := ocpropagation.Binary(utils.OTelSpanContextToOc(sc)) + carrier.Set(binaryHeader, string(h)) +} + +// Extract extracts the SpanContext from the TextMapCarrier +func (b Binary) Extract(ctx context.Context, carrier propagation.TextMapCarrier) context.Context { + state := carrier.Get(binaryHeader) + if state != "" { + ctx = context.WithValue(ctx, binaryKey, state) + } + + sc := b.extract(carrier) + if !sc.IsValid() { + return ctx + } + return trace.ContextWithRemoteSpanContext(ctx, sc) +} + +func (b Binary) extract(carrier propagation.TextMapCarrier) trace.SpanContext { + h := carrier.Get(binaryHeader) + if h == "" { + return trace.SpanContext{} + } + ocContext, ok := ocpropagation.FromBinary([]byte(h)) + if !ok { + return trace.SpanContext{} + } + return utils.OCSpanContextToOTel(ocContext) +} + +// Fields returns the fields that this propagator modifies. +func (b Binary) Fields() []string { + return []string{binaryHeader} +} diff --git a/bridge/opencensus/binary/binary_test.go b/bridge/opencensus/binary/binary_test.go new file mode 100644 index 000000000000..f2a9460ff054 --- /dev/null +++ b/bridge/opencensus/binary/binary_test.go @@ -0,0 +1,138 @@ +// Copyright The OpenTelemetry Authors +// +// 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 +// +// http://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 binary + +import ( + "context" + "fmt" + "net/http" + "testing" + + "go.opentelemetry.io/otel/oteltest" + "go.opentelemetry.io/otel/trace" +) + +var ( + traceID = trace.TraceID([16]byte{14, 54, 12}) + spanID = trace.SpanID([8]byte{2, 8, 14, 20}) + childSpanID = trace.SpanID([8]byte{0, 0, 0, 0, 0, 0, 0, 2}) + headerFmt = "\x00\x00\x0e6\f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00%s\x02%s" +) + +func TestFields(t *testing.T) { + b := Binary{} + fields := b.Fields() + if len(fields) != 1 { + t.Fatalf("Got %d fields, expected 1", len(fields)) + } + if fields[0] != "grpc-trace-bin" { + t.Errorf("Got fields[0] == %s, expected grpc-trace-bin", fields[0]) + } +} + +func TestInject(t *testing.T) { + mockTracer := oteltest.DefaultTracer() + prop := Binary{} + for _, tt := range []struct { + desc string + sc trace.SpanContext + wantHeader string + }{ + { + desc: "valid spancontext, sampled", + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + wantHeader: fmt.Sprintf(headerFmt, "\x02", "\x01"), + }, + { + desc: "valid spancontext, not sampled", + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + }, + wantHeader: fmt.Sprintf(headerFmt, "\x03", "\x00"), + }, + { + desc: "valid spancontext, with unsupported bit set in traceflags", + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: 0xff, + }, + wantHeader: fmt.Sprintf(headerFmt, "\x04", "\x01"), + }, + { + desc: "invalid spancontext", + sc: trace.SpanContext{}, + wantHeader: "", + }, + } { + t.Run(tt.desc, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + ctx := context.Background() + if tt.sc.IsValid() { + ctx = trace.ContextWithRemoteSpanContext(ctx, tt.sc) + ctx, _ = mockTracer.Start(ctx, "inject") + } + prop.Inject(ctx, req.Header) + + gotHeader := req.Header.Get("grpc-trace-bin") + if gotHeader != tt.wantHeader { + t.Errorf("Got header = %q, want %q", gotHeader, tt.wantHeader) + } + }) + } +} +func TestExtract(t *testing.T) { + prop := Binary{} + for _, tt := range []struct { + desc string + header string + wantSc trace.SpanContext + }{ + { + desc: "valid binary header", + header: fmt.Sprintf(headerFmt, "\x02", "\x00"), + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: childSpanID, + }, + }, + { + desc: "valid binary and sampled", + header: fmt.Sprintf(headerFmt, "\x02", "\x01"), + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: childSpanID, + TraceFlags: trace.FlagsSampled, + }, + }, + } { + t.Run(tt.desc, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("grpc-trace-bin", tt.header) + + ctx := context.Background() + ctx = prop.Extract(ctx, req.Header) + gotSc := trace.RemoteSpanContextFromContext(ctx) + if gotSc != tt.wantSc { + t.Errorf("Got SpanContext: %+v, wanted %+v", gotSc, tt.wantSc) + } + }) + } +} diff --git a/bridge/opencensus/bridge.go b/bridge/opencensus/bridge.go index 3c4e2840b25f..385265db88b3 100644 --- a/bridge/opencensus/bridge.go +++ b/bridge/opencensus/bridge.go @@ -20,6 +20,7 @@ import ( octrace "go.opencensus.io/trace" + "go.opentelemetry.io/otel/bridge/opencensus/utils" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/global" "go.opentelemetry.io/otel/label" @@ -68,7 +69,7 @@ func convertStartOptions(optFns []octrace.StartOption, name string) []trace.Span func (o *otelTracer) StartSpanWithRemoteParent(ctx context.Context, name string, parent octrace.SpanContext, s ...octrace.StartOption) (context.Context, *octrace.Span) { // make sure span context is zero'd out so we use the remote parent ctx = trace.ContextWithSpan(ctx, nil) - ctx = trace.ContextWithRemoteSpanContext(ctx, ocSpanContextToOTel(parent)) + ctx = trace.ContextWithRemoteSpanContext(ctx, utils.OCSpanContextToOTel(parent)) return o.StartSpan(ctx, name, s...) } @@ -98,7 +99,7 @@ func (s *span) End() { } func (s *span) SpanContext() octrace.SpanContext { - return otelSpanContextToOc(s.otSpan.SpanContext()) + return utils.OTelSpanContextToOc(s.otSpan.SpanContext()) } func (s *span) SetName(name string) { @@ -187,31 +188,3 @@ func (s *span) AddLink(l octrace.Link) { func (s *span) String() string { return fmt.Sprintf("span %s", s.otSpan.SpanContext().SpanID.String()) } - -func otelSpanContextToOc(sc trace.SpanContext) octrace.SpanContext { - if sc.IsDebug() || sc.IsDeferred() { - global.Handle(fmt.Errorf("ignoring OpenTelemetry Debug or Deferred trace flags for span %q because they are not supported by OpenCensus", sc.SpanID)) - } - var to octrace.TraceOptions - if sc.IsSampled() { - // OpenCensus doesn't expose functions to directly set sampled - to = 0x1 - } - return octrace.SpanContext{ - TraceID: octrace.TraceID(sc.TraceID), - SpanID: octrace.SpanID(sc.SpanID), - TraceOptions: to, - } -} - -func ocSpanContextToOTel(sc octrace.SpanContext) trace.SpanContext { - var traceFlags byte - if sc.IsSampled() { - traceFlags = trace.FlagsSampled - } - return trace.SpanContext{ - TraceID: trace.TraceID(sc.TraceID), - SpanID: trace.SpanID(sc.SpanID), - TraceFlags: traceFlags, - } -} diff --git a/bridge/opencensus/bridge_test.go b/bridge/opencensus/bridge_test.go index a2cdb5d562c8..e7394620f280 100644 --- a/bridge/opencensus/bridge_test.go +++ b/bridge/opencensus/bridge_test.go @@ -20,6 +20,7 @@ import ( octrace "go.opencensus.io/trace" + "go.opentelemetry.io/otel/bridge/opencensus/utils" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/label" "go.opentelemetry.io/otel/oteltest" @@ -101,7 +102,7 @@ func TestStartSpanWithRemoteParent(t *testing.T) { ctx := context.Background() ctx, parent := tracer.Start(ctx, "OpenTelemetrySpan1") - _, span := octrace.StartSpanWithRemoteParent(ctx, "OpenCensusSpan", otelSpanContextToOc(parent.SpanContext())) + _, span := octrace.StartSpanWithRemoteParent(ctx, "OpenCensusSpan", utils.OTelSpanContextToOc(parent.SpanContext())) span.End() spans := sr.Completed() diff --git a/bridge/opencensus/doc.go b/bridge/opencensus/doc.go new file mode 100644 index 000000000000..057241520d82 --- /dev/null +++ b/bridge/opencensus/doc.go @@ -0,0 +1,15 @@ +// Copyright The OpenTelemetry Authors +// +// 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 +// +// http://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 opencensus // import "go.opentelemetry.io/otel/bridge/opencensus" diff --git a/bridge/opencensus/go.mod b/bridge/opencensus/go.mod index a99609584148..f16ef4d0160b 100644 --- a/bridge/opencensus/go.mod +++ b/bridge/opencensus/go.mod @@ -1,6 +1,6 @@ -module go.opentelemetry.io/opentelemetry-go/bridge/opencensus +module go.opentelemetry.io/otel/bridge/opencensus -go 1.15 +go 1.14 require ( go.opencensus.io v0.22.6-0.20201102222123-380f4078db9f diff --git a/bridge/opencensus/utils/utils.go b/bridge/opencensus/utils/utils.go new file mode 100644 index 000000000000..6d4a79313f8f --- /dev/null +++ b/bridge/opencensus/utils/utils.go @@ -0,0 +1,57 @@ +// Copyright The OpenTelemetry Authors +// +// 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 +// +// http://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 utils // import "go.opentelemetry.io/otel/bridge/opencensus/utils" + +import ( + "fmt" + + octrace "go.opencensus.io/trace" + + "go.opentelemetry.io/otel/global" + "go.opentelemetry.io/otel/trace" +) + +// OTelSpanContextToOc converts from an OpenTelemetry SpanContext to an +// OpenCensus SpanContext, and handles any incompatibilities with the global +// error handler. +func OTelSpanContextToOc(sc trace.SpanContext) octrace.SpanContext { + if sc.IsDebug() || sc.IsDeferred() { + global.Handle(fmt.Errorf("ignoring OpenTelemetry Debug or Deferred trace flags for span %q because they are not supported by OpenCensus", sc.SpanID)) + } + var to octrace.TraceOptions + if sc.IsSampled() { + // OpenCensus doesn't expose functions to directly set sampled + to = 0x1 + } + return octrace.SpanContext{ + TraceID: octrace.TraceID(sc.TraceID), + SpanID: octrace.SpanID(sc.SpanID), + TraceOptions: to, + } +} + +// OCSpanContextToOTel converts from an OpenCensus SpanContext to an +// OpenTelemetry SpanContext. +func OCSpanContextToOTel(sc octrace.SpanContext) trace.SpanContext { + var traceFlags byte + if sc.IsSampled() { + traceFlags = trace.FlagsSampled + } + return trace.SpanContext{ + TraceID: trace.TraceID(sc.TraceID), + SpanID: trace.SpanID(sc.SpanID), + TraceFlags: traceFlags, + } +} diff --git a/bridge/opencensus/utils/utils_test.go b/bridge/opencensus/utils/utils_test.go new file mode 100644 index 000000000000..5c3866a4e514 --- /dev/null +++ b/bridge/opencensus/utils/utils_test.go @@ -0,0 +1,138 @@ +// Copyright The OpenTelemetry Authors +// +// 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 +// +// http://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 utils + +import ( + "testing" + + "go.opencensus.io/trace/tracestate" + + octrace "go.opencensus.io/trace" + + "go.opentelemetry.io/otel/trace" +) + +func TestOTelSpanContextToOc(t *testing.T) { + for _, tc := range []struct { + description string + input trace.SpanContext + expected octrace.SpanContext + }{ + { + description: "empty", + }, + { + description: "sampled", + input: trace.SpanContext{ + TraceID: trace.TraceID([16]byte{1}), + SpanID: trace.SpanID([8]byte{2}), + TraceFlags: trace.FlagsSampled, + }, + expected: octrace.SpanContext{ + TraceID: octrace.TraceID([16]byte{1}), + SpanID: octrace.SpanID([8]byte{2}), + TraceOptions: octrace.TraceOptions(0x1), + }, + }, + { + description: "not sampled", + input: trace.SpanContext{ + TraceID: trace.TraceID([16]byte{1}), + SpanID: trace.SpanID([8]byte{2}), + }, + expected: octrace.SpanContext{ + TraceID: octrace.TraceID([16]byte{1}), + SpanID: octrace.SpanID([8]byte{2}), + TraceOptions: octrace.TraceOptions(0), + }, + }, + { + description: "debug flag", + input: trace.SpanContext{ + TraceID: trace.TraceID([16]byte{1}), + SpanID: trace.SpanID([8]byte{2}), + TraceFlags: trace.FlagsDebug, + }, + expected: octrace.SpanContext{ + TraceID: octrace.TraceID([16]byte{1}), + SpanID: octrace.SpanID([8]byte{2}), + TraceOptions: octrace.TraceOptions(0), + }, + }, + } { + t.Run(tc.description, func(t *testing.T) { + output := OTelSpanContextToOc(tc.input) + if output != tc.expected { + t.Fatalf("Got %+v spancontext, exepected %+v.", output, tc.expected) + } + }) + } +} + +func TestOCSpanContextToOTel(t *testing.T) { + for _, tc := range []struct { + description string + input octrace.SpanContext + expected trace.SpanContext + }{ + { + description: "empty", + }, + { + description: "sampled", + input: octrace.SpanContext{ + TraceID: octrace.TraceID([16]byte{1}), + SpanID: octrace.SpanID([8]byte{2}), + TraceOptions: octrace.TraceOptions(0x1), + }, + expected: trace.SpanContext{ + TraceID: trace.TraceID([16]byte{1}), + SpanID: trace.SpanID([8]byte{2}), + TraceFlags: trace.FlagsSampled, + }, + }, + { + description: "not sampled", + input: octrace.SpanContext{ + TraceID: octrace.TraceID([16]byte{1}), + SpanID: octrace.SpanID([8]byte{2}), + TraceOptions: octrace.TraceOptions(0), + }, + expected: trace.SpanContext{ + TraceID: trace.TraceID([16]byte{1}), + SpanID: trace.SpanID([8]byte{2}), + }, + }, + { + description: "trace state is ignored", + input: octrace.SpanContext{ + TraceID: octrace.TraceID([16]byte{1}), + SpanID: octrace.SpanID([8]byte{2}), + Tracestate: &tracestate.Tracestate{}, + }, + expected: trace.SpanContext{ + TraceID: trace.TraceID([16]byte{1}), + SpanID: trace.SpanID([8]byte{2}), + }, + }, + } { + t.Run(tc.description, func(t *testing.T) { + output := OCSpanContextToOTel(tc.input) + if output != tc.expected { + t.Fatalf("Got %+v spancontext, exepected %+v.", output, tc.expected) + } + }) + } +}