1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-02-03 13:11:53 +02:00
Tyler Yahn f1dad21e47
Move propagators out of API and into their own package (#1118)
* Add new propagators package

* Move B3 propagator to propagators

Update all `api/trace` dependencies in the propagator.

Export the isDeferred and isDebug methods of the SpanContext. These are
needed by the B3 propagator to track trace state.

* Move W3C trace context propagator to propagators

* Update package docs with supported encodings

* Move unified propagators code to own file

* Update b3 exported documentation

* Update trace_context exported documentation

* Add code examples for B3 propagator

* Add TraceContext example code

* Remove internal package

Move common testing declarations to the propagators_test.go file.

* Add changes to Changelog

* Add test to check default propagators
2020-09-08 19:07:59 -04:00

344 lines
11 KiB
Go

// 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 propagators
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")
)
// B3Encoding is a bitmask representation of the B3 encoding type.
type B3Encoding uint8
// supports returns if e has o bit(s) set.
func (e B3Encoding) supports(o B3Encoding) 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 B3Encoding = 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 B3Encoding = 0
)
// B3 is a propagator that supports the B3 HTTP encoding.
//
// Both single (https://github.com/openzipkin/b3-propagation#single-header)
// and multiple (https://github.com/openzipkin/b3-propagation#single-header)
// header encoding are supported.
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 as it is the most
// backwards compatible.
InjectEncoding B3Encoding
}
var _ propagation.HTTPPropagator = B3{}
// Inject injects a context into the supplier as B3 HTTP 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.IsDebug() {
header = append(header, "d")
} else if !sc.IsDeferred() {
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.IsDebug() {
// Since Debug implies deferred, don't also send "X-B3-Sampled".
supplier.Set(b3DebugFlagHeader, "1")
} else if !sc.IsDeferred() {
if sc.IsSampled() {
supplier.Set(b3SampledHeader, "1")
} else {
supplier.Set(b3SampledHeader, "0")
}
}
}
}
// Extract extracts a context from the supplier if it contains B3 HTTP
// 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)
}
// GetAllKeys returns the HTTP header names this propagator will use when
// injecting.
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
}