From fd60b5f9e174703969daa0c145aef668a2851f7a Mon Sep 17 00:00:00 2001 From: alrex Date: Fri, 5 Feb 2021 12:21:53 -0800 Subject: [PATCH] Add ot-tracer-* propagator (#562) * adding propagator for opentracing header format This commit adds support for the OpenTracing header format * update to 0.16.0 * implementing baggage injection, extract still needs work * comment out unused test, will re-enable in the future * update name * Update propagators/ot/ot_propagator.go Co-authored-by: Yuri Shkuro * Update propagators/ot/doc.go Co-authored-by: Yuri Shkuro * fix var name * add link to keys issue * dont inject anything if trace/span ids are not valid * Update propagators/ot/ot_propagator.go Co-authored-by: Anthony Mirabella * update changelog Co-authored-by: Yuri Shkuro Co-authored-by: Anthony Mirabella --- CHANGELOG.md | 4 + propagators/ot/doc.go | 16 ++ propagators/ot/ot_data_test.go | 250 ++++++++++++++++++++++++++ propagators/ot/ot_example_test.go | 26 +++ propagators/ot/ot_integration_test.go | 134 ++++++++++++++ propagators/ot/ot_propagator.go | 156 ++++++++++++++++ propagators/ot/ot_propagator_test.go | 146 +++++++++++++++ 7 files changed, 732 insertions(+) create mode 100644 propagators/ot/doc.go create mode 100644 propagators/ot/ot_data_test.go create mode 100644 propagators/ot/ot_example_test.go create mode 100644 propagators/ot/ot_integration_test.go create mode 100644 propagators/ot/ot_propagator.go create mode 100644 propagators/ot/ot_propagator_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a9bc608c74..1e2d620c729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Added + +- Adding `ot-tracer` propagator (#562) + ### Changed - Rename project default branch from `master` to `main`. diff --git a/propagators/ot/doc.go b/propagators/ot/doc.go new file mode 100644 index 00000000000..e3a537e78e0 --- /dev/null +++ b/propagators/ot/doc.go @@ -0,0 +1,16 @@ +// 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. + +// This package implements the ot-tracer-* propagator used by Basic Tracer implementation from the OpenTracing project +package ot // import "go.opentelemetry.io/contrib/propagators/ot" diff --git a/propagators/ot/ot_data_test.go b/propagators/ot/ot_data_test.go new file mode 100644 index 00000000000..316136e4bb0 --- /dev/null +++ b/propagators/ot/ot_data_test.go @@ -0,0 +1,250 @@ +// 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 ot_test + +import ( + "go.opentelemetry.io/otel/label" + "go.opentelemetry.io/otel/trace" +) + +const ( + traceID16Str = "a3ce929d0e0e4736" + traceID32Str = "a1ce929d0e0e4736a3ce929d0e0e4736" + spanIDStr = "00f067aa0ba902b7" + traceIDHeader = "ot-tracer-traceid" + spanIDHeader = "ot-tracer-spanid" + sampledHeader = "ot-tracer-sampled" + baggageKey = "test" + baggageValue = "value123" + baggageHeader = "ot-baggage-test" + baggageKey2 = "test2" + baggageValue2 = "value456" + baggageHeader2 = "ot-baggage-test2" +) + +var ( + traceID16 = trace.TraceID{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa3, 0xce, 0x92, 0x9d, 0x0e, 0x0e, 0x47, 0x36} + traceID32 = trace.TraceID{0xa1, 0xce, 0x92, 0x9d, 0x0e, 0x0e, 0x47, 0x36, 0xa3, 0xce, 0x92, 0x9d, 0x0e, 0x0e, 0x47, 0x36} + spanID = trace.SpanID{0x00, 0xf0, 0x67, 0xaa, 0x0b, 0xa9, 0x02, 0xb7} + emptyBaggage = &label.Set{} + // TODO: once baggage extraction is supported, re-enable this + // baggageSet = label.NewSet( + // label.String(baggageKey, baggageValue), + // label.String(baggageKey2, baggageValue2), + // ) +) + +type extractTest struct { + name string + headers map[string]string + expected trace.SpanContext + baggage *label.Set +} + +var extractHeaders = []extractTest{ + { + "empty", + map[string]string{}, + trace.SpanContext{}, + emptyBaggage, + }, + { + "sampling state not sample", + map[string]string{ + traceIDHeader: traceID32Str, + spanIDHeader: spanIDStr, + sampledHeader: "0", + }, + trace.SpanContext{ + TraceID: traceID32, + SpanID: spanID, + }, + emptyBaggage, + }, + { + "sampling state sampled", + map[string]string{ + traceIDHeader: traceID32Str, + spanIDHeader: spanIDStr, + sampledHeader: "1", + baggageHeader: baggageValue, + }, + trace.SpanContext{ + TraceID: traceID32, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + emptyBaggage, + // TODO: once baggage extraction is supported, re-enable this + // &baggageSet, + }, + { + "left padding 64 bit trace ID", + map[string]string{ + traceIDHeader: traceID16Str, + spanIDHeader: spanIDStr, + sampledHeader: "1", + }, + trace.SpanContext{ + TraceID: traceID16, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + emptyBaggage, + }, + { + "128 bit trace ID", + map[string]string{ + traceIDHeader: traceID32Str, + spanIDHeader: spanIDStr, + sampledHeader: "1", + }, + trace.SpanContext{ + TraceID: traceID32, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + emptyBaggage, + }, +} + +var invalidExtractHeaders = []extractTest{ + { + name: "trace ID length > 32", + headers: map[string]string{ + traceIDHeader: traceID32Str + "0000", + spanIDHeader: spanIDStr, + sampledHeader: "1", + }, + }, + { + name: "trace ID length is not 32 or 16", + headers: map[string]string{ + traceIDHeader: "1234567890abcd01234", + spanIDHeader: spanIDStr, + sampledHeader: "1", + }, + }, + { + name: "span ID length is not 16 or 32", + headers: map[string]string{ + traceIDHeader: traceID32Str, + spanIDHeader: spanIDStr + "0000", + sampledHeader: "1", + }, + }, + { + name: "invalid trace ID", + headers: map[string]string{ + traceIDHeader: "zcd00v0000000000a3ce929d0e0e4736", + spanIDHeader: spanIDStr, + sampledHeader: "1", + }, + }, + { + name: "invalid span ID", + headers: map[string]string{ + traceIDHeader: traceID32Str, + spanIDHeader: "00f0wiredba902b7", + sampledHeader: "1", + }, + }, + { + name: "invalid sampled", + headers: map[string]string{ + traceIDHeader: traceID32Str, + spanIDHeader: spanIDStr, + sampledHeader: "wired", + }, + }, + { + name: "missing headers", + headers: map[string]string{}, + }, + { + name: "empty header value", + headers: map[string]string{ + traceIDHeader: "", + }, + }, +} + +type injectTest struct { + name string + sc trace.SpanContext + wantHeaders map[string]string + baggage []label.KeyValue +} + +var injectHeaders = []injectTest{ + { + name: "sampled", + sc: trace.SpanContext{ + TraceID: traceID32, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + wantHeaders: map[string]string{ + traceIDHeader: traceID16Str, + spanIDHeader: spanIDStr, + sampledHeader: "1", + }, + }, + { + name: "not sampled", + sc: trace.SpanContext{ + TraceID: traceID32, + SpanID: spanID, + }, + baggage: []label.KeyValue{ + label.String(baggageKey, baggageValue), + label.String(baggageKey2, baggageValue2), + }, + wantHeaders: map[string]string{ + traceIDHeader: traceID16Str, + spanIDHeader: spanIDStr, + sampledHeader: "0", + baggageHeader: baggageValue, + baggageHeader2: baggageValue2, + }, + }, +} + +var invalidInjectHeaders = []injectTest{ + { + name: "empty", + sc: trace.SpanContext{}, + }, + { + name: "missing traceID", + sc: trace.SpanContext{ + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + }, + { + name: "missing spanID", + sc: trace.SpanContext{ + TraceID: traceID32, + TraceFlags: trace.FlagsSampled, + }, + }, + { + name: "missing both traceID and spanID", + sc: trace.SpanContext{ + TraceFlags: trace.FlagsSampled, + }, + }, +} diff --git a/propagators/ot/ot_example_test.go b/propagators/ot/ot_example_test.go new file mode 100644 index 00000000000..0d1e87680f7 --- /dev/null +++ b/propagators/ot/ot_example_test.go @@ -0,0 +1,26 @@ +// 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 ot_test + +import ( + "go.opentelemetry.io/contrib/propagators/ot" + "go.opentelemetry.io/otel" +) + +func ExampleOT() { + otPropagator := ot.OT{} + // register ot propagator + otel.SetTextMapPropagator(otPropagator) +} diff --git a/propagators/ot/ot_integration_test.go b/propagators/ot/ot_integration_test.go new file mode 100644 index 00000000000..46c7954170a --- /dev/null +++ b/propagators/ot/ot_integration_test.go @@ -0,0 +1,134 @@ +// 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 ot_test + +import ( + "context" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" + + "go.opentelemetry.io/contrib/propagators/ot" + "go.opentelemetry.io/otel/baggage" + "go.opentelemetry.io/otel/oteltest" + "go.opentelemetry.io/otel/trace" +) + +var ( + mockTracer = oteltest.NewTracerProvider().Tracer("") + _, mockSpan = mockTracer.Start(context.Background(), "") +) + +func TestExtractOT(t *testing.T) { + testGroup := []struct { + name string + testcases []extractTest + }{ + { + name: "valid test case", + testcases: extractHeaders, + }, + { + name: "invalid test case", + testcases: invalidExtractHeaders, + }, + } + + for _, tg := range testGroup { + propagator := ot.OT{} + + for _, tc := range tg.testcases { + t.Run(tc.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + for k, v := range tc.headers { + req.Header.Set(k, v) + } + + ctx := context.Background() + ctx = propagator.Extract(ctx, req.Header) + resSc := trace.RemoteSpanContextFromContext(ctx) + if diff := cmp.Diff(resSc, tc.expected, cmp.AllowUnexported(trace.TraceState{})); diff != "" { + t.Errorf("%s: %s: -got +want %s", tg.name, tc.name, diff) + } + m := baggage.Set(ctx) + mi := tc.baggage.Iter() + for mi.Next() { + label := mi.Label() + val, ok := m.Value(label.Key) + if !ok { + t.Errorf("%s: %s: expected key '%s'", tg.name, tc.name, label.Key) + } + if diff := cmp.Diff(label.Value.AsString(), val.AsString()); diff != "" { + t.Errorf("%s: %s: -got +want %s", tg.name, tc.name, diff) + } + } + }) + } + } +} + +type testSpan struct { + trace.Span + sc trace.SpanContext +} + +func (s testSpan) SpanContext() trace.SpanContext { + return s.sc +} + +func TestInjectOT(t *testing.T) { + testGroup := []struct { + name string + testcases []injectTest + }{ + { + name: "valid test case", + testcases: injectHeaders, + }, + { + name: "invalid test case", + testcases: invalidInjectHeaders, + }, + } + + for _, tg := range testGroup { + for _, tc := range tg.testcases { + propagator := ot.OT{} + t.Run(tc.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + + ctx := baggage.ContextWithValues(context.Background(), + tc.baggage..., + ) + ctx = trace.ContextWithSpan( + ctx, + testSpan{ + Span: mockSpan, + sc: tc.sc, + }, + ) + propagator.Inject(ctx, req.Header) + + for h, v := range tc.wantHeaders { + result, want := req.Header.Get(h), v + if diff := cmp.Diff(result, want); diff != "" { + t.Errorf("%s: %s, header=%s: -got +want %s", tg.name, tc.name, h, diff) + } + } + }) + } + } +} diff --git a/propagators/ot/ot_propagator.go b/propagators/ot/ot_propagator.go new file mode 100644 index 00000000000..0cc9aed30dd --- /dev/null +++ b/propagators/ot/ot_propagator.go @@ -0,0 +1,156 @@ +// 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 ot + +import ( + "context" + "errors" + "fmt" + "strings" + + "go.opentelemetry.io/otel/baggage" + + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +const ( + // Default OT Header names. + traceIDHeader = "ot-tracer-traceid" + spanIDHeader = "ot-tracer-spanid" + sampledHeader = "ot-tracer-sampled" + + otTraceIDPadding = "0000000000000000" + + traceID64BitsWidth = 64 / 4 // 16 hex character Trace ID. +) + +var ( + empty = trace.SpanContext{} + + errInvalidSampledHeader = errors.New("invalid OT Sampled header found") + errInvalidTraceIDHeader = errors.New("invalid OT traceID header found") + errInvalidSpanIDHeader = errors.New("invalid OT spanID header found") + errInvalidScope = errors.New("require either both traceID and spanID or none") +) + +// OT propagator serializes SpanContext to/from ot-trace-* headers. +type OT struct { +} + +var _ propagation.TextMapPropagator = OT{} + +// Inject injects a context into the carrier as OT headers. +// NOTE: In order to interop with systems that use the OT header format, trace ids MUST be 64-bits +func (o OT) Inject(ctx context.Context, carrier propagation.TextMapCarrier) { + sc := trace.SpanFromContext(ctx).SpanContext() + + if !sc.TraceID.IsValid() || !sc.SpanID.IsValid() { + // don't bother injecting anything if either trace or span IDs are not valid + return + } + + carrier.Set(traceIDHeader, sc.TraceID.String()[len(sc.TraceID.String())-traceID64BitsWidth:]) + carrier.Set(spanIDHeader, sc.SpanID.String()) + + if sc.IsSampled() { + carrier.Set(sampledHeader, "1") + } else { + carrier.Set(sampledHeader, "0") + } + + m := baggage.Set(ctx) + mi := m.Iter() + + for mi.Next() { + label := mi.Label() + carrier.Set(fmt.Sprintf("ot-baggage-%s", label.Key), label.Value.Emit()) + } + +} + +// Extract extracts a context from the carrier if it contains OT headers. +func (o OT) Extract(ctx context.Context, carrier propagation.TextMapCarrier) context.Context { + var ( + sc trace.SpanContext + err error + ) + + var ( + traceID = carrier.Get(traceIDHeader) + spanID = carrier.Get(spanIDHeader) + sampled = carrier.Get(sampledHeader) + ) + sc, err = extract(traceID, spanID, sampled) + if err != nil || !sc.IsValid() { + return ctx + } + // TODO: implement extracting baggage + // + // this currently is not achievable without an implementation of `keys` + // on the carrier, see: + // https://github.com/open-telemetry/opentelemetry-go/issues/1493 + return trace.ContextWithRemoteSpanContext(ctx, sc) +} + +func (o OT) Fields() []string { + return []string{traceIDHeader, spanIDHeader, sampledHeader} +} + +// extract reconstructs a SpanContext from header values based on OT +// headers. +func extract(traceID, spanID, sampled string) (trace.SpanContext, error) { + var ( + err error + requiredCount int + sc = trace.SpanContext{} + ) + + switch strings.ToLower(sampled) { + case "0", "false": + // Zero value for TraceFlags sample bit is unset. + case "1", "true": + sc.TraceFlags = trace.FlagsSampled + case "": + sc.TraceFlags = trace.FlagsDeferred + default: + return empty, errInvalidSampledHeader + } + + if traceID != "" { + requiredCount++ + id := traceID + if len(traceID) == 16 { + // Pad 64-bit trace IDs. + id = otTraceIDPadding + traceID + } + if sc.TraceID, err = trace.TraceIDFromHex(id); err != nil { + return empty, errInvalidTraceIDHeader + } + } + + if spanID != "" { + requiredCount++ + if sc.SpanID, err = trace.SpanIDFromHex(spanID); err != nil { + return empty, errInvalidSpanIDHeader + } + } + + if requiredCount != 0 && requiredCount != 2 { + return empty, errInvalidScope + } + + return sc, nil +} diff --git a/propagators/ot/ot_propagator_test.go b/propagators/ot/ot_propagator_test.go new file mode 100644 index 00000000000..4a8e5e0b1ec --- /dev/null +++ b/propagators/ot/ot_propagator_test.go @@ -0,0 +1,146 @@ +// 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 ot + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/trace" +) + +var ( + traceID = trace.TraceID{0, 0, 0, 0, 0, 0, 0, 0, 0x7b, 0, 0, 0, 0, 0, 0x1, 0xc8} + traceID128Str = "00000000000000007b000000000001c8" + zeroTraceIDStr = "00000000000000000000000000000000" + traceID64Str = "7b000000000001c8" + spanID = trace.SpanID{0, 0, 0, 0, 0, 0, 0, 0x7b} + zeroSpanIDStr = "0000000000000000" + spanIDStr = "000000000000007b" +) + +func TestOT_Extract(t *testing.T) { + testData := []struct { + traceID string + spanID string + sampled string + expected trace.SpanContext + err error + }{ + { + traceID128Str, spanIDStr, "1", + trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + nil, + }, + { + traceID64Str, spanIDStr, "1", + trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + nil, + }, + { + traceID128Str, spanIDStr, "", + trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsDeferred, + }, + nil, + }, + { + // if we didn't set sampled bit when debug bit is 1, then assuming it's not sampled + traceID128Str, spanIDStr, "0", + trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: 0x00, + }, + nil, + }, + { + traceID128Str, spanIDStr, "1", + trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + nil, + }, + { + fmt.Sprintf("%32s", "This_is_a_string_len_64"), spanIDStr, "1", + trace.SpanContext{}, + errInvalidTraceIDHeader, + }, + { + "000000000007b00000000000001c8", spanIDStr, "1", + trace.SpanContext{}, + errInvalidTraceIDHeader, + }, + { + traceID128Str, fmt.Sprintf("%16s", "wiredspanid"), "1", + trace.SpanContext{}, + errInvalidSpanIDHeader, + }, + { + traceID128Str, "0000000000010", "1", + trace.SpanContext{}, + errInvalidSpanIDHeader, + }, + { + // reject invalid traceID(0) and spanID(0) + zeroTraceIDStr, zeroSpanIDStr, "1", + trace.SpanContext{}, + errInvalidTraceIDHeader, + }, + { + // reject invalid spanID(0) + traceID128Str, zeroSpanIDStr, "1", + trace.SpanContext{}, + errInvalidSpanIDHeader, + }, + { + // reject invalid spanID(0) + traceID128Str, spanIDStr, "invalid", + trace.SpanContext{}, + errInvalidSampledHeader, + }, + } + + for _, test := range testData { + sc, err := extract(test.traceID, test.spanID, test.sampled) + + info := []interface{}{ + "trace ID: %q, span ID: %q, sampled: %q", + test.traceID, + test.spanID, + test.sampled, + } + + if !assert.Equal(t, test.err, err, info...) { + continue + } + + assert.Equal(t, test.expected, sc, info...) + } +}