Skip to content

Commit

Permalink
Add AWS X-Ray Propagator (#248)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
KKelvinLo authored Nov 21, 2020
1 parent 1010e02 commit f58734f
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Added

- `otelhttp.{Get,Head,Post,PostForm}` convenience wrappers for their `http` counterparts. (#390)
- Add propagator for AWS X-Ray (#248)

### Changed

Expand Down
181 changes: 181 additions & 0 deletions propagators/awsxray/awsxray_propagator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// 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 aws

import (
"context"
"errors"
"strings"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/api/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.EmptySpanContext()
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, should be a string of hex, lowercase trace id can't be all zero")
errInvalidSpanIDLength = errors.New("invalid span id length, must be 16")
)

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

// Asserts that the propagator implements the otel.textMapPropagator interface
var _ otel.TextMapPropagator = &Xray{}

// Inject injects a context to the carrier following AWS X-Ray format.
func (awsxray Xray) Inject(ctx context.Context, carrier otel.TextMapCarrier) {
sc := trace.SpanFromContext(ctx).SpanContext()
headers := []string{}
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 = append(headers, 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 (awsxray Xray) Extract(ctx context.Context, carrier otel.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
}

//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, errMalformedTraceID
}
} 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
}

//returns position of the first occurrence of a substring 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
}

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

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.IDFromHex(result)
}

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

func (awsxray Xray) Fields() []string {
return []string{traceHeaderKey}
}
104 changes: 104 additions & 0 deletions propagators/awsxray/awsxray_propagator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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 aws

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"

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

var (
traceID = trace.ID{0x8a, 0x3c, 0x60, 0xf7, 0xd1, 0x88, 0xf8, 0xfa, 0x79, 0xd4, 0x8a, 0x39, 0x1a, 0x77, 0x8f, 0xa6}
xrayTraceID = "1-8a3c60f7-d188f8fa79d48a391a778fa6"
parentID64Str = "53995c3f42cd8ad8"
parentSpanID = trace.SpanID{0x53, 0x99, 0x5c, 0x3f, 0x42, 0xcd, 0x8a, 0xd8}
zeroSpanIDStr = "0000000000000000"
zeroTraceIDStr = "1-00000000-000000000000000000000000"
invalidTraceHeaderID = "1b00000000b000000000000000000000000"
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,
},
{
zeroTraceIDStr, parentID64Str, isSampled,
trace.SpanContext{},
errMalformedTraceID,
},
{
xrayTraceID, zeroSpanIDStr, isSampled,
trace.SpanContext{},
errInvalidSpanIDLength,
},
{
invalidTraceHeaderID, parentID64Str, isSampled,
trace.SpanContext{},
errMalformedTraceID,
},
{
wrongVersionTraceHeaderID, parentID64Str, isSampled,
trace.SpanContext{},
errMalformedTraceID,
},
}

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 f58734f

Please sign in to comment.