diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 552147c0b65..ba5fecfafc0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -309,6 +309,15 @@ updates: day: "sunday" - package-ecosystem: "gomod" + directory: "/propagators" + labels: + - dependencies + - go + - "Skip Changelog" + schedule: + interval: "weekly" + day: "sunday" + - package-ecosystem: "gomod" directory: "/tools" labels: - dependencies diff --git a/CHANGELOG.md b/CHANGELOG.md index 64248bf6b20..8db8d1af6e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Benchmark tests for the gRPC instrumentation. (#296) - Integration testing for the gRPC instrumentation. (#297) - Allow custom labels to be added to net/http metrics. (#306) +- Added B3 propagator, moving it out of open.telemetry.io/otel repo. (#344) ### Changed diff --git a/README.md b/README.md index dbfbf956da1..e7c77c7bc57 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Collection of 3rd-party instrumentation and exporters for [OpenTelemetry-Go](htt - [Instrumentation](./instrumentation/): Packages providing OpenTelemetry instrumentation for 3rd-party libraries. - [Exporters](./exporters/): Packages providing OpenTelemetry exporters for 3rd-party telemetry systems. +- [Propagators](./propagators/): Packages providing Opentelemetry context propagators for 3rd-party propagation formats. +- [Detectors](./detectors/): Packages providing OpenTelemetry resource detectors for 3rd-party cloud computing environments. ## Contributing diff --git a/propagators/b3/b3_benchmark_test.go b/propagators/b3/b3_benchmark_test.go new file mode 100644 index 00000000000..bffb4ac4484 --- /dev/null +++ b/propagators/b3/b3_benchmark_test.go @@ -0,0 +1,103 @@ +// 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 b3_test + +import ( + "context" + "net/http" + "testing" + + "go.opentelemetry.io/contrib/propagators/b3" + "go.opentelemetry.io/otel/api/trace" +) + +func BenchmarkExtractB3(b *testing.B) { + testGroup := []struct { + name string + tests []extractTest + }{ + { + name: "valid headers", + tests: extractHeaders, + }, + { + name: "invalid headers", + tests: extractInvalidHeaders, + }, + } + + for _, tg := range testGroup { + propagator := b3.B3{} + for _, tt := range tg.tests { + traceBenchmark(tg.name+"/"+tt.name, b, func(b *testing.B) { + ctx := context.Background() + req, _ := http.NewRequest("GET", "http://example.com", nil) + for h, v := range tt.headers { + req.Header.Set(h, v) + } + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = propagator.Extract(ctx, req.Header) + } + }) + } + } +} + +func BenchmarkInjectB3(b *testing.B) { + testGroup := []struct { + name string + tests []injectTest + }{ + { + name: "valid headers", + tests: injectHeader, + }, + { + name: "invalid headers", + tests: injectInvalidHeader, + }, + } + + for _, tg := range testGroup { + for _, tt := range tg.tests { + propagator := b3.B3{InjectEncoding: tt.encoding} + traceBenchmark(tg.name+"/"+tt.name, b, func(b *testing.B) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + ctx := trace.ContextWithSpan( + context.Background(), + testSpan{sc: tt.sc}, + ) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + propagator.Inject(ctx, req.Header) + } + }) + } + } +} + +func traceBenchmark(name string, b *testing.B, fn func(*testing.B)) { + b.Run(name, func(b *testing.B) { + b.ReportAllocs() + fn(b) + }) + b.Run(name, func(b *testing.B) { + b.ReportAllocs() + fn(b) + }) +} diff --git a/propagators/b3/b3_data_test.go b/propagators/b3/b3_data_test.go new file mode 100644 index 00000000000..21a4029cecc --- /dev/null +++ b/propagators/b3/b3_data_test.go @@ -0,0 +1,1047 @@ +// 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 b3_test + +import ( + "fmt" + + "go.opentelemetry.io/contrib/propagators/b3" + "go.opentelemetry.io/otel/api/trace" +) + +const ( + b3Context = "b3" + b3Flags = "x-b3-flags" + b3TraceID = "x-b3-traceid" + b3SpanID = "x-b3-spanid" + b3Sampled = "x-b3-sampled" + b3ParentSpanID = "x-b3-parentspanid" +) + +const ( + traceIDStr = "4bf92f3577b34da6a3ce929d0e0e4736" + spanIDStr = "00f067aa0ba902b7" +) + +var ( + traceID = mustTraceIDFromHex(traceIDStr) + spanID = mustSpanIDFromHex(spanIDStr) + + traceID64bitPadded = mustTraceIDFromHex("0000000000000000a3ce929d0e0e4736") +) + +func mustTraceIDFromHex(s string) (t trace.ID) { + var err error + t, err = trace.IDFromHex(s) + if err != nil { + panic(err) + } + return +} + +func mustSpanIDFromHex(s string) (t trace.SpanID) { + var err error + t, err = trace.SpanIDFromHex(s) + if err != nil { + panic(err) + } + return +} + +type extractTest struct { + name string + headers map[string]string + wantSc trace.SpanContext +} + +var extractHeaders = []extractTest{ + { + name: "empty", + headers: map[string]string{}, + wantSc: trace.EmptySpanContext(), + }, + { + name: "multiple: sampling state defer", + headers: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsDeferred, + }, + }, + { + name: "multiple: sampling state deny", + headers: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "0", + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + }, + }, + { + name: "multiple: sampling state accept", + headers: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "1", + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + }, + { + name: "multiple: sampling state as a boolean: true", + headers: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "true", + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + }, + { + name: "multiple: sampling state as a boolean: false", + headers: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "false", + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + }, + }, + { + name: "multiple: debug flag set", + headers: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Flags: "1", + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsDeferred | trace.FlagsDebug, + }, + }, + { + name: "multiple: debug flag set to not 1 (ignored)", + headers: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "1", + b3Flags: "2", + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + }, + { + // spec explicitly states "Debug implies an accept decision, so don't + // also send the X-B3-Sampled header", make sure sampling is + // deferred. + name: "multiple: debug flag set and sampling state is deny", + headers: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "0", + b3Flags: "1", + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsDebug, + }, + }, + { + name: "multiple: with parent span id", + headers: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "1", + b3ParentSpanID: "00f067aa0ba90200", + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + }, + { + name: "multiple: with only sampled state header", + headers: map[string]string{ + b3Sampled: "0", + }, + wantSc: trace.EmptySpanContext(), + }, + { + name: "multiple: left-padding 64-bit traceID", + headers: map[string]string{ + b3TraceID: "a3ce929d0e0e4736", + b3SpanID: spanIDStr, + }, + wantSc: trace.SpanContext{ + TraceID: traceID64bitPadded, + SpanID: spanID, + TraceFlags: trace.FlagsDeferred, + }, + }, + { + name: "single: sampling state defer", + headers: map[string]string{ + b3Context: fmt.Sprintf("%s-%s", traceIDStr, spanIDStr), + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsDeferred, + }, + }, + { + name: "single: sampling state deny", + headers: map[string]string{ + b3Context: fmt.Sprintf("%s-%s-0", traceIDStr, spanIDStr), + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + }, + }, + { + name: "single: sampling state accept", + headers: map[string]string{ + b3Context: fmt.Sprintf("%s-%s-1", traceIDStr, spanIDStr), + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + }, + { + name: "single: sampling state debug", + headers: map[string]string{ + b3Context: fmt.Sprintf("%s-%s-d", traceIDStr, spanIDStr), + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsDebug, + }, + }, + { + name: "single: with parent span id", + headers: map[string]string{ + b3Context: fmt.Sprintf("%s-%s-1-00000000000000cd", traceIDStr, spanIDStr), + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + }, + { + name: "single: with only sampling state deny", + headers: map[string]string{ + b3Context: "0", + }, + wantSc: trace.EmptySpanContext(), + }, + { + name: "single: left-padding 64-bit traceID", + headers: map[string]string{ + b3Context: fmt.Sprintf("a3ce929d0e0e4736-%s", spanIDStr), + }, + wantSc: trace.SpanContext{ + TraceID: traceID64bitPadded, + SpanID: spanID, + TraceFlags: trace.FlagsDeferred, + }, + }, + { + name: "both single and multiple: single priority", + headers: map[string]string{ + b3Context: fmt.Sprintf("%s-%s-1", traceIDStr, spanIDStr), + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "0", + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + }, + // An invalid Single Headers should fallback to multiple. + { + name: "both single and multiple: invalid single", + headers: map[string]string{ + b3Context: fmt.Sprintf("%s-%s-", traceIDStr, spanIDStr), + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "0", + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + }, + }, + // Invalid Mult Header should not be noticed as Single takes precedence. + { + name: "both single and multiple: invalid multiple", + headers: map[string]string{ + b3Context: fmt.Sprintf("%s-%s-1", traceIDStr, spanIDStr), + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "invalid", + }, + wantSc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + }, +} + +var extractInvalidHeaders = []extractTest{ + { + name: "multiple: trace ID length > 32", + headers: map[string]string{ + b3TraceID: "ab00000000000000000000000000000000", + b3SpanID: "cd00000000000000", + b3Sampled: "1", + }, + }, + { + name: "multiple: trace ID length >16 and <32", + headers: map[string]string{ + b3TraceID: "ab0000000000000000000000000000", + b3SpanID: "cd00000000000000", + b3Sampled: "1", + }, + }, + { + name: "multiple: trace ID length <16", + headers: map[string]string{ + b3TraceID: "ab0000000000", + b3SpanID: "cd00000000000000", + b3Sampled: "1", + }, + }, + { + name: "multiple: wrong span ID length", + headers: map[string]string{ + b3TraceID: "ab000000000000000000000000000000", + b3SpanID: "cd0000000000000000", + b3Sampled: "1", + }, + }, + { + name: "multiple: wrong sampled flag length", + headers: map[string]string{ + b3TraceID: "ab000000000000000000000000000000", + b3SpanID: "cd00000000000000", + b3Sampled: "10", + }, + }, + { + name: "multiple: bogus trace ID", + headers: map[string]string{ + b3TraceID: "qw000000000000000000000000000000", + b3SpanID: "cd00000000000000", + b3Sampled: "1", + }, + }, + { + name: "multiple: bogus span ID", + headers: map[string]string{ + b3TraceID: "ab000000000000000000000000000000", + b3SpanID: "qw00000000000000", + b3Sampled: "1", + }, + }, + { + name: "multiple: bogus sampled flag", + headers: map[string]string{ + b3TraceID: "ab000000000000000000000000000000", + b3SpanID: "cd00000000000000", + b3Sampled: "d", + }, + }, + { + name: "multiple: upper case trace ID", + headers: map[string]string{ + b3TraceID: "AB000000000000000000000000000000", + b3SpanID: "cd00000000000000", + b3Sampled: "1", + }, + }, + { + name: "multiple: upper case span ID", + headers: map[string]string{ + b3TraceID: "ab000000000000000000000000000000", + b3SpanID: "CD00000000000000", + b3Sampled: "1", + }, + }, + { + name: "multiple: zero trace ID", + headers: map[string]string{ + b3TraceID: "00000000000000000000000000000000", + b3SpanID: "cd00000000000000", + b3Sampled: "1", + }, + }, + { + name: "multiple: zero span ID", + headers: map[string]string{ + b3TraceID: "ab000000000000000000000000000000", + b3SpanID: "0000000000000000", + b3Sampled: "1", + }, + }, + { + name: "multiple: missing span ID", + headers: map[string]string{ + b3TraceID: "ab000000000000000000000000000000", + b3Sampled: "1", + }, + }, + { + name: "multiple: missing trace ID", + headers: map[string]string{ + b3SpanID: "cd00000000000000", + b3Sampled: "1", + }, + }, + { + name: "multiple: sampled header set to 1 but trace ID and span ID are missing", + headers: map[string]string{ + b3Sampled: "1", + }, + }, + { + name: "single: wrong trace ID length", + headers: map[string]string{ + b3Context: "ab00000000000000000000000000000000-cd00000000000000-1", + }, + }, + { + name: "single: wrong span ID length", + headers: map[string]string{ + b3Context: "ab000000000000000000000000000000-cd0000000000000000-1", + }, + }, + { + name: "single: wrong sampled state length", + headers: map[string]string{ + b3Context: "00-ab000000000000000000000000000000-cd00000000000000-01", + }, + }, + { + name: "single: wrong parent span ID length", + headers: map[string]string{ + b3Context: "ab000000000000000000000000000000-cd00000000000000-1-cd0000000000000000", + }, + }, + { + name: "single: bogus trace ID", + headers: map[string]string{ + b3Context: "qw000000000000000000000000000000-cd00000000000000-1", + }, + }, + { + name: "single: bogus span ID", + headers: map[string]string{ + b3Context: "ab000000000000000000000000000000-qw00000000000000-1", + }, + }, + { + name: "single: bogus sampled flag", + headers: map[string]string{ + b3Context: "ab000000000000000000000000000000-cd00000000000000-q", + }, + }, + { + name: "single: bogus parent span ID", + headers: map[string]string{ + b3Context: "ab000000000000000000000000000000-cd00000000000000-1-qw00000000000000", + }, + }, + { + name: "single: upper case trace ID", + headers: map[string]string{ + b3Context: "AB000000000000000000000000000000-cd00000000000000-1", + }, + }, + { + name: "single: upper case span ID", + headers: map[string]string{ + b3Context: "ab000000000000000000000000000000-CD00000000000000-1", + }, + }, + { + name: "single: upper case parent span ID", + headers: map[string]string{ + b3Context: "ab000000000000000000000000000000-cd00000000000000-1-EF00000000000000", + }, + }, + { + name: "single: zero trace ID and span ID", + headers: map[string]string{ + b3Context: "00000000000000000000000000000000-0000000000000000-1", + }, + }, + { + name: "single: with sampling set to true", + headers: map[string]string{ + b3Context: "ab000000000000000000000000000000-cd00000000000000-true", + }, + }, +} + +type injectTest struct { + name string + encoding b3.Encoding + sc trace.SpanContext + wantHeaders map[string]string + doNotWantHeaders []string +} + +var injectHeader = []injectTest{ + { + name: "none: sampled", + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + wantHeaders: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "1", + }, + doNotWantHeaders: []string{ + b3ParentSpanID, + b3Flags, + b3Context, + }, + }, + { + name: "none: not sampled", + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + }, + wantHeaders: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "0", + }, + doNotWantHeaders: []string{ + b3ParentSpanID, + b3Flags, + b3Context, + }, + }, + { + name: "none: unset sampled", + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsDeferred, + }, + wantHeaders: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + }, + doNotWantHeaders: []string{ + b3Sampled, + b3ParentSpanID, + b3Flags, + b3Context, + }, + }, + { + name: "none: sampled only", + sc: trace.SpanContext{ + TraceFlags: trace.FlagsSampled, + }, + wantHeaders: map[string]string{ + b3Sampled: "1", + }, + doNotWantHeaders: []string{ + b3TraceID, + b3SpanID, + b3ParentSpanID, + b3Flags, + b3Context, + }, + }, + { + name: "none: debug", + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsDebug, + }, + wantHeaders: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Flags: "1", + }, + doNotWantHeaders: []string{ + b3Sampled, + b3ParentSpanID, + b3Context, + }, + }, + { + name: "none: debug omitting sample", + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled | trace.FlagsDebug, + }, + wantHeaders: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Flags: "1", + }, + doNotWantHeaders: []string{ + b3Sampled, + b3ParentSpanID, + b3Context, + }, + }, + { + name: "multiple: sampled", + encoding: b3.B3MultipleHeader, + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + wantHeaders: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "1", + }, + doNotWantHeaders: []string{ + b3ParentSpanID, + b3Flags, + b3Context, + }, + }, + { + name: "multiple: not sampled", + encoding: b3.B3MultipleHeader, + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + }, + wantHeaders: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "0", + }, + doNotWantHeaders: []string{ + b3ParentSpanID, + b3Flags, + b3Context, + }, + }, + { + name: "multiple: unset sampled", + encoding: b3.B3MultipleHeader, + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsDeferred, + }, + wantHeaders: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + }, + doNotWantHeaders: []string{ + b3Sampled, + b3ParentSpanID, + b3Flags, + b3Context, + }, + }, + { + name: "multiple: sampled only", + encoding: b3.B3MultipleHeader, + sc: trace.SpanContext{ + TraceFlags: trace.FlagsSampled, + }, + wantHeaders: map[string]string{ + b3Sampled: "1", + }, + doNotWantHeaders: []string{ + b3TraceID, + b3SpanID, + b3ParentSpanID, + b3Flags, + b3Context, + }, + }, + { + name: "multiple: debug", + encoding: b3.B3MultipleHeader, + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsDebug, + }, + wantHeaders: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Flags: "1", + }, + doNotWantHeaders: []string{ + b3Sampled, + b3ParentSpanID, + b3Context, + }, + }, + { + name: "multiple: debug omitting sample", + encoding: b3.B3MultipleHeader, + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled | trace.FlagsDebug, + }, + wantHeaders: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Flags: "1", + }, + doNotWantHeaders: []string{ + b3Sampled, + b3ParentSpanID, + b3Context, + }, + }, + { + name: "single: sampled", + encoding: b3.B3SingleHeader, + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + wantHeaders: map[string]string{ + b3Context: fmt.Sprintf("%s-%s-1", traceIDStr, spanIDStr), + }, + doNotWantHeaders: []string{ + b3TraceID, + b3SpanID, + b3Sampled, + b3ParentSpanID, + b3Flags, + }, + }, + { + name: "single: not sampled", + encoding: b3.B3SingleHeader, + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + }, + wantHeaders: map[string]string{ + b3Context: fmt.Sprintf("%s-%s-0", traceIDStr, spanIDStr), + }, + doNotWantHeaders: []string{ + b3TraceID, + b3SpanID, + b3Sampled, + b3ParentSpanID, + b3Flags, + }, + }, + { + name: "single: unset sampled", + encoding: b3.B3SingleHeader, + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsDeferred, + }, + wantHeaders: map[string]string{ + b3Context: fmt.Sprintf("%s-%s", traceIDStr, spanIDStr), + }, + doNotWantHeaders: []string{ + b3TraceID, + b3SpanID, + b3Sampled, + b3ParentSpanID, + b3Flags, + }, + }, + { + name: "single: sampled only", + encoding: b3.B3SingleHeader, + sc: trace.SpanContext{ + TraceFlags: trace.FlagsSampled, + }, + wantHeaders: map[string]string{ + b3Context: "1", + }, + doNotWantHeaders: []string{ + b3Sampled, + b3TraceID, + b3SpanID, + b3ParentSpanID, + b3Flags, + b3Context, + }, + }, + { + name: "single: debug", + encoding: b3.B3SingleHeader, + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsDebug, + }, + wantHeaders: map[string]string{ + b3Context: fmt.Sprintf("%s-%s-d", traceIDStr, spanIDStr), + }, + doNotWantHeaders: []string{ + b3TraceID, + b3SpanID, + b3Flags, + b3Sampled, + b3ParentSpanID, + b3Context, + }, + }, + { + name: "single: debug omitting sample", + encoding: b3.B3SingleHeader, + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled | trace.FlagsDebug, + }, + wantHeaders: map[string]string{ + b3Context: fmt.Sprintf("%s-%s-d", traceIDStr, spanIDStr), + }, + doNotWantHeaders: []string{ + b3TraceID, + b3SpanID, + b3Flags, + b3Sampled, + b3ParentSpanID, + b3Context, + }, + }, + { + name: "single+multiple: sampled", + encoding: b3.B3SingleHeader | b3.B3MultipleHeader, + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + wantHeaders: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "1", + b3Context: fmt.Sprintf("%s-%s-1", traceIDStr, spanIDStr), + }, + doNotWantHeaders: []string{ + b3ParentSpanID, + b3Flags, + }, + }, + { + name: "single+multiple: not sampled", + encoding: b3.B3SingleHeader | b3.B3MultipleHeader, + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + }, + wantHeaders: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Sampled: "0", + b3Context: fmt.Sprintf("%s-%s-0", traceIDStr, spanIDStr), + }, + doNotWantHeaders: []string{ + b3ParentSpanID, + b3Flags, + }, + }, + { + name: "single+multiple: unset sampled", + encoding: b3.B3SingleHeader | b3.B3MultipleHeader, + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsDeferred, + }, + wantHeaders: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Context: fmt.Sprintf("%s-%s", traceIDStr, spanIDStr), + }, + doNotWantHeaders: []string{ + b3Sampled, + b3ParentSpanID, + b3Flags, + }, + }, + { + name: "single+multiple: sampled only", + encoding: b3.B3SingleHeader | b3.B3MultipleHeader, + sc: trace.SpanContext{ + TraceFlags: trace.FlagsSampled, + }, + wantHeaders: map[string]string{ + b3Context: "1", + b3Sampled: "1", + }, + doNotWantHeaders: []string{ + b3TraceID, + b3SpanID, + b3ParentSpanID, + b3Flags, + }, + }, + { + name: "single+multiple: debug", + encoding: b3.B3SingleHeader | b3.B3MultipleHeader, + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsDebug, + }, + wantHeaders: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Flags: "1", + b3Context: fmt.Sprintf("%s-%s-d", traceIDStr, spanIDStr), + }, + doNotWantHeaders: []string{ + b3Sampled, + b3ParentSpanID, + }, + }, + { + name: "single+multiple: debug omitting sample", + encoding: b3.B3SingleHeader | b3.B3MultipleHeader, + sc: trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled | trace.FlagsDebug, + }, + wantHeaders: map[string]string{ + b3TraceID: traceIDStr, + b3SpanID: spanIDStr, + b3Flags: "1", + b3Context: fmt.Sprintf("%s-%s-d", traceIDStr, spanIDStr), + }, + doNotWantHeaders: []string{ + b3Sampled, + b3ParentSpanID, + }, + }, +} + +var injectInvalidHeaderGenerator = []injectTest{ + { + name: "empty", + sc: trace.SpanContext{}, + }, + { + name: "missing traceID", + sc: trace.SpanContext{ + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }, + }, + { + name: "missing spanID", + sc: trace.SpanContext{ + TraceID: traceID, + TraceFlags: trace.FlagsSampled, + }, + }, + { + name: "missing traceID and spanID", + sc: trace.SpanContext{ + TraceFlags: trace.FlagsSampled, + }, + }, +} + +var injectInvalidHeader []injectTest + +func init() { + // Preform a test for each invalid injectTest with all combinations of + // encoding values. + injectInvalidHeader = make([]injectTest, 0, len(injectInvalidHeaderGenerator)*4) + allHeaders := []string{ + b3TraceID, + b3SpanID, + b3Sampled, + b3ParentSpanID, + b3Flags, + b3Context, + } + // Nothing should be set for any header regardless of encoding. + for _, t := range injectInvalidHeaderGenerator { + injectInvalidHeader = append(injectInvalidHeader, injectTest{ + name: "none: " + t.name, + sc: t.sc, + doNotWantHeaders: allHeaders, + }) + injectInvalidHeader = append(injectInvalidHeader, injectTest{ + name: "multiple: " + t.name, + encoding: b3.B3MultipleHeader, + sc: t.sc, + doNotWantHeaders: allHeaders, + }) + injectInvalidHeader = append(injectInvalidHeader, injectTest{ + name: "single: " + t.name, + encoding: b3.B3SingleHeader, + sc: t.sc, + doNotWantHeaders: allHeaders, + }) + injectInvalidHeader = append(injectInvalidHeader, injectTest{ + name: "single+multiple: " + t.name, + encoding: b3.B3SingleHeader | b3.B3MultipleHeader, + sc: t.sc, + doNotWantHeaders: allHeaders, + }) + } +} diff --git a/propagators/b3/b3_example_test.go b/propagators/b3/b3_example_test.go new file mode 100644 index 00000000000..f8d05e538b9 --- /dev/null +++ b/propagators/b3/b3_example_test.go @@ -0,0 +1,42 @@ +// 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 b3_test + +import ( + "go.opentelemetry.io/contrib/propagators/b3" + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/propagation" +) + +func ExampleB3() { + b3 := b3.B3{} + // Register the B3 propagator globally. + global.SetPropagators(propagation.New( + propagation.WithExtractors(b3), + propagation.WithInjectors(b3), + )) +} + +func ExampleB3_injectEncoding() { + // Create a B3 propagator configured to inject context with both multiple + // and single header B3 HTTP encoding. + b3 := b3.B3{ + InjectEncoding: b3.B3MultipleHeader | b3.B3SingleHeader, + } + global.SetPropagators(propagation.New( + propagation.WithExtractors(b3), + propagation.WithInjectors(b3), + )) +} diff --git a/propagators/b3/b3_integration_test.go b/propagators/b3/b3_integration_test.go new file mode 100644 index 00000000000..eaeda28cd3a --- /dev/null +++ b/propagators/b3/b3_integration_test.go @@ -0,0 +1,178 @@ +// 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 b3_test + +import ( + "context" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" + + mocktracer "go.opentelemetry.io/contrib/internal/trace" + "go.opentelemetry.io/contrib/propagators/b3" + "go.opentelemetry.io/otel/api/propagation" + "go.opentelemetry.io/otel/api/trace" +) + +var ( + mockTracer = mocktracer.NewTracer("") + _, mockSpan = mockTracer.Start(context.Background(), "") +) + +func TestExtractB3(t *testing.T) { + testGroup := []struct { + name string + tests []extractTest + }{ + { + name: "valid extract headers", + tests: extractHeaders, + }, + { + name: "invalid extract headers", + tests: extractInvalidHeaders, + }, + } + + for _, tg := range testGroup { + propagator := b3.B3{} + props := propagation.New(propagation.WithExtractors(propagator)) + + for _, tt := range tg.tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + for h, v := range tt.headers { + req.Header.Set(h, v) + } + + ctx := context.Background() + ctx = propagation.ExtractHTTP(ctx, props, req.Header) + gotSc := trace.RemoteSpanContextFromContext(ctx) + if diff := cmp.Diff(gotSc, tt.wantSc); diff != "" { + t.Errorf("%s: %s: -got +want %s", tg.name, tt.name, diff) + } + }) + } + } +} + +type testSpan struct { + trace.Span + sc trace.SpanContext +} + +func (s testSpan) SpanContext() trace.SpanContext { + return s.sc +} + +func TestInjectB3(t *testing.T) { + testGroup := []struct { + name string + tests []injectTest + }{ + { + name: "valid inject headers", + tests: injectHeader, + }, + { + name: "invalid inject headers", + tests: injectInvalidHeader, + }, + } + + for _, tg := range testGroup { + for _, tt := range tg.tests { + propagator := b3.B3{InjectEncoding: tt.encoding} + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + ctx := trace.ContextWithSpan( + context.Background(), + testSpan{ + Span: mockSpan, + sc: tt.sc, + }, + ) + propagator.Inject(ctx, req.Header) + + for h, v := range tt.wantHeaders { + got, want := req.Header.Get(h), v + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("%s: %s, header=%s: -got +want %s", tg.name, tt.name, h, diff) + } + } + for _, h := range tt.doNotWantHeaders { + v, gotOk := req.Header[h] + if diff := cmp.Diff(gotOk, false); diff != "" { + t.Errorf("%s: %s, header=%s: -got +want %s, value=%s", tg.name, tt.name, h, diff, v) + } + } + }) + } + } +} + +func TestB3Propagator_GetAllKeys(t *testing.T) { + tests := []struct { + name string + propagator b3.B3 + want []string + }{ + { + name: "no encoding specified", + propagator: b3.B3{}, + want: []string{ + b3TraceID, + b3SpanID, + b3Sampled, + b3Flags, + }, + }, + { + name: "B3MultipleHeader encoding specified", + propagator: b3.B3{InjectEncoding: b3.B3MultipleHeader}, + want: []string{ + b3TraceID, + b3SpanID, + b3Sampled, + b3Flags, + }, + }, + { + name: "B3SingleHeader encoding specified", + propagator: b3.B3{InjectEncoding: b3.B3SingleHeader}, + want: []string{ + b3Context, + }, + }, + { + name: "B3SingleHeader and B3MultipleHeader encoding specified", + propagator: b3.B3{InjectEncoding: b3.B3SingleHeader | b3.B3MultipleHeader}, + want: []string{ + b3Context, + b3TraceID, + b3SpanID, + b3Sampled, + b3Flags, + }, + }, + } + + for _, test := range tests { + if diff := cmp.Diff(test.propagator.GetAllKeys(), test.want); diff != "" { + t.Errorf("%s: GetAllKeys: -got +want %s", test.name, diff) + } + } +} diff --git a/propagators/b3/b3_propagator.go b/propagators/b3/b3_propagator.go new file mode 100644 index 00000000000..12682c3aac1 --- /dev/null +++ b/propagators/b3/b3_propagator.go @@ -0,0 +1,344 @@ +// 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 b3 + +import ( + "context" + "errors" + "strings" + + "go.opentelemetry.io/otel/api/propagation" + "go.opentelemetry.io/otel/api/trace" +) + +const ( + // Default B3 Header names. + b3ContextHeader = "b3" + b3DebugFlagHeader = "x-b3-flags" + b3TraceIDHeader = "x-b3-traceid" + b3SpanIDHeader = "x-b3-spanid" + b3SampledHeader = "x-b3-sampled" + b3ParentSpanIDHeader = "x-b3-parentspanid" + + b3TraceIDPadding = "0000000000000000" + + // B3 Single Header encoding widths. + separatorWidth = 1 // Single "-" character. + samplingWidth = 1 // Single hex character. + traceID64BitsWidth = 64 / 4 // 16 hex character Trace ID. + traceID128BitsWidth = 128 / 4 // 32 hex character Trace ID. + spanIDWidth = 16 // 16 hex character ID. + parentSpanIDWidth = 16 // 16 hex character ID. +) + +var ( + empty = trace.EmptySpanContext() + + errInvalidSampledByte = errors.New("invalid B3 Sampled found") + errInvalidSampledHeader = errors.New("invalid B3 Sampled header found") + errInvalidTraceIDHeader = errors.New("invalid B3 traceID header found") + errInvalidSpanIDHeader = errors.New("invalid B3 spanID header found") + errInvalidParentSpanIDHeader = errors.New("invalid B3 ParentSpanID header found") + errInvalidScope = errors.New("require either both traceID and spanID or none") + errInvalidScopeParent = errors.New("ParentSpanID requires both traceID and spanID to be available") + errInvalidScopeParentSingle = errors.New("ParentSpanID requires traceID, spanID and Sampled to be available") + errEmptyContext = errors.New("empty request context") + errInvalidTraceIDValue = errors.New("invalid B3 traceID value found") + errInvalidSpanIDValue = errors.New("invalid B3 spanID value found") + errInvalidParentSpanIDValue = errors.New("invalid B3 ParentSpanID value found") +) + +// Encoding is a bitmask representation of the B3 encoding type. +type Encoding uint8 + +// supports returns if e has o bit(s) set. +func (e Encoding) supports(o Encoding) bool { + return e&o == o +} + +const ( + // B3MultipleHeader is a B3 encoding that uses multiple headers to + // transmit tracing information all prefixed with `x-b3-`. + B3MultipleHeader Encoding = 1 << iota + // B3SingleHeader is a B3 encoding that uses a single header named `b3` + // to transmit tracing information. + B3SingleHeader + // B3Unspecified is an unspecified B3 encoding. + B3Unspecified Encoding = 0 +) + +// B3 propagator serializes SpanContext to/from B3 Headers. +// This propagator supports both versions of B3 headers, +// 1. Single Header: +// b3: {TraceId}-{SpanId}-{SamplingState}-{ParentSpanId} +// 2. Multiple Headers: +// x-b3-traceid: {TraceId} +// x-b3-parentspanid: {ParentSpanId} +// x-b3-spanid: {SpanId} +// x-b3-sampled: {SamplingState} +// x-b3-flags: {DebugFlag} +type B3 struct { + // InjectEncoding are the B3 encodings used when injecting trace + // information. If no encoding is specified (i.e. `B3Unspecified`) + // `B3MultipleHeader` will be used as the default. + InjectEncoding Encoding +} + +var _ propagation.HTTPPropagator = B3{} + +// Inject injects a context into the supplier as B3 headers. +// The parent span ID is omitted because it is not tracked in the +// SpanContext. +func (b3 B3) Inject(ctx context.Context, supplier propagation.HTTPSupplier) { + sc := trace.SpanFromContext(ctx).SpanContext() + + if b3.InjectEncoding.supports(B3SingleHeader) { + header := []string{} + if sc.TraceID.IsValid() && sc.SpanID.IsValid() { + header = append(header, sc.TraceID.String(), sc.SpanID.String()) + } + + if sc.TraceFlags&trace.FlagsDebug == trace.FlagsDebug { + header = append(header, "d") + } else if !(sc.TraceFlags&trace.FlagsDeferred == trace.FlagsDeferred) { + if sc.IsSampled() { + header = append(header, "1") + } else { + header = append(header, "0") + } + } + + supplier.Set(b3ContextHeader, strings.Join(header, "-")) + } + + if b3.InjectEncoding.supports(B3MultipleHeader) || b3.InjectEncoding == B3Unspecified { + if sc.TraceID.IsValid() && sc.SpanID.IsValid() { + supplier.Set(b3TraceIDHeader, sc.TraceID.String()) + supplier.Set(b3SpanIDHeader, sc.SpanID.String()) + } + + if sc.TraceFlags&trace.FlagsDebug == trace.FlagsDebug { + // Since Debug implies deferred, don't also send "X-B3-Sampled". + supplier.Set(b3DebugFlagHeader, "1") + } else if !(sc.TraceFlags&trace.FlagsDeferred == trace.FlagsDeferred) { + if sc.IsSampled() { + supplier.Set(b3SampledHeader, "1") + } else { + supplier.Set(b3SampledHeader, "0") + } + } + } +} + +// Extract extracts a context from the supplier if it contains B3 headers. +func (b3 B3) Extract(ctx context.Context, supplier propagation.HTTPSupplier) context.Context { + var ( + sc trace.SpanContext + err error + ) + + // Default to Single Header if a valid value exists. + if h := supplier.Get(b3ContextHeader); h != "" { + sc, err = extractSingle(h) + if err == nil && sc.IsValid() { + return trace.ContextWithRemoteSpanContext(ctx, sc) + } + // The Single Header value was invalid, fallback to Multiple Header. + } + + var ( + traceID = supplier.Get(b3TraceIDHeader) + spanID = supplier.Get(b3SpanIDHeader) + parentSpanID = supplier.Get(b3ParentSpanIDHeader) + sampled = supplier.Get(b3SampledHeader) + debugFlag = supplier.Get(b3DebugFlagHeader) + ) + sc, err = extractMultiple(traceID, spanID, parentSpanID, sampled, debugFlag) + if err != nil || !sc.IsValid() { + return ctx + } + return trace.ContextWithRemoteSpanContext(ctx, sc) +} + +func (b3 B3) GetAllKeys() []string { + header := []string{} + if b3.InjectEncoding.supports(B3SingleHeader) { + header = append(header, b3ContextHeader) + } + if b3.InjectEncoding.supports(B3MultipleHeader) || b3.InjectEncoding == B3Unspecified { + header = append(header, b3TraceIDHeader, b3SpanIDHeader, b3SampledHeader, b3DebugFlagHeader) + } + return header +} + +// extractMultiple reconstructs a SpanContext from header values based on B3 +// Multiple header. It is based on the implementation found here: +// https://github.com/openzipkin/zipkin-go/blob/v0.2.2/propagation/b3/spancontext.go +// and adapted to support a SpanContext. +func extractMultiple(traceID, spanID, parentSpanID, sampled, flags string) (trace.SpanContext, error) { + var ( + err error + requiredCount int + sc = trace.SpanContext{} + ) + + // correct values for an existing sampled header are "0" and "1". + // For legacy support and being lenient to other tracing implementations we + // allow "true" and "false" as inputs for interop purposes. + 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 + } + + // The only accepted value for Flags is "1". This will set Debug to + // true. All other values and omission of header will be ignored. + if flags == "1" { + sc.TraceFlags |= trace.FlagsDebug + } + + if traceID != "" { + requiredCount++ + id := traceID + if len(traceID) == 16 { + // Pad 64-bit trace IDs. + id = b3TraceIDPadding + traceID + } + if sc.TraceID, err = trace.IDFromHex(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 + } + + if parentSpanID != "" { + if requiredCount == 0 { + return empty, errInvalidScopeParent + } + // Validate parent span ID but we do not use it so do not save it. + if _, err = trace.SpanIDFromHex(parentSpanID); err != nil { + return empty, errInvalidParentSpanIDHeader + } + } + + return sc, nil +} + +// extractSingle reconstructs a SpanContext from contextHeader based on a B3 +// Single header. It is based on the implementation found here: +// https://github.com/openzipkin/zipkin-go/blob/v0.2.2/propagation/b3/spancontext.go +// and adapted to support a SpanContext. +func extractSingle(contextHeader string) (trace.SpanContext, error) { + if contextHeader == "" { + return empty, errEmptyContext + } + + var ( + sc = trace.SpanContext{} + sampling string + ) + + headerLen := len(contextHeader) + + if headerLen == samplingWidth { + sampling = contextHeader + } else if headerLen == traceID64BitsWidth || headerLen == traceID128BitsWidth { + // Trace ID by itself is invalid. + return empty, errInvalidScope + } else if headerLen >= traceID64BitsWidth+spanIDWidth+separatorWidth { + pos := 0 + var traceID string + if string(contextHeader[traceID64BitsWidth]) == "-" { + // traceID must be 64 bits + pos += traceID64BitsWidth // {traceID} + traceID = b3TraceIDPadding + string(contextHeader[0:pos]) + } else if string(contextHeader[32]) == "-" { + // traceID must be 128 bits + pos += traceID128BitsWidth // {traceID} + traceID = string(contextHeader[0:pos]) + } else { + return empty, errInvalidTraceIDValue + } + var err error + sc.TraceID, err = trace.IDFromHex(traceID) + if err != nil { + return empty, errInvalidTraceIDValue + } + pos += separatorWidth // {traceID}- + + sc.SpanID, err = trace.SpanIDFromHex(contextHeader[pos : pos+spanIDWidth]) + if err != nil { + return empty, errInvalidSpanIDValue + } + pos += spanIDWidth // {traceID}-{spanID} + + if headerLen > pos { + if headerLen == pos+separatorWidth { + // {traceID}-{spanID}- is invalid. + return empty, errInvalidSampledByte + } + pos += separatorWidth // {traceID}-{spanID}- + + if headerLen == pos+samplingWidth { + sampling = string(contextHeader[pos]) + } else if headerLen == pos+parentSpanIDWidth { + // {traceID}-{spanID}-{parentSpanID} is invalid. + return empty, errInvalidScopeParentSingle + } else if headerLen == pos+samplingWidth+separatorWidth+parentSpanIDWidth { + sampling = string(contextHeader[pos]) + pos += samplingWidth + separatorWidth // {traceID}-{spanID}-{sampling}- + + // Validate parent span ID but we do not use it so do not + // save it. + _, err = trace.SpanIDFromHex(contextHeader[pos:]) + if err != nil { + return empty, errInvalidParentSpanIDValue + } + } else { + return empty, errInvalidParentSpanIDValue + } + } + } else { + return empty, errInvalidTraceIDValue + } + switch sampling { + case "": + sc.TraceFlags = trace.FlagsDeferred + case "d": + sc.TraceFlags = trace.FlagsDebug + case "1": + sc.TraceFlags = trace.FlagsSampled + case "0": + // Zero value for TraceFlags sample bit is unset. + default: + return empty, errInvalidSampledByte + } + + return sc, nil +} diff --git a/propagators/b3/b3_propagator_test.go b/propagators/b3/b3_propagator_test.go new file mode 100644 index 00000000000..e31f5818996 --- /dev/null +++ b/propagators/b3/b3_propagator_test.go @@ -0,0 +1,325 @@ +// 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 b3 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/api/trace" +) + +var ( + traceID = trace.ID{0, 0, 0, 0, 0, 0, 0, 0x7b, 0, 0, 0, 0, 0, 0, 0x1, 0xc8} + traceIDStr = "000000000000007b00000000000001c8" + spanID = trace.SpanID{0, 0, 0, 0, 0, 0, 0, 0x7b} + spanIDStr = "000000000000007b" +) + +func TestExtractMultiple(t *testing.T) { + tests := []struct { + traceID string + spanID string + parentSpanID string + sampled string + flags string + expected trace.SpanContext + err error + }{ + { + "", "", "", "0", "", + trace.SpanContext{}, + nil, + }, + { + "", "", "", "", "", + trace.SpanContext{TraceFlags: trace.FlagsDeferred}, + nil, + }, + { + "", "", "", "1", "", + trace.SpanContext{TraceFlags: trace.FlagsSampled}, + nil, + }, + { + "", "", "", "", "1", + trace.SpanContext{TraceFlags: trace.FlagsDeferred | trace.FlagsDebug}, + nil, + }, + { + "", "", "", "0", "1", + trace.SpanContext{TraceFlags: trace.FlagsDebug}, + nil, + }, + { + "", "", "", "1", "1", + trace.SpanContext{TraceFlags: trace.FlagsSampled | trace.FlagsDebug}, + nil, + }, + { + traceIDStr, spanIDStr, "", "", "", + trace.SpanContext{TraceID: traceID, SpanID: spanID, TraceFlags: trace.FlagsDeferred}, + nil, + }, + { + traceIDStr, spanIDStr, "", "0", "", + trace.SpanContext{TraceID: traceID, SpanID: spanID}, + nil, + }, + // Ensure backwards compatibility. + { + traceIDStr, spanIDStr, "", "false", "", + trace.SpanContext{TraceID: traceID, SpanID: spanID}, + nil, + }, + { + traceIDStr, spanIDStr, "", "1", "", + trace.SpanContext{TraceID: traceID, SpanID: spanID, TraceFlags: trace.FlagsSampled}, + nil, + }, + // Ensure backwards compatibility. + { + traceIDStr, spanIDStr, "", "true", "", + trace.SpanContext{TraceID: traceID, SpanID: spanID, TraceFlags: trace.FlagsSampled}, + nil, + }, + { + traceIDStr, spanIDStr, "", "a", "", + empty, + errInvalidSampledHeader, + }, + { + traceIDStr, spanIDStr, "", "1", "1", + trace.SpanContext{TraceID: traceID, SpanID: spanID, TraceFlags: trace.FlagsSampled | trace.FlagsDebug}, + nil, + }, + // Invalid flags are discarded. + { + traceIDStr, spanIDStr, "", "1", "invalid", + trace.SpanContext{TraceID: traceID, SpanID: spanID, TraceFlags: trace.FlagsSampled}, + nil, + }, + // Support short trace IDs. + { + "00000000000001c8", spanIDStr, "", "0", "", + trace.SpanContext{ + TraceID: trace.ID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x1, 0xc8}, + SpanID: spanID, + }, + nil, + }, + { + "00000000000001c", spanIDStr, "", "0", "", + empty, + errInvalidTraceIDHeader, + }, + { + "00000000000001c80", spanIDStr, "", "0", "", + empty, + errInvalidTraceIDHeader, + }, + { + traceIDStr[:len(traceIDStr)-2], spanIDStr, "", "0", "", + empty, + errInvalidTraceIDHeader, + }, + { + traceIDStr + "0", spanIDStr, "", "0", "", + empty, + errInvalidTraceIDHeader, + }, + { + traceIDStr, "00000000000001c", "", "0", "", + empty, + errInvalidSpanIDHeader, + }, + { + traceIDStr, "00000000000001c80", "", "0", "", + empty, + errInvalidSpanIDHeader, + }, + { + traceIDStr, "", "", "0", "", + empty, + errInvalidScope, + }, + { + "", spanIDStr, "", "0", "", + empty, + errInvalidScope, + }, + { + "", "", spanIDStr, "0", "", + empty, + errInvalidScopeParent, + }, + { + traceIDStr, spanIDStr, "00000000000001c8", "0", "", + trace.SpanContext{TraceID: traceID, SpanID: spanID}, + nil, + }, + { + traceIDStr, spanIDStr, "00000000000001c", "0", "", + empty, + errInvalidParentSpanIDHeader, + }, + { + traceIDStr, spanIDStr, "00000000000001c80", "0", "", + empty, + errInvalidParentSpanIDHeader, + }, + } + + for _, test := range tests { + actual, err := extractMultiple( + test.traceID, + test.spanID, + test.parentSpanID, + test.sampled, + test.flags, + ) + info := []interface{}{ + "trace ID: %q, span ID: %q, parent span ID: %q, sampled: %q, flags: %q", + test.traceID, + test.spanID, + test.parentSpanID, + test.sampled, + test.flags, + } + if !assert.Equal(t, test.err, err, info...) { + continue + } + assert.Equal(t, test.expected, actual, info...) + } +} + +func TestExtractSingle(t *testing.T) { + tests := []struct { + header string + expected trace.SpanContext + err error + }{ + {"0", trace.SpanContext{}, nil}, + {"1", trace.SpanContext{TraceFlags: trace.FlagsSampled}, nil}, + {"d", trace.SpanContext{TraceFlags: trace.FlagsDebug}, nil}, + {"a", empty, errInvalidSampledByte}, + {"3", empty, errInvalidSampledByte}, + {"000000000000007b", empty, errInvalidScope}, + {"000000000000007b00000000000001c8", empty, errInvalidScope}, + // Support short trace IDs. + { + "00000000000001c8-000000000000007b", + trace.SpanContext{ + TraceID: trace.ID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x1, 0xc8}, + SpanID: spanID, + TraceFlags: trace.FlagsDeferred, + }, + nil, + }, + { + "000000000000007b00000000000001c8-000000000000007b", + trace.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsDeferred, + }, + nil, + }, + { + "000000000000007b00000000000001c8-000000000000007b-", + empty, + errInvalidSampledByte, + }, + { + "000000000000007b00000000000001c8-000000000000007b-3", + empty, + errInvalidSampledByte, + }, + { + "000000000000007b00000000000001c8-000000000000007b-00000000000001c8", + empty, + errInvalidScopeParentSingle, + }, + { + "000000000000007b00000000000001c8-000000000000007b-1", + trace.SpanContext{TraceID: traceID, SpanID: spanID, TraceFlags: trace.FlagsSampled}, + nil, + }, + // ParentSpanID is discarded, but should still result in a parsable header. + { + "000000000000007b00000000000001c8-000000000000007b-1-00000000000001c8", + trace.SpanContext{TraceID: traceID, SpanID: spanID, TraceFlags: trace.FlagsSampled}, + nil, + }, + { + "000000000000007b00000000000001c8-000000000000007b-1-00000000000001c", + empty, + errInvalidParentSpanIDValue, + }, + {"", empty, errEmptyContext}, + } + + for _, test := range tests { + actual, err := extractSingle(test.header) + if !assert.Equal(t, test.err, err, "header: %s", test.header) { + continue + } + assert.Equal(t, test.expected, actual, "header: %s", test.header) + } +} + +func TestB3EncodingOperations(t *testing.T) { + encodings := []Encoding{ + B3MultipleHeader, + B3SingleHeader, + B3Unspecified, + } + + // Test for overflow (or something really unexpected). + for i, e := range encodings { + for j := i + 1; j < i+len(encodings); j++ { + o := encodings[j%len(encodings)] + assert.False(t, e == o, "%v == %v", e, o) + } + } + + // B3Unspecified is a special case, it supports only itself, but is + // supported by everything. + assert.True(t, B3Unspecified.supports(B3Unspecified)) + for _, e := range encodings[:len(encodings)-1] { + assert.False(t, B3Unspecified.supports(e), e) + assert.True(t, e.supports(B3Unspecified), e) + } + + // Skip the special case for B3Unspecified. + for i, e := range encodings[:len(encodings)-1] { + // Everything should support itself. + assert.True(t, e.supports(e)) + for j := i + 1; j < i+len(encodings); j++ { + o := encodings[j%len(encodings)] + // Any "or" combination should be supportive of an operand. + assert.True(t, (e | o).supports(e), "(%[0]v|%[1]v).supports(%[0]v)", e, o) + // Bitmasks should be unique. + assert.False(t, o.supports(e), "%v.supports(%v)", o, e) + } + } + + // Encoding.supports should be more inclusive than equality. + all := ^B3Unspecified + for _, e := range encodings { + assert.True(t, all.supports(e)) + } +} diff --git a/propagators/b3/doc.go b/propagators/b3/doc.go new file mode 100644 index 00000000000..a26b922a2b1 --- /dev/null +++ b/propagators/b3/doc.go @@ -0,0 +1,17 @@ +// 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 B3 propagator specification as defined +// at https://github.com/openzipkin/b3-propagation +package b3 // import "go.opentelemetry.io/contrib/propagators/b3" diff --git a/propagators/doc.go b/propagators/doc.go new file mode 100644 index 00000000000..883fed60484 --- /dev/null +++ b/propagators/doc.go @@ -0,0 +1,19 @@ +// 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 module contains all of its functionality in its subpackages. +// This top-level package is to allow clients to pull in all propagator +// implementations at once using +// require go.opentelemetry.io/contrib/propagators +package propagators diff --git a/propagators/go.mod b/propagators/go.mod new file mode 100644 index 00000000000..3e6e87e6773 --- /dev/null +++ b/propagators/go.mod @@ -0,0 +1,12 @@ +module go.opentelemetry.io/contrib/propagators + +go 1.14 + +replace go.opentelemetry.io/contrib => ./.. + +require ( + github.com/google/go-cmp v0.5.1 + github.com/stretchr/testify v1.6.1 + go.opentelemetry.io/contrib v0.11.0 + go.opentelemetry.io/otel v0.11.0 +) diff --git a/propagators/go.sum b/propagators/go.sum new file mode 100644 index 00000000000..eb822f841bf --- /dev/null +++ b/propagators/go.sum @@ -0,0 +1,26 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/otel v0.11.0 h1:IN2tzQa9Gc4ZVKnTaMbPVcHjvzOdg5n9QfnmlqiET7E= +go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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=