Skip to content

Commit

Permalink
Add AWS X-Ray Propagator (#462)
Browse files Browse the repository at this point in the history
* Update CHANGELOG.md

* add space before comment

* Add AWS X-Ray Propagator (#248)

* add AWS X-Ray propagator and tests

* made naming convention changes to adhere to linter

* added comment to interface implementation for  clarity

* update naming convention and added description for method

* add space before comment

* update propagator to use v0.14.0 release changes

* refactor folder and update error messages for clarity

* refactored file structure and naming

* update comment for Propagator struct
  • Loading branch information
KKelvinLo authored Dec 1, 2020
1 parent f9f5a21 commit e1c598c
Show file tree
Hide file tree
Showing 3 changed files with 282 additions and 1 deletion.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- `otelhttp.{Get,Head,Post,PostForm}` convenience wrappers for their `http` counterparts. (#390)
- The AWS detector now adds the cloud zone, host image ID, host type, and host name to the returned `Resource`. (#410)
- Add Amazon ECS Resource Detector for AWS X-Ray. (#466)

- Add propagator for AWS X-Ray (#462)

### Changed

- Add semantic version to `Tracer` / `Meter` created by instrumentation packages `otelsaram`, `otelrestful`, `otelmongo`, `otelhttp` and `otelhttptrace`. (#412)
Expand Down
182 changes: 182 additions & 0 deletions propagators/aws/xray/propagator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// 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 xray

import (
"context"
"errors"
"strings"

"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)

const (
traceHeaderKey = "X-Amzn-Trace-Id"
traceHeaderDelimiter = ";"
kvDelimiter = "="
traceIDKey = "Root"
sampleFlagKey = "Sampled"
parentIDKey = "Parent"
traceIDVersion = "1"
traceIDDelimiter = "-"
isSampled = "1"
notSampled = "0"

traceFlagNone = 0x0
traceFlagSampled = 0x1 << 0
traceIDLength = 35
traceIDDelimitterIndex1 = 1
traceIDDelimitterIndex2 = 10
traceIDFirstPartLength = 8
sampledFlagLength = 1
)

var (
empty = trace.SpanContext{}
errInvalidTraceHeader = errors.New("invalid X-Amzn-Trace-Id header value, should contain 3 different part separated by ;")
errMalformedTraceID = errors.New("cannot decode trace ID from header")
errLengthTraceIDHeader = errors.New("incorrect length of X-Ray trace ID found, 35 character length expected")
errInvalidTraceIDVersion = errors.New("invalid X-Ray trace ID header found, does not have valid trace ID version")
errInvalidSpanIDLength = errors.New("invalid span ID length, must be 16")
)

// Propagator serializes Span Context to/from AWS X-Ray headers.
//
// Example AWS X-Ray format:
//
// X-Amzn-Trace-Id: Root={traceId};Parent={parentId};Sampled={samplingFlag}
type Propagator struct{}

// Asserts that the propagator implements the otel.TextMapPropagator interface at compile time.
var _ propagation.TextMapPropagator = &Propagator{}

// Inject injects a context to the carrier following AWS X-Ray format.
func (xray Propagator) Inject(ctx context.Context, carrier propagation.TextMapCarrier) {
sc := trace.SpanFromContext(ctx).SpanContext()
if !sc.TraceID.IsValid() || !sc.SpanID.IsValid() {
return
}
otTraceID := sc.TraceID.String()
xrayTraceID := traceIDVersion + traceIDDelimiter + otTraceID[0:traceIDFirstPartLength] +
traceIDDelimiter + otTraceID[traceIDFirstPartLength:]
parentID := sc.SpanID
samplingFlag := notSampled
if sc.TraceFlags == traceFlagSampled {
samplingFlag = isSampled
}
headers := []string{traceIDKey, kvDelimiter, xrayTraceID, traceHeaderDelimiter, parentIDKey,
kvDelimiter, parentID.String(), traceHeaderDelimiter, sampleFlagKey, kvDelimiter, samplingFlag}

carrier.Set(traceHeaderKey, strings.Join(headers, ""))
}

// Extract gets a context from the carrier if it contains AWS X-Ray headers.
func (xray Propagator) Extract(ctx context.Context, carrier propagation.TextMapCarrier) context.Context {
// extract tracing information
if header := carrier.Get(traceHeaderKey); header != "" {
sc, err := extract(header)
if err == nil && sc.IsValid() {
return trace.ContextWithRemoteSpanContext(ctx, sc)
}
}
return ctx
}

// extract extracts Span Context from context.
func extract(headerVal string) (trace.SpanContext, error) {
var (
sc = trace.SpanContext{}
err error
delimiterIndex int
part string
)
pos := 0
for pos < len(headerVal) {
delimiterIndex = indexOf(headerVal, traceHeaderDelimiter, pos)
if delimiterIndex >= 0 {
part = headerVal[pos:delimiterIndex]
pos = delimiterIndex + 1
} else {
//last part
part = strings.TrimSpace(headerVal[pos:])
pos = len(headerVal)
}
equalsIndex := strings.Index(part, kvDelimiter)
if equalsIndex < 0 {
return empty, errInvalidTraceHeader
}
value := part[equalsIndex+1:]
if strings.HasPrefix(part, traceIDKey) {
sc.TraceID, err = parseTraceID(value)
if err != nil {
return empty, err
}
} else if strings.HasPrefix(part, parentIDKey) {
//extract parentId
sc.SpanID, err = trace.SpanIDFromHex(value)
if err != nil {
return empty, errInvalidSpanIDLength
}
} else if strings.HasPrefix(part, sampleFlagKey) {
//extract traceflag
sc.TraceFlags = parseTraceFlag(value)
}
}
return sc, nil
}

// indexOf returns position of the first occurrence of a substr in str starting at pos index.
func indexOf(str string, substr string, pos int) int {
index := strings.Index(str[pos:], substr)
if index > -1 {
index += pos
}
return index
}

// parseTraceID returns trace ID if valid else return invalid trace ID.
func parseTraceID(xrayTraceID string) (trace.TraceID, error) {
if len(xrayTraceID) != traceIDLength {
return empty.TraceID, errLengthTraceIDHeader
}
if !strings.HasPrefix(xrayTraceID, traceIDVersion) {
return empty.TraceID, errInvalidTraceIDVersion
}

if xrayTraceID[traceIDDelimitterIndex1:traceIDDelimitterIndex1+1] != traceIDDelimiter ||
xrayTraceID[traceIDDelimitterIndex2:traceIDDelimitterIndex2+1] != traceIDDelimiter {
return empty.TraceID, errMalformedTraceID
}

epochPart := xrayTraceID[traceIDDelimitterIndex1+1 : traceIDDelimitterIndex2]
uniquePart := xrayTraceID[traceIDDelimitterIndex2+1 : traceIDLength]

result := epochPart + uniquePart
return trace.TraceIDFromHex(result)
}

// parseTraceFlag returns a parsed trace flag.
func parseTraceFlag(xraySampledFlag string) byte {
if len(xraySampledFlag) == sampledFlagLength && xraySampledFlag != isSampled {
return traceFlagNone
}
return trace.FlagsSampled
}

// Fields returns list of fields used by HTTPTextFormat.
func (xray Propagator) Fields() []string {
return []string{traceHeaderKey}
}
98 changes: 98 additions & 0 deletions propagators/aws/xray/propagator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// 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 xray

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"

"go.opentelemetry.io/otel/trace"
)

var (
traceID = trace.TraceID{0x8a, 0x3c, 0x60, 0xf7, 0xd1, 0x88, 0xf8, 0xfa, 0x79, 0xd4, 0x8a, 0x39, 0x1a, 0x77, 0x8f, 0xa6}
xrayTraceID = "1-8a3c60f7-d188f8fa79d48a391a778fa6"
xrayTraceIDIncorrectLength = "1-82138-1203123"
parentID64Str = "53995c3f42cd8ad8"
parentSpanID = trace.SpanID{0x53, 0x99, 0x5c, 0x3f, 0x42, 0xcd, 0x8a, 0xd8}
zeroSpanIDStr = "0000000000000000"
wrongVersionTraceHeaderID = "5b00000000b000000000000000000000000"
)

func TestAwsXrayExtract(t *testing.T) {
testData := []struct {
traceID string
parentSpanID string
samplingFlag string
expected trace.SpanContext
err error
}{
{
xrayTraceID, parentID64Str, notSampled,
trace.SpanContext{
TraceID: traceID,
SpanID: parentSpanID,
TraceFlags: traceFlagNone,
},
nil,
},
{
xrayTraceID, parentID64Str, isSampled,
trace.SpanContext{
TraceID: traceID,
SpanID: parentSpanID,
TraceFlags: traceFlagSampled,
},
nil,
},
{
xrayTraceID, zeroSpanIDStr, isSampled,
trace.SpanContext{},
errInvalidSpanIDLength,
},
{
xrayTraceIDIncorrectLength, parentID64Str, isSampled,
trace.SpanContext{},
errLengthTraceIDHeader,
},
{
wrongVersionTraceHeaderID, parentID64Str, isSampled,
trace.SpanContext{},
errInvalidTraceIDVersion,
},
}

for _, test := range testData {
headerVal := strings.Join([]string{traceIDKey, kvDelimiter, test.traceID, traceHeaderDelimiter, parentIDKey, kvDelimiter,
test.parentSpanID, traceHeaderDelimiter, sampleFlagKey, kvDelimiter, test.samplingFlag}, "")

sc, err := extract(headerVal)

info := []interface{}{
"trace ID: %q, parent span ID: %q, sampling flag: %q",
test.traceID,
test.parentSpanID,
test.samplingFlag,
}

if !assert.Equal(t, test.err, err, info...) {
continue
}

assert.Equal(t, test.expected, sc, info...)
}
}

0 comments on commit e1c598c

Please sign in to comment.