Add tracecontextb3 HTTPFormats (#1429)

* Add a b3tracecontext.HTTPFormat.

It will utilize either B3 or TraceContext propagation formats coming in (preferring TraceContext) and while sending both.

* hack/update-deps.sh

* PR comments.

* Move to HTTPFormatSequence.

* Remove the struct.

* Allow distinct ingress and egress formats.
This commit is contained in:
Adam Harwayne 2020-06-23 10:35:27 -07:00 committed by GitHub
parent aa30bc3ac0
commit 5658d93fb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 635 additions and 2 deletions

View File

@ -34,9 +34,9 @@ import (
"knative.dev/pkg/test/ingress"
"knative.dev/pkg/test/logging"
"knative.dev/pkg/test/zipkin"
"knative.dev/pkg/tracing/propagation/tracecontextb3"
"go.opencensus.io/plugin/ochttp"
"go.opencensus.io/plugin/ochttp/propagation/b3"
"go.opencensus.io/trace"
)
@ -135,7 +135,7 @@ func New(
// Enable Zipkin tracing
roundTripper := &ochttp.Transport{
Base: transport,
Propagation: &b3.HTTPFormat{},
Propagation: tracecontextb3.TraceContextEgress,
}
sc := SpoofingClient{

View File

@ -22,6 +22,7 @@ import (
"go.opencensus.io/plugin/ochttp"
"go.opencensus.io/trace"
"k8s.io/apimachinery/pkg/util/sets"
"knative.dev/pkg/tracing/propagation/tracecontextb3"
)
var (
@ -50,6 +51,7 @@ func HTTPSpanIgnoringPaths(pathsToIgnore ...string) func(http.Handler) http.Hand
}
return underlyingSampling
},
Propagation: tracecontextb3.TraceContextEgress,
}
}
}

View File

@ -0,0 +1,53 @@
/*
Copyright 2020 The Knative 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 propagation
import (
"net/http"
"go.opencensus.io/trace"
"go.opencensus.io/trace/propagation"
)
// HTTPFormatSequence is a propagation.HTTPFormat that applies multiple other propagation formats.
// For incoming requests, it will use the first SpanContext it can find, checked in the order of
// HTTPFormatSequence.Ingress.
// For outgoing requests, it will apply all the formats to the outgoing request, in the order of
// HTTPFormatSequence.Egress.
type HTTPFormatSequence struct {
Ingress []propagation.HTTPFormat
Egress []propagation.HTTPFormat
}
var _ propagation.HTTPFormat = (*HTTPFormatSequence)(nil)
// SpanContextFromRequest satisfies the propagation.HTTPFormat interface.
func (h *HTTPFormatSequence) SpanContextFromRequest(req *http.Request) (trace.SpanContext, bool) {
for _, format := range h.Ingress {
if sc, ok := format.SpanContextFromRequest(req); ok {
return sc, true
}
}
return trace.SpanContext{}, false
}
// SpanContextToRequest satisfies the propagation.HTTPFormat interface.
func (h *HTTPFormatSequence) SpanContextToRequest(sc trace.SpanContext, req *http.Request) {
for _, format := range h.Egress {
format.SpanContextToRequest(sc, req)
}
}

View File

@ -0,0 +1,150 @@
/*
Copyright 2020 The Knative 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 propagation
import (
"fmt"
"net/http"
"testing"
"github.com/google/go-cmp/cmp"
"go.opencensus.io/plugin/ochttp/propagation/b3"
"go.opencensus.io/plugin/ochttp/propagation/tracecontext"
"go.opencensus.io/trace"
"go.opencensus.io/trace/propagation"
_ "knative.dev/pkg/metrics/testing"
)
var (
sampled trace.TraceOptions = 1
notSampled trace.TraceOptions = 1
traceID = trace.TraceID{99, 108, 97, 101, 114, 115, 105, 103, 104, 116, 101, 100, 110, 101, 115, 115}
spanID = trace.SpanID{107, 110, 97, 116, 105, 118, 101, 0}
tracePropagators = []propagation.HTTPFormat{
&b3.HTTPFormat{},
&tracecontext.HTTPFormat{},
both,
}
)
var both = &HTTPFormatSequence{
Ingress: []propagation.HTTPFormat{
&tracecontext.HTTPFormat{},
&b3.HTTPFormat{},
},
Egress: []propagation.HTTPFormat{
&tracecontext.HTTPFormat{},
&b3.HTTPFormat{},
},
}
func TestSpanContextFromRequest(t *testing.T) {
testCases := map[string]struct {
sc *trace.SpanContext
}{
"no incoming trace": {},
"not sampled": {
sc: &trace.SpanContext{
TraceID: traceID,
SpanID: spanID,
TraceOptions: notSampled,
},
},
"sampled": {
sc: &trace.SpanContext{
TraceID: traceID,
SpanID: spanID,
TraceOptions: sampled,
},
},
}
for _, tracePropagator := range tracePropagators {
for n, tc := range testCases {
t.Run(fmt.Sprintf("%T-%s", tracePropagator, n), func(t *testing.T) {
r := &http.Request{}
r.Header = http.Header{}
if tc.sc != nil {
tracePropagator.SpanContextToRequest(*tc.sc, r)
}
// Check we extract the correct SpanContext with both the original and the
// 'both' propagators.
for _, extractFormat := range []propagation.HTTPFormat{tracePropagator, both} {
actual, ok := extractFormat.SpanContextFromRequest(r)
if tc.sc == nil {
if ok {
t.Errorf("Expected no span context using %T, found %v", extractFormat, actual)
}
continue
}
if diff := cmp.Diff(*tc.sc, actual); diff != "" {
t.Errorf("Unexpected span context using %T (-want +got): %s", extractFormat, diff)
}
}
})
}
}
}
func TestSpanContextToRequest(t *testing.T) {
testCases := map[string]struct {
sc *trace.SpanContext
}{
"no incoming trace": {},
"not sampled": {
sc: &trace.SpanContext{
TraceID: traceID,
SpanID: spanID,
TraceOptions: notSampled,
},
},
"sampled": {
sc: &trace.SpanContext{
TraceID: traceID,
SpanID: spanID,
TraceOptions: sampled,
},
},
}
for n, tc := range testCases {
t.Run(n, func(t *testing.T) {
r := &http.Request{}
r.Header = http.Header{}
if tc.sc != nil {
// Apply using the TraceContextB3 propagator.
both.SpanContextToRequest(*tc.sc, r)
}
// Verify that we extract the correct SpanContext with all three formats.
for _, tracePropagator := range tracePropagators {
actual, ok := tracePropagator.SpanContextFromRequest(r)
if tc.sc == nil {
if ok {
t.Errorf("Expected no span context using %T, found %v", tracePropagator, actual)
}
continue
}
if diff := cmp.Diff(*tc.sc, actual); diff != "" {
t.Errorf("Unexpected span context using %T (-want +got): %s", tracePropagator, diff)
}
}
})
}
}

View File

@ -0,0 +1,61 @@
/*
Copyright 2020 The Knative 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 tracecontextb3
import (
"go.opencensus.io/plugin/ochttp/propagation/b3"
"go.opencensus.io/plugin/ochttp/propagation/tracecontext"
ocpropagation "go.opencensus.io/trace/propagation"
"knative.dev/pkg/tracing/propagation"
)
// TraceContextB3Egress is a propagation.HTTPFormat that reads both TraceContext and B3 tracing
// formats, preferring TraceContext. It always writes both formats.
var TraceContextB3Egress = &propagation.HTTPFormatSequence{
Ingress: []ocpropagation.HTTPFormat{
&tracecontext.HTTPFormat{},
&b3.HTTPFormat{},
},
Egress: []ocpropagation.HTTPFormat{
&tracecontext.HTTPFormat{},
&b3.HTTPFormat{},
},
}
// TraceContextEgress is a propagation.HTTPFormat that reads both TraceContext and B3 tracing
// formats, preferring TraceContext. It always writes TraceContext format exclusively.
var TraceContextEgress = &propagation.HTTPFormatSequence{
Ingress: []ocpropagation.HTTPFormat{
&tracecontext.HTTPFormat{},
&b3.HTTPFormat{},
},
Egress: []ocpropagation.HTTPFormat{
&tracecontext.HTTPFormat{},
},
}
// B3Egress is a propagation.HTTPFormat that reads both TraceContext and B3 tracing formats,
// preferring TraceContext. It always writes B3 format exclusively.
var B3Egress = &propagation.HTTPFormatSequence{
Ingress: []ocpropagation.HTTPFormat{
&tracecontext.HTTPFormat{},
&b3.HTTPFormat{},
},
Egress: []ocpropagation.HTTPFormat{
&b3.HTTPFormat{},
},
}

View File

@ -0,0 +1,179 @@
/*
Copyright 2020 The Knative 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 tracecontextb3
import (
"net/http"
"testing"
"github.com/google/go-cmp/cmp"
"go.opencensus.io/plugin/ochttp/propagation/b3"
"go.opencensus.io/plugin/ochttp/propagation/tracecontext"
"go.opencensus.io/trace"
ocpropagation "go.opencensus.io/trace/propagation"
_ "knative.dev/pkg/metrics/testing"
)
var (
sampled trace.TraceOptions = 1
traceID = trace.TraceID{99, 108, 97, 101, 114, 115, 105, 103, 104, 116, 101, 100, 110, 101, 115, 115}
spanID = trace.SpanID{107, 110, 97, 116, 105, 118, 101, 0}
spanContext = trace.SpanContext{
TraceID: traceID,
SpanID: spanID,
TraceOptions: sampled,
}
)
func TestTraceContextB3Egress_Ingress(t *testing.T) {
testCases := map[string]struct {
ingress ocpropagation.HTTPFormat
}{
"traceContext": {
ingress: &tracecontext.HTTPFormat{},
},
"b3": {
ingress: &b3.HTTPFormat{},
},
}
for n, tc := range testCases {
t.Run(n, func(t *testing.T) {
r := &http.Request{}
r.Header = http.Header{}
tc.ingress.SpanContextToRequest(spanContext, r)
assertFormatReadsSpanContext(t, r, TraceContextB3Egress)
})
}
}
func TestTraceContextB3Egress_Egress(t *testing.T) {
testCases := map[string]struct {
egress ocpropagation.HTTPFormat
}{
"traceContext": {
egress: &tracecontext.HTTPFormat{},
},
"b3": {
egress: &b3.HTTPFormat{},
},
}
for n, tc := range testCases {
t.Run(n, func(t *testing.T) {
r := &http.Request{}
r.Header = http.Header{}
TraceContextB3Egress.SpanContextToRequest(spanContext, r)
assertFormatReadsSpanContext(t, r, tc.egress)
})
}
}
func TestTraceContextEgress_Ingress(t *testing.T) {
testCases := map[string]struct {
ingress ocpropagation.HTTPFormat
}{
"traceContext": {
ingress: &tracecontext.HTTPFormat{},
},
"b3": {
ingress: &b3.HTTPFormat{},
},
}
for n, tc := range testCases {
t.Run(n, func(t *testing.T) {
r := &http.Request{}
r.Header = http.Header{}
tc.ingress.SpanContextToRequest(spanContext, r)
assertFormatReadsSpanContext(t, r, TraceContextEgress)
})
}
}
func TestTraceContextEgress_Egress(t *testing.T) {
testCases := map[string]struct {
egress ocpropagation.HTTPFormat
}{
"traceContext": {
egress: &tracecontext.HTTPFormat{},
},
}
for n, tc := range testCases {
t.Run(n, func(t *testing.T) {
r := &http.Request{}
r.Header = http.Header{}
TraceContextEgress.SpanContextToRequest(spanContext, r)
assertFormatReadsSpanContext(t, r, tc.egress)
})
}
}
func TestB3Egress_Ingress(t *testing.T) {
testCases := map[string]struct {
ingress ocpropagation.HTTPFormat
}{
"traceContext": {
ingress: &tracecontext.HTTPFormat{},
},
"b3": {
ingress: &b3.HTTPFormat{},
},
}
for n, tc := range testCases {
t.Run(n, func(t *testing.T) {
r := &http.Request{}
r.Header = http.Header{}
tc.ingress.SpanContextToRequest(spanContext, r)
assertFormatReadsSpanContext(t, r, B3Egress)
})
}
}
func TestB3Egress_Egress(t *testing.T) {
testCases := map[string]struct {
egress ocpropagation.HTTPFormat
}{
"b3": {
egress: &b3.HTTPFormat{},
},
}
for n, tc := range testCases {
t.Run(n, func(t *testing.T) {
r := &http.Request{}
r.Header = http.Header{}
B3Egress.SpanContextToRequest(spanContext, r)
assertFormatReadsSpanContext(t, r, tc.egress)
})
}
}
func assertFormatReadsSpanContext(t *testing.T, r *http.Request, format ocpropagation.HTTPFormat) {
sc, ok := format.SpanContextFromRequest(r)
if !ok {
t.Error("Expected to get the SpanContext")
}
if diff := cmp.Diff(spanContext, sc); diff != "" {
t.Errorf("Unexpected SpanContext (-want +got): %s", diff)
}
}

View File

@ -0,0 +1,187 @@
// Copyright 2018, OpenCensus 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 tracecontext contains HTTP propagator for TraceContext standard.
// See https://github.com/w3c/distributed-tracing for more information.
package tracecontext // import "go.opencensus.io/plugin/ochttp/propagation/tracecontext"
import (
"encoding/hex"
"fmt"
"net/http"
"net/textproto"
"regexp"
"strings"
"go.opencensus.io/trace"
"go.opencensus.io/trace/propagation"
"go.opencensus.io/trace/tracestate"
)
const (
supportedVersion = 0
maxVersion = 254
maxTracestateLen = 512
traceparentHeader = "traceparent"
tracestateHeader = "tracestate"
trimOWSRegexFmt = `^[\x09\x20]*(.*[^\x20\x09])[\x09\x20]*$`
)
var trimOWSRegExp = regexp.MustCompile(trimOWSRegexFmt)
var _ propagation.HTTPFormat = (*HTTPFormat)(nil)
// HTTPFormat implements the TraceContext trace propagation format.
type HTTPFormat struct{}
// SpanContextFromRequest extracts a span context from incoming requests.
func (f *HTTPFormat) SpanContextFromRequest(req *http.Request) (sc trace.SpanContext, ok bool) {
h, ok := getRequestHeader(req, traceparentHeader, false)
if !ok {
return trace.SpanContext{}, false
}
sections := strings.Split(h, "-")
if len(sections) < 4 {
return trace.SpanContext{}, false
}
if len(sections[0]) != 2 {
return trace.SpanContext{}, false
}
ver, err := hex.DecodeString(sections[0])
if err != nil {
return trace.SpanContext{}, false
}
version := int(ver[0])
if version > maxVersion {
return trace.SpanContext{}, false
}
if version == 0 && len(sections) != 4 {
return trace.SpanContext{}, false
}
if len(sections[1]) != 32 {
return trace.SpanContext{}, false
}
tid, err := hex.DecodeString(sections[1])
if err != nil {
return trace.SpanContext{}, false
}
copy(sc.TraceID[:], tid)
if len(sections[2]) != 16 {
return trace.SpanContext{}, false
}
sid, err := hex.DecodeString(sections[2])
if err != nil {
return trace.SpanContext{}, false
}
copy(sc.SpanID[:], sid)
opts, err := hex.DecodeString(sections[3])
if err != nil || len(opts) < 1 {
return trace.SpanContext{}, false
}
sc.TraceOptions = trace.TraceOptions(opts[0])
// Don't allow all zero trace or span ID.
if sc.TraceID == [16]byte{} || sc.SpanID == [8]byte{} {
return trace.SpanContext{}, false
}
sc.Tracestate = tracestateFromRequest(req)
return sc, true
}
// getRequestHeader returns a combined header field according to RFC7230 section 3.2.2.
// If commaSeparated is true, multiple header fields with the same field name using be
// combined using ",".
// If no header was found using the given name, "ok" would be false.
// If more than one headers was found using the given name, while commaSeparated is false,
// "ok" would be false.
func getRequestHeader(req *http.Request, name string, commaSeparated bool) (hdr string, ok bool) {
v := req.Header[textproto.CanonicalMIMEHeaderKey(name)]
switch len(v) {
case 0:
return "", false
case 1:
return v[0], true
default:
return strings.Join(v, ","), commaSeparated
}
}
// TODO(rghetia): return an empty Tracestate when parsing tracestate header encounters an error.
// Revisit to return additional boolean value to indicate parsing error when following issues
// are resolved.
// https://github.com/w3c/distributed-tracing/issues/172
// https://github.com/w3c/distributed-tracing/issues/175
func tracestateFromRequest(req *http.Request) *tracestate.Tracestate {
h, _ := getRequestHeader(req, tracestateHeader, true)
if h == "" {
return nil
}
var entries []tracestate.Entry
pairs := strings.Split(h, ",")
hdrLenWithoutOWS := len(pairs) - 1 // Number of commas
for _, pair := range pairs {
matches := trimOWSRegExp.FindStringSubmatch(pair)
if matches == nil {
return nil
}
pair = matches[1]
hdrLenWithoutOWS += len(pair)
if hdrLenWithoutOWS > maxTracestateLen {
return nil
}
kv := strings.Split(pair, "=")
if len(kv) != 2 {
return nil
}
entries = append(entries, tracestate.Entry{Key: kv[0], Value: kv[1]})
}
ts, err := tracestate.New(nil, entries...)
if err != nil {
return nil
}
return ts
}
func tracestateToRequest(sc trace.SpanContext, req *http.Request) {
var pairs = make([]string, 0, len(sc.Tracestate.Entries()))
if sc.Tracestate != nil {
for _, entry := range sc.Tracestate.Entries() {
pairs = append(pairs, strings.Join([]string{entry.Key, entry.Value}, "="))
}
h := strings.Join(pairs, ",")
if h != "" && len(h) <= maxTracestateLen {
req.Header.Set(tracestateHeader, h)
}
}
}
// SpanContextToRequest modifies the given request to include traceparent and tracestate headers.
func (f *HTTPFormat) SpanContextToRequest(sc trace.SpanContext, req *http.Request) {
h := fmt.Sprintf("%x-%x-%x-%x",
[]byte{supportedVersion},
sc.TraceID[:],
sc.SpanID[:],
[]byte{byte(sc.TraceOptions)})
req.Header.Set(traceparentHeader, h)
tracestateToRequest(sc, req)
}

1
vendor/modules.txt vendored
View File

@ -266,6 +266,7 @@ go.opencensus.io/metric/metricproducer
go.opencensus.io/plugin/ocgrpc
go.opencensus.io/plugin/ochttp
go.opencensus.io/plugin/ochttp/propagation/b3
go.opencensus.io/plugin/ochttp/propagation/tracecontext
go.opencensus.io/resource
go.opencensus.io/resource/resourcekeys
go.opencensus.io/stats